新特性:PostgreSQL 的 vacuum 漫談

收藏待读

新特性:PostgreSQL 的 vacuum 漫談

關注我們,下載更多資源

新特性:PostgreSQL 的 vacuum 漫談

劉偉

劉偉,雲和恩墨軟件開發部研究院研究員;前微博DBA,主要研究方向為開源數據庫,分佈式數據庫,擅長自動化運維以及數據庫內核研究。

◆◆

前言

◆◆

即便是從數據庫特性,SQL功能性等方面,PostgreSQL都是一個更接近Oracle,在這方面遠勝於MySQL的數據庫,但是這個來源是學校的教學數據庫的開源數據庫,在很多地方,設計實現上的考慮,從我目前來看,是不完備的,其中的典型代表,就是vacuum機制.

如果是一直搞別的數據庫的人,無論是MySQL還是Oracle的DBA,看PostgreSQL總感覺會有個很顯眼的數據庫概念不見了–UNDO表空間.而用來解決UNDO表空間這個設計需要解決的問題的,在PG中對應的概念總稱,就是vacuum這個詞了.

正好最近看到pg的新版本特性討論中,提到在pg新版本中要引入undo這個概念,於是就有了對這一系列概念進行整理的想法.

◆◆

MVCC

◆◆

首先說一下,在最原始的數據庫理論中,是沒有MVCC這個概念的.如果現在的DBA學習數據庫理論,用一些比較老的教材,會發現,課本上理論的實現,與實際上操作的數據庫之間,是有很大差別的.

簡單描述來說(這裡就不展開ACID以及隔離級別的拓展討論了),在最早的數據庫理論里,行上的鎖有兩種,讀鎖與寫鎖,當要訪問一行數據的時候,如果是select,會獲取讀鎖,讀鎖會阻塞寫鎖,但不會阻塞讀鎖,當有update或者delete發生的時候,如果已經有select,那麼修改行為會等到前面的select執行完之後才執行,而反過來,如果有一行正在被update,那麼對這一行所有的select就都會被阻塞,直到這個修改完成提交.

這樣一來,很明顯有一個問題,就是讀會阻塞寫,寫也會阻塞讀,而且單行來看代價小,但如果視線擴展到整個數據庫,假設是一個比較繁忙的數據庫,這種對某一行的鎖,就會帶來很糟糕的問題了.

實際上這個的現實世界的實現,就是IBM DB2,以數據庫理論來說,DB2實現得更加理論化,但如果有用過DB2的DBA,應該對這個數據庫與主流數據庫(Oracle,MySQL)的實現不一致深為頭疼,而實際上,更頭疼的,是IBM DB2的銷售人員.

眾所周知,Oracle非常早就使用UNDO實現了MVCC,而MVCC最大的的特點,就是讀寫不再相互阻塞,讀不會阻塞寫,寫也不會阻塞讀,Oracle的銷售,可以拿着這個特性對DB2有技術上的優勢,在很多性能測試中,有好的表現,雖然DB2的失敗(無論從任何意義上來說,目前DB2的狀態都不能算作成功吧?)有很多原因,市場,社區等等都是因素,但其在技術上的確沒有很大的亮點功能,我認為也是佔據了很大因素的.

另外說個題外話,undo的主要作用,是當事務回滾的時候,可以直接取到修改前的數據塊,這是一個隨機讀的過程,但DB2的undo日誌記錄在redo里,事務回滾需要讀redo,這個以及db2的鎖機制(所有行鎖都是內存的數據結構,行鎖數量過多的話會升級為表鎖),是我早些年對DB2最大的槽點(如果拋開那個巨丑巨丑的數據庫圖形工具的話).

而PG在面對MySQL的領域,拿着vacuum來面對MySQL的undo實現的話,也難免在這方面有太多被動.

◆◆

MySQL中的MVCC

◆◆

mvcc,Multiversion Concurrency Control,多版本並發控制機制,本身上是一個指導性的概念,本身的指導思想是這樣的:與其鎖定數據行,不如讓寫入去寫這一行新的版本,而需要讀的時候,在新行提交之前(假設隔離級別是Read Commited),直接去讀老的行數據,既保證隔離性,也讓讀寫可以不要相互鎖定.

當然,對同一行的寫,永遠是排他性的,寫必然會阻塞寫.

mvcc的代表性實現,就是Oracle的undo機制,以及模仿其實現的MySQL InnoDB Undo,這倆的實現基本上類似(但Oracle是堆表(實際上也有索引組織表,但使用不多),InnoDB是索引組織表,細節上的實現還是有很多區別的),我對MySQL比較熟,就以此來簡單介紹下InnoDB UNDO的實現.

MySQL中,每個事務都會被分配到一個事務id,這個事務id是全局自增的數字,保證新事務的id必然大於老事務,然後這個id也會作為一個讀視圖id去用來讀取數據(如果是可重複讀的隔離級別的話,對於讀已提交隔離級別來說,類似的比喻來說,是最新提交的事務id作為讀視圖的).

每當發生數據寫入(delete或者update),InnoDB會做一個操作,就是把老的行做一個刪除標記,然後帶着當前的事務id插入新行(由於是索引組織表,保證必須在同一個數據塊中),這個操作本身,一是會把修改本身寫入redo,二是會讓這個數據塊被記錄到undo,而undo表空間的寫入,也會生成一個對應的redo,寫入到redo,也就是說,每次數據修改,會產生兩個redo記錄(對於insert來說,由於數據前鏡像是空,所以並沒有第二個undo對應的redo生成,也就是只產生一個redo記錄,需要注意),詳細說明可以參考 http://hedengcheng.com/?p=489

新特性:PostgreSQL 的 vacuum 漫談

圖片來自http://hedengcheng.com

當修改期間,有讀行為過來的時候,讀的游標,就會直接去讀undo中的老數據,而不會去求正在被修改的數據的鎖.

而為了實現隔離級別(可重複讀級別),事務id的作用在於,如果一個數據塊在事務開始後,才被修改並提交了,當游標讀取到這裡,會掃到當前數據塊裏面,所有在這期間被修改並提交的行,讀取到對應行id小於事務id的數據.

打個比方,一個事務開始之後,sleep了10秒,期間別的三個事務修改並提交了同一行記錄,當這個事務在之後讀取的時候,會沿着undo一路讀取到10秒前的記錄.

那老的數據會在什麼時候被徹底刪除呢?

MySQL中有個purge機制,這個(些)線程的工作就是,對於數據對應的事務id已經比當前數據庫最老的事務還小,並且被標記為刪除的數據,進行清理.在MySQL高版本(5.6及以上)中,這個工作是多線程並行執行的.

很明顯的問題是,就是undo表空間,曾經MySQL的undo表空間是和系統表空間在一起的,如果事務變更密度過大,並且有大事務之類的,會讓系統表空間放大非常多,解決辦法最直接的,就是搞個從庫,把數據全部導出,然後導入到新建的數據庫實例,以前坐我旁邊的哥們經常干這個事情,是一件漫長而枯燥的事情,所幸,這個問題在MySQL 5.6開始,通過undo表空間獨立解決了.

基本上來說,MySQL 5.6以上的版本來說,MySQL DBA不需要在mvcc這個機制上,花費太多精力去管理.

下面,來看看我們的主題,PG中,通過vacuum機制實現的MVCC.

◆◆

pg中的vacuum

◆◆

第一個需要說明的是,PG中,是沒有UNDO的.

基本實現上,和MySQL是一致的,也是每個update和delete,都會對老行搞一個刪除標記,作為”死亡”記錄,然後帶着當前的事務id寫入對應的行,這個過程中,對數據塊的修改會記錄為redo.

新特性:PostgreSQL 的 vacuum 漫談

圖片來着阿里內核月報

是不是看着很清爽?

曾經我也認為的確很清爽,透露着學院派的威嚴,until THE TIME COME.

在MySQL中,曾經提到過MySQL為了清理舊數據,引入了purge這個東西,而在PG中,對應的就是vacuum,主要作用是,回收已經不需要的記錄佔據的空間.這點上來說,並沒有什麼問題.

新特性:PostgreSQL 的 vacuum 漫談

圖片來自 Masahiko Sawada of NTT,

但是這個實現本身,遠遠比MySQL來得”痛”.

◆◆

目前的問題

◆◆

其根本原因,在於事務id的實現.在2018年底,除了很老的windows xp機器之外,我們應該都很少聽說哪裡還在用32位的操作系統了,但在PG中,由於種種歷史原因,其事務id,是32位的數字,而作為對比,MySQL是64位的數字.

讓我們做一個簡單的算術:

2**32/24/3600/1000=49

簡單概括下,如果是一個每秒鐘一千個事務的數據庫,不到50天就可以耗光事務id,對於比較繁忙的庫,比如平均每秒鐘1w事務來說,4天就可以耗光.事實上,PG的最新事務和最老事務的差不能超過2**31也就是20億,這個時間範圍還需要減半.

新特性:PostgreSQL 的 vacuum 漫談

圖片來自 Masahiko Sawada of NTT,

這種情況下,PG是怎麼解決這個問題的么?

首先,事務id得能續上,pg採用的方式是,如果到達限制,則從頭開始繼續算數字,參數autovacuum_freeze_max_age的默認值是200,000,000(2億),按照1wqps計算,十幾個小時就會耗光,當到達這個限制之後,事務id就會從3開始重新計數(9.4之前,現在已經變成比特位標記了).

這麼處理之後,就不能單純通過比較數據的事務id大小區分可見性了(重置id之後的事務id必然小於重置前),PG在這裡,引入了名為”凍結”的概念:當重置的時候,會對當前所有數據表的行進行一遍凍結標,設置其為可以對任意事務可見.這樣,重置事務id之後,如果新的事務訪問到這個表,就直接可以訪問到所有需要的數據了.

新特性:PostgreSQL 的 vacuum 漫談

圖片來自 Masahiko Sawada of NTT,

但是,這麼做,很明顯會有的問題是,凍結這個操作,必然會有非常大的IO消耗以及cpu消耗(所有表的所有行讀一遍,重置標記)無從避免.

實際上,這裡隱藏的一個很大的pg危機是,如果凍結操作很慢(比如系統資源不足),導致事務id耗凈,最終的結果就是,數據庫拒絕所有事務的執行,直到凍結操作結束.

我喜歡稱這個機製為” 凍結炸彈 “,業務約繁忙的庫,越容易觸發—-如果缺乏dba管理的話.這一點上的優化,到PG 9.6才終於走出第一步,就是對已經全部是”凍結”的行的數據塊,不再進行凍結處理.

而PG另外一個問題,就是垃圾回收這個本職了.到目前為止,pg官方版本在單表上只能串行地vacuum,對超大的單表處理時候,會有非常漫長的處理時間.並且,期間的IO消耗以及cpu消耗,會極大地影響到所在的服務器的性能.

相對來說,由於有超長時間事務,導致的表空間膨脹的問題,就沒有那麼致命了.pg官方也好,社區也好,都有通過觸發器或者redo日誌進行在線表重做的工具,很大程度上可以處理偶發大事務導致的單表過大問題.

除此之外,出於能者多勞的考慮,並且”反正都需要掃描一遍表”,包括表的統計數據分析,也由vacuum進程代勞了,這點看着很是彆扭.

◆◆

目前的解決方案

◆◆

在PG的各種技術討論中,vacuum永遠是主要話題之一, 圍繞如何對數據庫,表進行合適的vacuum策略,有非常多的討論與想法,我就已知的方法進行了總結 ,其中vacuum本身與vacuum凍結分開討論的.

vacuum自動策略

PG自身,對vacuum有一套默認的調度策略,主要參數表達如下:

autovacuum=on 默認打開自動垃圾回收

log_autovacuum_min_duration 默認-1,設置為0會記錄所有vacuum行為,大於0的話,記錄運行超過這個時間的vacuum,單位毫秒

autovacuum_max_workers vacuum同時運行的進程數量默認3

autovacuum_naptime vacuum每次運行的時間間隔,默認為1分鐘

autovacuum_vacuum_threshold autovacuum_vacuum_scale_factor 默認50,0.2,只有表內行數據update/delete超過autovacuum_vacuum_threshold autovacuum_vacuum_scale_factor*table row num之後,才會觸發vacuum

autovacuum_analyze_threshold autovacuum_analyze_scale_factor 默認50,0.1,數據修改量超過 autovacuum_analyze_threshold+autovacuum_analyze_scale_factor*table row num 默之後,才會觸發vacuum的表分析

autovacuum_freeze_max_age autovacuum_multixact_freeze_max_age觸發強制freeze的事務時間點 默認2億與4億 題外說一下,對於數據庫裏面的表,不會等到到達這個限制之後才去freeze,默認情況下,在autovacuum_freeze_max_age*0.95的事務數量時候,就會開始凍結操作,也可以通過vacuum_freeze_table_age(表級別粒度)參數控制

vacuum_freeze_min_age參數,如果表的這個參數設置了,每次vacuum時候,行事務id大於這個數字的時候,都會被設置freeze

autovacuum_vacuum_cost_limit 與autovacuum_vacuum_cost_delay 當vacuum操作的cost超過limit,則把vacuum延後指定的時間.cost來源是vacuum_cost_limit參數默認200,

人工策略

來自@德哥博客的建議,主要有三個:

1 是對錶進行分區,每個表不大於32GB,降低freeze的時間以及IO代價
2 是對不同的表,設置不同的freeze時間,alter table t set (autovacuum_freeze_max_age=xxxx),比如autovacuum_freeze_max_age為5億,表1設置為2.1億,表2設置為2.2億,以此類推.
3 人工在業務低峰調度.最直覺的辦法,就是在業務低峰搞vacuum.結合vacuum_freeze_min_age參數,讓表freeze更加靈活.

以下的人工策略討論,就是以人工調度為基礎,討論vacuum的監控以及治理方式,主要參考平安的PG治理策略,詳細情況請參考原始ppt.

1 監控長事務 pg_stat_activity 表的xact_start列就是當前活動事務的開始時間,比較就可以獲取到運行時間過長的事務

2 使用pg_squeeze工具執行空間回收的任務,pg_squeeze是基於pg邏輯複製實現的在線處理工具,實現原理實際上是創建新表,然後使用新表的文件替代原先表文件的方式,這種方式一來不訪問原先的表,二來不需要觸發器或者長時間的排他過程鎖,是非常好用的工具.

在具體的流程上,平安的自動化vacuum調度流程,可以說是非常完備:

其主要分為,策略制定,並行調度,調度報表三部分.

策略上,區分發版日(應用程序變更日,由於應用程序變更可能對數據庫變化比較敏感,單獨處理)與日常日,周末.

首先,需要滿足table age已經大於設定的min_age,並且”死亡”數據數量大於指定比例兩個條件,發版日的時候,只處理尺寸小於指定大小的數據表,而在日常,則處理尺寸大於指定大小的數據表,周末的時候,則不進行單獨的判斷.

在調度的時候,對於連續vacuum失敗進行報告.

在按照優先級,大小,最後一次vacuum排序之後,進入調度隊列,調度隊列中,檢查cpu,內存資源是否足夠(這裡個人存疑的問題是,為什麼不去檢查IO狀況),檢查cgroup的資源是否充足,資源條件滿足之後,才去調度vacuum,如果調度時候,發現已經超過指定的時間區間,則調度就不會繼續進行,而是退出過程了.

這一套策略,在保障數據庫運行穩定,數據庫變更對業務影響的前提下,做到了很好的平衡.

新特性:PostgreSQL 的 vacuum 漫談

配圖來着平安pg使用的ppt

◆◆

未來的解決方案

◆◆

人工策略終究是人工策略,無論如何,對於DBA能力沒有那麼強,自動化能力不足的團隊,在使用PG的時候,vacuum造成的困擾必然是個少不了的問題,而要從本質上解決這個問題,是需要官方來進行發行版的版本增強,而非依賴外部工具修修補補.

以下三個,是從pg官方的討論中,我認為會對這一系列問題有所優化,或者從根本上解決問題的方式,但就目前來看,還僅僅只是展望,離實際能用上,還是有較長的時間的,這個時候,就應該是PG的定製化發威的時候了,國內對PG的定製化,總是免不了糾結於Oracle兼容,以有窮應無窮,而對PG本質性的問題解決上,說實話,並沒有看到多少努力,期望以後在這些事情上,可以見到國內的力量.

undo by enterprisedb

https://www.postgresql.org/message-id/flat/CAEepm%3D2EqROYJ_xYz4v5kfr4b0qw_Lq_6Pe8RTEC8rx3upWsSQ%40mail.gmail.com

enterprisedb是目前PG服務公司裏面,名頭最大的,也是國內普遍使用的postgresql商業版的edb提供者,今年開始推進其存儲格式zheap加入到postgresql社區版本,其帶來的特性之一,就是對undo表空間的支持.

而其最正當的理由,就是:一個已經足夠大的表,如果其實際大小是”本來應該的大小”的兩倍,那vacuum的代價也是兩倍了.

我簡述一下其實現模式(實際上zheap是一整套存儲引擎,我這裡僅提取出來undo與vacuum相關的內容):

1. 預分配一組獨立的順序數字編號文件(每個1MB)作為undo文件,並且是從buffer pool過來的隨機訪問,使用上作為表空間訪問,而非獨立的緩衝區,其變更記錄也一樣會寫入redo.
2. undo記錄逐條記錄到undo中,當一個undo上最大的事務id已經小於當前數據庫最小事務id了,這個文件就可以被回收掉
3. undo的處理本身,由單獨的undo進程操作,其包括undo文件清理,以及事務的回滾處理.
4. 數據的修改為原地修改,老數據寫入undo,讀取的時候,沿着修改指針去讀,不需要重置事務id(vacuum凍結)
5. 老數據清理已經被undo處理了,因此vacuum整個機制就可以去掉了.
6. 包括臨時表,無日誌表在內都會支持.

但zheap重新組織了數據塊結構,這樣的話,必然會是一個全部替換升級的大方案,代碼合併也好,替換也罷,都不是短時間可以解決的事情,但目前這個事情上,看着還是有非常多人感興趣並且在討論的.

64bit tx id by postgrespro

https://www.postgresql.org/message-id/flat/DA1E65A4-7C5A-461D-B211-2AD5F9A6F2FD%40gmail.com

這個最早來源,是一個社區的討論郵件,而在pg的第三方發行版postgrespro中,這個功能早已實現,其作者就此從發行版中,提取出來整個補丁.

實際上如果不考慮vacuum凍結本身,vacuum本身,最多也就造成數據文件膨脹,而不會”在某個時間點數據庫不可訪問”,這個補丁就是基於這種考慮處理的,但事務id在數據庫中用處何其多,因此代碼補丁是個相當龐大的玩意,主要是修改事務相關的內存結構,數據塊的讀寫部分等一堆地方.

雖然最直接的想法,是把目前數據塊行格式中的xid直接從32位數字轉為64位數字,postgrespro就是這麼乾的,甚至給出了一個數據塊轉化的工具.郵件列表的討論中,也有提到採用一些變種,比如偏移量等,避免整個數據塊的重構,或者乾脆就是從32位取偏移量.

但這個補丁的最終的結果是,2017-06-05 被提出來,2017-06-22給出第一個補丁, 2018-03-01 經歷過最後的討論之後,就此擱置.

並行塊級別vacuum

https://commitfest.postgresql.org/13/954/

這個補丁比較簡單,概括來說,就是vacuum目前只能在單表上串行執行,但實際上vacuum的機制本身,並不是非得在單表上執行,無論是掃描表,還是對某個塊內”死”行的清理,都是可以並行化執行的.

作者最初的想法,是從B樹出發,分區並行掃描,後來在討論中,變成從表上的多個索引出發並行,單個索引上還是單進程(如果表上只有一個索引,那還是單進程vacuum)

最終的實現是:

首先並行掃描一遍表,取出來需要處理的行號,然後按照物理順序排序,多個進程在這個排好序的列表上在塊級別並行掃描,而對於索引,則是每個索引單獨一個進程處理其的vacuum.

性能等多方面都達到了預期,但是,最終由於測試不足,遇到了問題,最終還是沒有合併入官方分支.

◆◆

總結

◆◆

以上,就是我對pg的vacuum的目前狀況以及相關資料材料的整理,希望對有志於此的人有所幫助.

◆◆

參考

◆◆

http://hedengcheng.com/?p=148 InnoDB多版本(MVCC)實現簡要分析
 
https://github.com/digoal/blog/blob/master/201610/20161002_03.md PostgreSQL 9.6 vacuum freeze大幅性能提升 代碼淺析
 
https://github.com/digoal/blog/blob/master/201803/20180301_01.md PostgreSQL 並行vacuum patch - 暨為什麼需要並行vacuum或分區表
 
https://commitfest.postgresql.org/13/954/  Block level parallel vacuum
 
https://github.com/digoal/blog/blob/master/201605/20160520_01.md PostgreSQL 大表自動 freeze 優化思路
 
PostgresChina2018_石勇虎_庖丁解牛之平安vacuum優化之路
 
https://github.com/EnterpriseDB/zheap/tree/undo-log-storage/src/backend/access/undo
http://mysql.taobao.org/monthly/2017/10/01/ PgSQL · 特性分析 · MVCC機制淺析
 
 

資源下載

關注公眾號:數據和雲( OraNews )回復關鍵字獲取

2018DTCC  , 數據庫大會PPT

2018DTC ,2018 DTC 大會 PPT

DBALIFE  ,「DBA 的一天」海報

DBA04  ,DBA 手記4 電子書

122ARCH  ,Oracle 12.2體系結構圖

2018OOW  ,Oracle OpenWorld 資料

產品推薦

雲和恩墨zData一體機現已發佈超融合版本和精簡版,支持各種簡化場景部署,零數據丟失備份一體機ZDBM也已發佈,歡迎關注。

新特性:PostgreSQL 的 vacuum 漫談

新特性:PostgreSQL 的 vacuum 漫談

原文 :

相關閱讀

免责声明:本文内容来源于mp.weixin.qq.com,已注明原文出处和链接,文章观点不代表立场,如若侵犯到您的权益,或涉不实谣言,敬请向我们提出检举。