事務ID(XID)基本概念
從Transactions and Identifiers可知:
事務 ID,例如 278394,會根據 PostgreSQL 集群內所有數據庫使用的全局計數器按順序分配給事務。此分配會在事務首次寫入數據庫時進行。這意味著編號較低的 xid 會先于編號較高的 xid 寫入。
?
事務 ID 類型 xid 為 32 位寬,每 40 億次事務繞回一次。每次繞回時,都會增加一個 32 位的紀元 (epoch)。此外,還有一個 64 位類型 xid8,它包含這個紀元,因此在安裝的生命周期內不會繞回;它可以通過強制類型轉換轉換為 xid。xid 是 PostgreSQL MVCC 并發機制和流復制的基礎。
交易ID和快照信息函數參見這里。
日常清理(VACUUM)中的凍結操作
PostgreSQL 數據庫需要定期維護,稱為清理(vacuum)。其中除更新統計信息,回收空間外,一項重要的任務就是防止由于事務 ID 回繞或多事務 ID 回繞而丟失非常舊的數據。
從Preventing Transaction ID Wraparound Failures可知:
PostgreSQL 的 MVCC 事務語義依賴于能夠比較事務 ID (XID) 編號:如果行版本的插入 XID 大于當前事務的 XID,則該行版本“位于未來”,對當前事務不可見。但由于事務 ID 的大小有限(32 位),因此長期運行的集群(超過 40 億個事務)將遭遇事務 ID 回繞:XID 計數器會回繞為零,過去的事務會突然變成未來的事務 — — 這意味著它們的輸出變得不可見。簡而言之,就是災難性的數據丟失。(實際上數據仍然存在,但如果您無法獲取數據,這也只是些安慰。)為了避免這種情況,有必要至少每 20 億個事務清理一次每個數據庫中的每個表。
為何是至少每 20 億個事務
清理一次,這是由autovacuum_freeze_max_age參數控制的:
sampledb=> show autovacuum_freeze_max_age;autovacuum_freeze_max_age
---------------------------200000000
(1 row)
20 億實際是XID取值范圍的一半,即231。
定期清理能夠解決這個問題的原因是,VACUUM 會將行標記為凍結,表明這些行是由一個提交時間足夠久的事務插入的,因此插入事務的影響對所有當前和未來的事務都可見。普通 XID 使用模 232 算法進行比較。這意味著對于每個普通 XID,都有 20 億個“更舊”的 XID 和 20 億個“更新”的 XID;換句話說,普通 XID 空間是循環的,沒有端點。因此,一旦使用特定的普通 XID 創建了行版本,那么在接下來的 20 億個事務中,無論我們討論的是哪個普通 XID,該行版本都會看起來像是“過去”的。如果在超過 20 億個事務之后,該行版本仍然存在,它就會突然看起來像是未來。為了防止這種情況,PostgreSQL 保留了一個特殊的 XID,FrozenTransactionId,它不遵循普通 XID 比較規則,并且始終被認為比所有普通 XID 都舊。凍結行版本被視為插入 XID 是 FrozenTransactionId,因此無論環繞問題如何,它們對于所有正常事務都將顯示為“過去”,因此此類行版本將一直有效,直到被刪除,無論時間有多長。
所謂是循環的,沒有端點
類似于下圖:
0 → 1 → 2 → ... → 2^32-1 → 0 → 1 → ...
每一個表都有系統定義的隱含列,xmin和xmax:
sampledb=> select xmin, xmax from regions limit 1;xmin | xmax
------+------5190 | 0
(1 row)
所謂凍結就是將xmin的值設為FrozenTransactionId(實際值為2),設置后xmin的值不會再被修改。
vacuum_freeze_min_age 控制 XID 值的有效期,超過該 XID 值的行才會被凍結。如果原本會被凍結的行很快會被再次修改,則增加此設置可以避免不必要的工作;但降低此設置會增加在必須再次清理表之前可以處理的事務數。
表未清理的最長時間是20億個事務數減去上次激進清理時的vacuum_freeze_min_age值。如果未清理的時間超過該時間,可能會導致數據丟失。為確保不會發生這種情況,任何可能包含XID大于配置參數autovacuum_freeze_max_age指定的未凍結行的表都會被調用自動清理。(即使禁用自動清理,也會發生這種情況。)
事務ID環繞問題是如何產生和解決的
前面已經談到了普通 XID 使用模 232 算法進行比較。這個規則就是:
如果(NextXID - xmin) % 2^32 < 2^31,則 xmin 屬于過去
PostgreSQL 將 “當前 XID ± 2^31 (≈ 20 億)” 作為 可見窗口,因此總有約20億屬于過去,20億屬于未來(中間那個|
即NextXID):
<---------------- 2^31 = 2,147,483,648 ---------------->“過去” “未來”
-----------------------|------------------------------>可見 不可見
先看一個屬于過去的例子。
假設xmin = 4,294,967,000,接近2^32。XID已經回繞,此時NextXID = 100。
根據算法(NextXID - xmin) % 2^32 < 2^31
。
NextXID = 100
xmin = 4,294,967,000
delta = (100 - 4,294,967,000) % 4,294,967,296= ( -4,294,966,900 ) % 4,294,967,296= 396
顯然,396小于2^31,因此xmin雖然接近XID的最大值,但屬于過去。
再看一個屬于未來的例子,xmin和上例相同:
NextXID = 2,147,483,700
xmin = 4,294,967,000
delta = (2,147,483,700 - 4,294,967,000) % 4,294,967,296= (-2,147,483,300) % 4,294,967,296= 2,147,483,996
此時,delta大于2^31,因此xmin屬于未來。
xmin并沒有發生變化,而此時卻被視為未來,這顯然是錯誤的。通過凍結,即將xmin置為FrozenTransactionId,即可解決事務ID環繞問題。
在源碼文件./backend/access/transam/transam.c中,可以找到此算法:
// git clone https://github.com/postgres/postgres.git 檢出源碼
/** TransactionIdPrecedes --- is id1 logically < id2?*/
bool
TransactionIdPrecedes(TransactionId id1, TransactionId id2)
{/** If either ID is a permanent XID then we can just do unsigned* comparison. If both are normal, do a modulo-2^32 comparison.*/int32 diff;if (!TransactionIdIsNormal(id1) || !TransactionIdIsNormal(id2))return (id1 < id2);diff = (int32) (id1 - id2);return (diff < 0);
}
監控XID環繞
postgres=# SELECT datname,age(datfrozenxid) AS xid_age,2000000000 - age(datfrozenxid) AS remaining_before_wraparound
FROM pg_database;datname | xid_age | remaining_before_wraparound
--------------------+---------+-----------------------------postgres | 5375 | 1999994625template1 | 5375 | 1999994625template0 | 5375 | 1999994625world_temperatures | 5375 | 1999994625demo | 5375 | 1999994625sampledb | 5375 | 1999994625
(6 rows)
可以參照Transaction ID wraparound: a walk on the wild side,模擬事務ID環繞問題。