跳轉到內容

PostgreSQL/迴圈和凍結

來自 Wikibooks,開放世界的開放書籍


兩個基本問題

[編輯 | 編輯原始碼]

事務由識別符號識別,這些識別符號被實現為無符號的 32 位整數,稱為 XID。事務及其 XID 在叢集級別是已知的,涵蓋所有資料庫。類似於序列,XID 會隨著每個新事務而增加 。遲早,這個有限的 數字空間將被耗盡,因此有必要從頭開始重新啟動序列(值 0、1 和 2 被保留用於特定目的)。這個 XID 的重啟被稱為 *迴圈*,每個迴圈被稱為一個*紀元*。

不太可能在叢集中同時存在超過 個事務,或者單個事務持續時間很長,以至於其 XID 與下一個迴圈的相同值發生衝突。乍一看,這種對“ 宇宙”的迴圈使用似乎是安全的,易於實現的。然而,這種簡單的策略會導致巨大的問題。原因是 XID 儲存在每行內的系統列中(請參見 MVCC 章節中的 xmin, xmax)。並且行會長期停留在資料庫中,在許多情況下會永遠存在。

XID 衝突

[編輯 | 編輯原始碼]

第一個問題是,迴圈後,下一個 XID(3、4、5,…)可能會與上一個紀元的 XID 發生衝突。它們不再是唯一的,因為舊行的系統列可能包含相同的值。但事務必須能夠確定檢索到的行是在其自身開始時間後還是很久以前被其他事務修改的。我們將第一個問題稱為 **XID 衝突**。

突然死亡

[編輯 | 編輯原始碼]

第二個問題與 MVCC 和事務時間線相關。行可能存在於多個版本中。當事務修改行並保持更長時間存活時——由於更多活動而沒有 COMMIT 或 ROLLBACK——其他程序應該“看到”該行的版本為事務開始時間時的版本,而不是未提交的修改。因此,機制必須隱藏正在進行的更改,讓其他事務感覺資料環境是穩定的。系統透過在每個 SQL 命令(尤其是,但不僅僅是)系統列 xmin 中考慮其他標準來實現這一點。

您可以想象,這些額外的標準可以讓系統在每個查詢中無聲地新增謂詞 xmin < my_xid。(這僅僅是虛擬碼中的說明,實際實現方式不同。)它保證了在請求事務開始後發生的更改對該事務是不可見的。

到目前為止,一切都很好。

但迴圈後會發生什麼?下一個事務將擁有非常小的 XID,例如“5”。而 xmin < 5 的結果將是什麼?幾乎什麼都沒有。所有 xmin 介於 之間的行將不再是任何查詢的一部分。與片刻之前的情況相反,所有資料突然消失了。它仍然存在於資料庫中,但無法訪問。我們將第二個問題稱為 **突然死亡**。

解決方案

[編輯 | 編輯原始碼]

步驟 1:在概念層面上,完整的 ' 宇宙' 被劃分為兩個 個數字的半部分。一個分割點是當前事務 ID pg_current_xact_id(在 PostgreSQL 版本 13 之前稱為 txid_current),另一個是圓圈的另一端 pg_current_xact_id + (或 pg_current_xact_id - ,它們是相同的)。因此,分割點不是固定值,而是動態地跟隨新事務的進行而變化。一半代表以前使用過並因此用盡的 XID;另一半代表定義上是自由的 XID。它們將在未來分配。請注意動態方面:隨著叢集中每個新事務的出現,pg_current_xact_id 和 '過去 / 未來' 之間的邊界都向前移動。這是一種穿越時間的無休止的行走比喻,在一段時間後,舊問題將被遺忘,或者至少被理想化。

該想法可以透過修改上述 xmin < my_xid 謂詞為 if/else 塊來實現。

if (my_xid < )
  return rows with: xmin < my_xid OR  xmin > my_xid + 
else
  return rows with: xmin < my_xid AND xmin > my_xid + 

當然,這是一種簡化,必須考慮許多其他標準,例如 COMMIT 狀態、'已刪除' 以及其他因素。它純粹關注 '過去 / 未來' 比喻的方面。

注意:使用此演算法,'關鍵點' 從 或者 變為 pg_current_xact_id + 。它被稱為迴繞點,而 pg_current_xact_id + pg_current_xact_id 之間的線稱為迴繞視界

步驟 2:概述的演算法確保了所有可能 XID 的 50% 可見。但是其他 XID 怎麼樣呢?如上所述,行可能會永遠保留在資料庫中,在 xmin 中儲存非常舊的 XID。這部分也必須考慮。訪問所有可能的 XID 範圍的想法是,用一個標誌來補充以前的演算法,該標誌將某些行標記為 '永遠可見'(或者直到下一次寫入操作才可見)。只要有一個或多個事務可能獲得對它們的寫訪問許可權,這種標記就不可能實現。幸運的是,新 XID 的序列嚴格向前移動,並且隨著時間的推移,具有舊 XID 的事務會結束。PostgreSQL 不僅知道當前 XID pg_current_xact_id,而且還知道每個連線的最舊活動 XID(pg_stat_activity.backend_xmin),以及每個表(pg_class.relfrozenxid)和每個資料庫(pg_database.datfrozenxid)的所有未凍結 XID 的下限。xminoldest(pg_stat_activity.backend_xmin) 舊的行是此類標誌的候選。沒有執行的事務擁有或將獲得對它們的寫訪問許可權,只有更新的事務可以。根據MVCC,它們將建立一個新的行版本,這個版本將保留原樣。

它是 VACUUM 執行的兩項主要職責之一,即執行凍結。它在這些已識別行的頭部的 t_infomask 中用一個標誌標記它們為 '永遠可見'。從這一點起,不再進行與 xmin 的比較。這些行始終被視為可見,即使它們是 '未來' 的一部分。這種標記稱為 FREEZE,行狀態稱為 FROZEN。

現在,用於檢索行的演算法更改為

-- 'frozen' rows will always be returned
if (my_xid < )
  return rows with: frozen OR 
                    (xmin < my_xid OR  xmin > my_xid + )
else
  return rows with: frozen OR
                    (xmin < my_xid AND xmin > my_xid + )

透過此擴充套件,解決了上述兩個問題。系統即使在迴繞後也能生成 XID,而不會有與舊 XID 衝突的風險。舊的 XID 可能會存在,但它們不會以任何方式被觸碰。其次,該演算法可以找到所有相關的 XID,無論是否發生了迴繞。

迴繞失敗

[edit | edit source]

一個事務可能有意或由於應用程式中的錯誤而長時間保持活動狀態。隨著時間的推移,它的 XID 成為整個叢集中最舊的 XID,可以從 pg_stat_activity.backend_xmin 中檢索到。只要這種情況持續存在,迴繞點 pg_current_xact_id + pg_stat_activity.backend_xmin 之間的差距越來越小。如果差距完全消失,我們將看到本章開頭描述的所有問題。這稱為迴繞失敗,必須在任何情況下都避免這種情況。VACUUM 盡其所能凍結儘可能多的行。但是,如果長時間執行的事務阻止凍結,並且差距的大小降至某個限制以下,VACUUM 將以 '激進模式' 執行,並作用於受影響表的全部頁面,而與上述值無關;如果這也失敗,叢集將停止建立新事務並阻止進一步的寫操作。

詳細資訊

[edit | edit source]

注意:初學者可以跳過這些細節,不會影響對正在進行的章節的理解。

為了凍結任何行版本,VACUUM 必須檢查幾個條件

  • xmax 必須為零,因為只有未刪除的行才能永遠可見。
  • xmin 必須比所有當前存在的事務 oldest(pg_stat_activity.backend_xmin) 舊。這保證了沒有現有事務可以修改或刪除該版本。
  • xminxmax 的事務必須已提交。

凍結操作在什麼時候發生?請注意,存在一些名為 xxx_age 的配置引數。它們定義了距離 - 主要到 pg_current_xact_id -,VACUUM 的操作將從這些距離開始。在這種情況下,'age' 並不意味著特定時間段,它始終是一個純數字,表示事務數,例如 5000 萬。另外請注意,VACUUM 始終讀取完整的物理頁面,並在其中找到的行版本上進行操作。

  • 當客戶端發出帶有 FREEZE 選項的 SQL 命令 VACUUM 時。在這種情況下,將處理受影響表的所有頁面,這些頁面在可見性地圖 中被標記為可能包含未凍結行。
  • 當客戶端發出沒有任何選項的 SQL 命令 VACUUM,並且存在比 vacuum_freeze_table_age(預設值:15000 萬)減去 vacuum_freeze_min_age(預設值:5000 萬)舊的 XID 時。與之前一樣,將處理在可見性地圖中被標記為可能包含未凍結行的所有頁面。
  • 當 Autovacuum 程序執行時。此類程序以兩種模式之一執行:在正常模式下,它會跳過具有比 vacuum_freeze_min_age(預設值:5000 萬)年輕的行版本的頁面,只作用於所有 XID 都較舊的頁面。跳過較年輕的 XID 可以防止對這些頁面進行處理,因為這些頁面很可能被將來的 SQL 命令之一更改。如果程序發現正在處理的表的最新 XID 超過 vacuum_freeze_table_age(預設值:15000 萬),它將切換到激進模式。在激進模式下,Autovacuum 處理受影響表的全部頁面。

VACUUM 和 Autovacuum 知道每個表的最舊未凍結 XID 向前移動到了哪個值,並將該值記錄在pg_class.relfrozenxid 中。該值與pg_current_xact_id 分割點的距離會變小(可能存在未凍結的行),而到環繞點 pg_current_xact_id + 的距離會變大(只有凍結的行)。這就是凍結如何跟隨移動的“過去”/“未來”邊界。

注意:在 PostgreSQL 9.4 版本之前,凍結演算法將值“2”(FrozenTransactionId)儲存在xmin 中,而不是在t_infomask 中設定標誌。


華夏公益教科書