硬核-深度剖析PostgreSQL數據庫“凍結炸彈”原理機制

凍結(FREEZE),相信熟悉pg的人都對這個詞不陌生,因爲凍結過程對數據庫的資源消耗極大,影響業務的正常運行,所以也被稱爲“凍結炸彈”。網上關於凍結的文章也比較多,本文就系統性的介紹一下凍結過程的原理以及如何預防。

事務號回捲問題

先介紹下事務號回捲的問題,這也是爲什麼需要凍結的根本原因。我們知道,postgresql數據庫使用32位事務號,最大容納42億左右的事務號,事務號是循環使用的,當事務號耗盡後又會從3開始循環使用。事務環被分爲兩個半圓,當前事務號過去的21億事務屬於過去的事務號,當前事務號往前的21億屬於未來的事務號,未來的事務號對當前事務是不可見的。
在這裏插入圖片描述

如上圖所示,當前事務號走到了+100,由txid=100的事務號創建的元組(元組的xmin=100)對於當前事務屬於過去來說是可見的,當下一個事務+101開啓時,該元組就變爲未來的事務號了,該元組就變爲了不可見。爲了解決這個問題,pg引入了凍結事務id的概念,並使用freeze過程實現舊事務號的凍結。

Postgresql有三個特殊事務號:0代表無效事務號;1表示數據庫集羣初始化的事務id,也就是在執行initdb操作時的事務號;2代表凍結事務id。Txid=2的事務在參與事務id比較時總是比所有事務都舊,凍結的txid始終處理非活躍狀態,並且始終對其他事務可見。

如果發生當新老事務id差超過21億的時候,事務號會發生回捲,此時數據庫會報出如下錯誤並且拒絕接受所有連接,必須進入單用戶模式執行vacuum freeze操作。
ERROR: database is not accepting commands to avoid wraparound data loss in database “xxdb”
HINT: Stop the postmaster and vacuum that database in single-user mode

所以凍結過程應該在平時不斷地自動做而不是等到事務號需要回卷的時候纔去做。這時就需要引入一個參數:vacuum_freeze_min_age(默認5000萬),當凍結過程在掃描表頁面元組的時候發現元組xmin比當前事務號current_txid-vacuum_freeze_min_age更小時,就可以將該元組事務id置爲2,換個角度理解,也就是對於當前事務來說,如果存在某個元組的事務年齡超過vacuum_freeze_min_age參數值時,就可以在vacuum時把該元組事務號凍結。凍結會將元組結構體中的t_infomask字段置爲XMIN_FROZEN(我之前有一篇講HOT技術的文章講到了元組結構)。

可見性映射

可見性映射VM和vacuum有關,vacuum是一個比較消耗資源的操作,爲了提高vacuum的效率,讓vacuum只掃描存在死元組的頁面,而跳過全部都是活躍元組的頁面,設計了VM數據結構。在數據base目錄,每個表都存在一個對應的vm文件,vm由若干個8k頁面組成,類似一個數組結構,記錄了該表各個頁面上是否包含死亡元組信息。VM結構如下:

在這裏插入圖片描述

在9.6以後的版本中,針對凍結過程,vm的功能進行了增強,vm中除了記錄死亡元組信息,還記錄了頁面元組的凍結標識信息。如果頁面所有元組都已經被凍結,則置vm中的凍結標識爲1,freeze操作就會跳過該頁面,提升效率。

凍結過程

凍結有兩種模式,懶惰模式(lazy mode)和急切模式(eager mode)。他們之間的區別在於懶惰模式是跟隨者普通vacuum進程進行的,只會掃描包含死元組的頁面,而急切模式會掃描所有頁面(當然9.6之後已經優化),同時更新相關係統視圖frozenxid信息,並且清理無用的clog文件。

在凍結開始時,postgresql會計算freezelimit_txid的值,並凍結xmin小於freezelimit_txid的元組,freezelimit_txid的計算前面也提到過,freezelimit_txid=oldestxmin-vacuum_freeze_min_age,vacuum_freeze_min_age可以理解爲一個元組可以做freeze的最小間隔年齡,因爲事務回捲的問題,這個值最大設置爲20億,oldestxmin代表當前活躍的所有事務中的最小的事務標識,如果不存在其他事務,那oldestxmin就是當前執行vacuum的事務id。普通vacuum進程會挨個掃描頁面,同時配合vm可見性映射跳過不存在死元組的頁面,將xmin小於freezelimit_txid的元組t_infomask置爲XMIN_FROZEN,清理完成之後,相關統計視圖中n_live_tuple、n_dead_tuple、vacuum_count、autovacuum_count、last_autovacuum、last_vacuum之類的統計信息會被更新。

普通的vacuum只會掃描髒頁,而freeze操作會掃描所有可見且沒有被全部凍結的頁面,所以在每次vacuum時都去掃描是不合適的。這時就有了急切凍結模式,急切凍結引入一個參數vacuum_freeze_table_age,同理該參數的最大值也只能是20億,當表的年齡大於vacuum_freeze_table_age時,會執行急切凍結,表的年齡通過oldestxmin-pg_class.relfrozenxid計算得到,pg_class.relfrozenxid字段是在某個表被凍結後更新的,代表着某個表最近的凍結事務id。而pg_database.relfrozenxid代表着當前庫所有表的最小凍結標識,所以只有當該庫具有最小凍結標識的表被凍結時,pg_database.relfrozenxid字段纔會被更新。急切凍結的觸發條件是pg_database.relfrozenxid<oldestxmin-vacuum_freeze_table_age,這其實和上面的說法不衝突,因爲某個數據庫所有表中的最老的relfrozenxid就是數據庫的relfrozenxid,所以凍結可以用一句話來理解:當數據庫中存在某個表的年齡大於vacuum_freeze_table_age參數設定值,就會執行急切凍結過程,當表中元組年齡超過vacuum_freeze_min_age,就可以被凍結,這裏其實是必須和可以的區別。

最佳實踐

Freeze是運維好pg數據庫必須要十分關注的點。關於freeze有如下三個參數:vacuum_freeze_min_age、vacuum_freeze_table_age、autovacuum_freeze_max_age。前兩個參數其實前面介紹的差不多了,感覺這兩個參數已經足夠了,那麼爲什麼需要第三個參數呢?

下面我們這樣假設:vacuum_freeze_min_age=2億,vacuum_freeze_table_age=19億,那麼只有當表中元組年齡達到2億時纔可以執行freeze操作,這其中部分元組id被置爲凍結,部分沒有被凍結,同時更新表的relfrozenxid爲2億,然後假設我們從2億開始表的年齡又過了19億,這時候表的年齡達到了,這時候會強制執行急切凍結,但是此時新老事務號差距已經達到了21億,超過了20億的限制,從另一個角度理解,vacuum_freeze_min_age是相當於在年齡線上增加了一段長度,而且必須有這段長度才能執行freeze操作,這樣就不能保證vacuum_freeze_table_age+vacuum_freeze_min_age<20億,此時就需要單獨弄一個參數來保證新老事務差不超過20億,這個參數就是autovacuum_freeze_max_age。這個參數會強制限制元組的年齡(oldestxmin-xmin)如果超過該值就必須進行急切凍結操作,這個限制是個硬限制。

針對生產環境中,有如下建議:

①autovacuum_freeze_max_age的值應該大於vacuum_freeze_table_age的值,因爲如果反過來設置,那麼每次當表年齡vacuum_freeze_table_age達到時,autovacuum_freeze_max_age也達到了,那麼剛剛做的freeze操作又會去掃描一遍,造成浪費。但是vacuum_freeze_table_age的值也不能太小,太小的話會造成頻繁的急切凍結。

②執行急切凍結時,vacuum_freeze_table_age真正的值會去取vacuum_freeze_table_age和0.95autovacuum_freeze_max_age中的較小值,所以官方建議將vacuum_freeze_table_age設置爲0.95autovacuum_freeze_max_age。

③autovacuum_freeze_max_age和vacuum_freeze_table_age的值也不適合設置過大,因爲過大會造成pg_clog中的日誌文件堆積,來不及清理。

④vacuum_freeze_min_age不易設置過小,比如我們freeze某個元組後,這個元組馬上又被更新,那麼之前的freeze操作其實是無用功,freeze真正應該針對的是那些長時間不被更新的元組。

⑤生產環境中做好pg_database.frozenxid的監控,當快達到觸發值時,我們應該選擇一個業務低峯期窗口主動執行vacuum freeze操作,而不是等待數據庫被動觸發。

歡迎關注我的公衆號:數據庫架構之美

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章