新特性:postgresql的vacuum漫談

資料和雲發表於2019-01-08

1546423068249372.jpg

劉偉

雲和恩墨軟體開發部研究院研究員;前微博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機制淺析

https://www.slideshare.net/pgdayasia/introduction-to-vacuum-freezing-and-xid 

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31556440/viewspace-2375109/,如需轉載,請註明出處,否則將追究法律責任。

相關文章