克服交易 ID (TXID) 環繞 (wraparound) 防護錯誤

本頁說明資料庫在 PostgreSQL 中遇到交易 ID 包圍保護機制時,可以採取的做法。這會顯示為 ERROR 訊息,如下所示:

database is not accepting commands to avoid wraparound data loss in database
dbname.

Stop the postmaster and vacuum that database in single-user mode.

You might also need to commit or roll back old prepared transactions, or drop
stale replication slots.

或者,系統可能會顯示下列 WARNING 訊息:

database dbname must be vacuumed within 10985967 transactions.

To avoid a database shutdown, execute a database-wide VACUUM in that database.

基本步驟說明

  • 找出造成重疊的資料庫和資料表。
  • 檢查是否有任何因素阻礙 (AUTO)VACUUM 執行 (例如卡住的交易 ID)。
  • 測量 AUTOVACUUM 的速度。如果速度很慢,您可以視需要嘗試加快速度。
  • 如有需要,請手動執行更多 VACUUM 指令。
  • 請嘗試其他方法加快掃地機器人的速度。有時候,最快的方法是刪除資料表或部分索引。

許多旗標值的建議值並非精確值,因為它們取決於許多資料庫參數。如要進一步瞭解這個主題,請參閱本頁結尾處的相關連結文件。

找出導致回繞的資料庫和資料表

尋找資料庫

如要找出哪些資料庫包含導致迴繞的資料表,請執行下列查詢:

SELECT datname, 
       age(datfrozenxid), 
       2^31-1000000-age(datfrozenxid) as remaining
  FROM pg_database
 ORDER BY 3

remaining 值接近 0 的資料庫會導致問題。

尋找表格

連線至該資料庫並執行下列查詢:

SELECT c.relnamespace::regnamespace as schema_name,
       c.relname as table_name,
       greatest(age(c.relfrozenxid),age(t.relfrozenxid)) as age,
       2^31-1000000-greatest(age(c.relfrozenxid),age(t.relfrozenxid)) as remaining
  FROM pg_class c
  LEFT JOIN pg_class t ON c.reltoastrelid = t.oid
 WHERE c.relkind IN ('r', 'm')
 ORDER BY 4;

這項查詢會傳回導致問題的資料表。

臨時資料表

如果 schema_namepg_temp_ 開頭,則解決問題的唯一方法是刪除資料表,因為 PostgreSQL 不允許您在其他工作階段中建立的臨時資料表執行 VACUUM。有時如果該工作階段已開啟且可存取,您可以在此處清除資料表,但這通常不會發生。使用下列 SQL 陳述式捨棄臨時表:

SET cloudsql.enable_maintenance_mode = 'on'; /* get extra transaction ids */
DROP TABLE pg_temp_<N>.<tablename>;

如果這是唯一的阻斷項目,Autovacuum 會在約一分鐘內擷取這項變更,並在 pg_database 中將 datfrozenxid 移至前方。這可解決包覆保護的唯讀狀態。

一般表格

針對一般 (非暫時性) 資料表,請繼續執行下列步驟,瞭解是否有任何項目阻止清理作業、是否有足夠的時間執行 VACUUM,以及是否正在清理最重要的資料表。

檢查卡住的交易 ID

系統可能會耗盡交易 ID,其中一個原因是 PostgreSQL 無法凍結 (也就是標示為可供所有交易檢視) 在目前執行最久的交易開始後建立的任何交易 ID。這是因為多版本並行控制 (MVCC) 規則。在極端情況下,這類交易可能會變得過於陳舊,導致 VACUUM 無法清理任何舊交易,以符合 20 億個交易 ID 的迴轉限制,進而導致整個系統停止接受新的 DML。您通常也會在記錄檔案中看到 WARNING: oldest xmin is far in the past 的警告。

只有在解決卡住的交易 ID 後,您才應繼續進行最佳化。

以下是導致交易 ID 卡住的四個可能原因,以及如何因應:

  • 長時間執行的交易:找出這些交易,並取消或終止後端,以便解除真空狀態。
  • 孤立的準備交易:復原這些交易。
  • 已放棄的複製運算單元:放棄已放棄的運算單元。
  • 在複本上執行長時間交易,且有 hot_standby_feedback = on請找出這些交易,並取消或終止後端,以便解除真空狀態。

在這些情況下,下列查詢會傳回最久以前的交易年齡,以及在循環結束前剩餘的交易數量:

 WITH q AS (
SELECT
  (SELECT max(age(backend_xmin))
      FROM pg_stat_activity  WHERE state != 'idle' )       AS oldest_running_xact_age,
  (SELECT max(age(transaction)) FROM pg_prepared_xacts)    AS oldest_prepared_xact_age,
  (SELECT max(greatest(age(catalog_xmin),age(xmin))) FROM pg_replication_slots)        AS oldest_replication_slot_age,
  (SELECT max(age(backend_xmin)) FROM pg_stat_replication) AS oldest_replica_xact_age
)
SELECT *,
       2^31 - oldest_running_xact_age AS oldest_running_xact_left,
       2^31 - oldest_prepared_xact_age AS oldest_prepared_xact_left,
       2^31 - oldest_replication_slot_age AS oldest_replication_slot_left,
       2^31 - oldest_replica_xact_age AS oldest_replica_xact_left
FROM q;

這個查詢可能會傳回任何 *_left 值,這些值的回報值與 100 萬的差距小於或等於 1 百萬。這個值是 PostgreSQL 停止接受新寫入指令時的迴轉保護限制。在這種情況下,請參閱「移除 VACUUM 封鎖器」或「調整 VACUUM」。

舉例來說,上述查詢可能會傳回:

┌─[ RECORD 1 ]─────────────────┬────────────┐
│ oldest_running_xact_age       2146483655 │
│ oldest_prepared_xact_age      2146483655 │
│ oldest_replication_slot_age   ¤          │
│ oldest_replica_xact_age       ¤          │
│ oldest_running_xact_left      999993     │
│ oldest_prepared_xact_left     999993     │
│ oldest_replication_slot_left  ¤          │
│ oldest_replica_xact_left      ¤          │
└──────────────────────────────┴────────────┘

其中 oldest_running_xact_leftoldest_prepared_xact_left 都位於 1 百萬個迴轉保護上限內。在這種情況下,您必須先移除阻礙因素,才能繼續使用 VACUUM。

移除 VACUUM 封鎖條件

長時間執行的交易

在上述查詢中,如果 oldest_running_xact 等於 oldest_prepared_xact,請前往「 孤立的準備交易」一節,因為「最新執行中」值也包含已準備的交易。

您可能需要先以 postgres 使用者身分執行下列指令:

GRANT pg_signal_backend TO postgres;

如果違規交易屬於任何系統使用者 (以 cloudsql... 開頭),您就無法直接取消交易。您必須重新啟動資料庫才能取消。

如要找出長時間執行的查詢,並取消或終止查詢以解除真空狀態,請先選取幾個最久未執行的查詢。LIMIT 10 行可讓結果適合螢幕大小。您可能需要在解決最早執行的查詢後重複這項操作。

SELECT pid,
       age(backend_xid) AS age_in_xids,
       now() - xact_start AS xact_age,
       now() - query_start AS query_age,
       state,
       query
 FROM pg_stat_activity
 WHERE state != 'idle'
 ORDER BY 2 DESC
 LIMIT 10;

如果 age_in_xids 傳回的結果為 NULL,表示交易尚未分配永久交易 ID,因此可以安全地忽略。

取消 xids_left_to_wraparound 接近 100 萬的查詢。

如果 stateactive,則可以使用 SELECT pg_cancel_backend(pid); 取消查詢。否則,您必須使用 SELECT pg_terminate_backend(pid); 終止整個連線,其中 pid 是上一個查詢的 pid

孤立的準備交易

列出所有已準備好的交易:

DB_NAME=> SELECT age(transaction),* FROM pg_prepared_xacts ;
┌─[ RECORD 1 ]┬───────────────────────────────┐
 age          2146483656                    
 transaction  2455493932                    
 gid          trx_id_pin                    
 prepared     2021-03-03 16:54:07.923158+00 
 owner        postgres                      
 database     DB_NAME                       
└─────────────┴───────────────────────────────┘

使用上次查詢的 gid (在本例中為 trx_id_pin) 做為交易 ID,回溯最早的孤立已準備交易:

ROLLBACK PREPARED trx_id_pin;

或者,您也可以提交:

COMMIT PREPARED trx_id_pin;

如需完整說明,請參閱 SQL ROLLBACK PREPARED 說明文件。

放棄的複製時段

如果現有的複本已停止、暫停或發生其他問題,複製運算單元就會遭到放棄,您可以從 gcloud 或 Google Cloud 主控台刪除複本。

首先,請確認備用資源並未遭到停用,如管理唯讀備用資源一節所述。如果副本已停用,請再次啟用。如果延遲時間仍偏高,請刪除備用資源。

複製時段會顯示在 pg_replication_slots 系統檢視畫面中。

以下查詢會擷取相關資訊:

SELECT *, age(xmin) AS age FROM pg_replication_slots;
┌─[ RECORD 1 ]────────┬─────────────────────────────────────────────────┐
 slot_name            cloudsql_1_355b114b_9ff4_4bc3_ac88_6a80199bd738 
 plugin               ¤                                               
 slot_type            physical                                        
 datoid               ¤                                               
 database             ¤                                               
 active               t                                               
 active_pid           1126                                            
 xmin                 2453745071                                      
 catalog_xmin         ¤                                               
 restart_lsn          C0/BEF7C2D0                                     
 confirmed_flush_lsn  ¤                                               
 age                  59                                              
└─────────────────────┴─────────────────────────────────────────────────┘

在這個範例中,pg_replication_slots 值為健康 (age == 59)。如果年齡接近 20 億,您應該刪除該時段。如果查詢傳回多個記錄,就沒有簡單的方法可以得知哪個副本是哪個。因此,請檢查所有這些項目,以防任何副本上有長時間執行的交易。

備援機制上的長時間交易

檢查副本,找出 hot_standby_feedback 設為 on 的最舊執行中的交易,然後在副本上停用該交易。

pg_stat_replication 檢視畫面中的 backend_xmin 欄會顯示備份資料所需的最舊 TXID

如要將其推進,請停止在複本上保留的查詢。如要找出造成延遲的查詢,請使用執行時間過長的交易中的查詢,但這次是在複本上執行。

另一個選項是重新啟動複本。

設定 VACUUM

設定下列兩個標記:

  • autovacuum_vacuum_cost_delay = 0
  • autovacuum_work_mem = 1048576

第一個選項會停用 PostgreSQL 的磁碟節流功能,讓 VACUUM 以全速執行。根據預設,autovacuum 會受到節流限制,因此不會在速度最慢的伺服器上使用所有磁碟 I/O。

第二個標記 autovacuum_work_mem 會減少索引清理通過次數。盡可能將其設為足夠大,以便在 VACUUM 要清除的資料表中儲存所有已淘汰資料列的 ID。設定這個值時,請考量這是每個執行中的 VACUUM 可配置的本地記憶體上限。請確認您允許的數量不超過可用數量,並保留一些備用數量。如果讓資料庫以唯讀模式執行,請同時考量用於唯讀查詢的本機記憶體。

在大多數系統上,請使用最大值 (1 GB 或 1048576 KB,如範例所示)。這個值最多可容納約 1 億 7800 萬個無效元組。但仍會導致多次索引掃描。

如要進一步瞭解這些旗標和其他旗標,請參閱「最佳化、監控 PostgreSQL 的 VACUUM 作業並進行疑難排解」。

設定這些旗標後,請重新啟動資料庫,讓 autovacuum 以新值啟動。

您可以使用 pg_stat_progress_vacuum 檢視畫面監控自動啟動 VACUUM 的進度。這個檢視畫面會顯示所有資料庫中執行的 VACUUM,以及其他資料庫中的資料表 (關聯),您無法使用檢視資料欄 relid 查詢資料表名稱。

如要找出下次需要進行真空處理的資料庫和資料表,請使用最佳化、監控 PostgreSQL 的 VACUUM 作業並進行疑難排解中的查詢。如果伺服器 VM 效能足夠,且有足夠頻寬可啟動比自動真空程序更多的並行真空程序,您可以啟動一些手動真空程序。

檢查真空吸塵器的速度

本節說明如何檢查 VACUUM 速度,以及如何視需要加速。

檢查正在執行的自動清理功能

所有執行 VACUUM 的後端都會顯示在系統檢視畫面中的 pg_stat_progress_vacuum

如果目前的階段為 scanning heap,您可以監控進度,觀察 heap_blks_scanned 欄中的變更。很抱歉,目前沒有簡單的方法可在其他階段判斷掃描速度。

預估 VACUUM 掃描速度

如要預估掃描速度,您必須先儲存基礎值,然後計算時間變化,以便預估完成時間。首先,您必須使用下列快照查詢,將 heap_blks_scanned 的快照與時間戳記一併儲存:

SELECT set_config('save.ts', clock_timestamp()::text, false),
       set_config('save.heap_blks_scanned', heap_blks_scanned::text, false)
FROM pg_stat_progress_vacuum
WHERE datname = 'DB_NAME';

由於我們無法在已循環的資料表中儲存任何內容,請使用 set_config(flag, value) 將兩個使用者定義的旗標 (save.tssave.heap_blks_scanned) 設為 pg_stat_progress_vacuum 的目前值。

在下一個查詢中,我們會使用這兩個值做為比較基準,以判斷速度並估算完成時間。

注意:WHERE datname = DB_NAME 會將調查限制在一次一個資料庫。如果這個資料庫中只有一個 autovacuum 執行中,且每個資料庫都有多個資料列,這個數量就足夠了。您必須在 WHERE 中加入額外的篩選條件 ('AND relid= …''),才能指明單一自動清除資料列。下一個查詢也是如此。

儲存基礎值後,您可以執行下列查詢:

with q as (
    SELECT datname,
           phase,
           heap_blks_total,
           heap_blks_scanned,
           clock_timestamp() - current_setting('save.ts')::timestamp AS ts_delta,
           heap_blks_scanned - current_setting('save.heap_blks_scanned')::bigint AS scanned_delta
     FROM pg_stat_progress_vacuum
     WHERE datname = DB_NAME
), q2 AS (
SELECT *,
       scanned_delta / extract('epoch' FROM ts_delta) AS pages_per_second
  FROM q
)
SELECT *,
       (heap_blks_total - heap_blks_scanned) / pages_per_second AS remaining_time
  FROM q2
;
┌─[ RECORD 1 ]──────┬──────────────────┐
│ datname            DB_NAME          │
│ phase              scanning heap    │
│ heap_blks_total    9497174          │
│ heap_blks_scanned  18016            │
│ ts_delta           00:00:40.30126   │
│ as_scanned_delta   11642            │
│ pages_per_second   288.87434288655  │
│ remaining_time     32814.1222418038 │
└───────────────────┴──────────────────┘

這項查詢會將目前值與儲存的基礎值進行比較,並計算 pages_per_secondremaining_time,讓我們判斷 VACUUM 是否執行得夠快,或是否需要加快速度。remaining_time 值僅適用於 scanning heap 階段。其他階段也需要時間,有時甚至需要更長的時間。你可以進一步瞭解吸塵器,並查看網路上的部落格文章,瞭解吸塵器的某些複雜面向。

加快 VACUUM 速度

如要讓 VACUUM 掃描速度加快,最簡單快速的方法就是設定 autovacuum_vacuum_cost_delay=0。您可以透過Google Cloud 控制台執行這項操作。

很抱歉,已在執行的 VACUUM 不會擷取這個值,因此您可能需要重新啟動資料庫。

重新啟動後,您可能會看到類似以下的結果:

┌─[ RECORD 1 ]──────┬──────────────────┐
│ datname            DB_NAME          │
│ phase              scanning heap    │
│ heap_blks_total    9497174          │
│ heap_blks_scanned  222382           │
│ ts_delta           00:00:21.422615  │
│ as_scanned_delta   138235           │
│ pages_per_second   6452.76031894332 │
│ remaining_time     1437.33713040171 │
└───────────────────┴──────────────────┘

在這個範例中,速度從 <300 頁/秒提升至約 6500 頁/秒,堆積掃描階段的預期剩餘時間則從 9 小時縮短至 23 分鐘。

其他階段的掃描速度不容易評估,但應該會顯示類似的加速效果。

另外,請考慮盡可能將 autovacuum_work_mem 設為較大,以免多次掃描索引。每當記憶體填入無效元組指標時,就會發生索引傳遞。

如果資料庫未用於其他用途,請將 autovacuum_work_mem 設為在允許 shared_buffers 所需的記憶體量後,保留約 80% 的記憶體空間。這是每個由 autovacuum 啟動的 VACUUM 程序的上限。如果您想繼續執行唯讀工作負載,請減少記憶體用量。

其他可提高速度的方式

避免清空索引

對於大型資料表,VACUUM 會花費大部分時間清理索引。

PostgreSQL 14 提供特殊最佳化功能,可在系統有循環回風險時避免索引清理作業。

在 PostgreSQL 12 和 13 中,您可以手動執行下列陳述式:

VACUUM (INDEX_CLEANUP OFF, TRUNCATE OFF) <tablename>;

在 11 以下版本中,您可以在執行清除作業前 DROP 索引,並在之後重新建立索引。

在資料表上執行自動清除作業時,如要刪除索引,就必須取消執行中的清除作業,然後在自動清除作業再次管理該資料表的清除作業之前,立即執行刪除索引指令。

首先,請執行下列陳述式,找出需要終止的 autovacuum 程序 PID:

SELECT pid, query 
  FROM pg_stat_activity
 WHERE state != 'idle'
   AND query ilike '%vacuum%';

接著執行下列陳述式,終止執行中的清除作業,並刪除一或多個索引:

SET cloudsql.enable_maintenance_mode = 'on'; /* get extra transaction ids */
SELECT pg_terminate_backend(<pid>);DROP INDEX <index1>;DROP INDEX <index2>; ...

捨棄違規資料表

在某些罕見情況下,您可以刪除表格。舉例來說,如果是從備份或其他資料庫等其他來源輕鬆還原的表格。

您仍需要使用 cloudsql.enable_maintenance_mode = 'on',並可能還要終止該資料表上的 VACUUM,如上一節所述。

真空裝置已滿

在少數情況下,執行 VACUUM FULL FREEZE 會更快,通常是當資料表只有少量即時元組時。您可以透過 pg_stat_user_tables 檢視畫面查看這項資訊 (除非發生當機而導致統計資料遭到清除)。

VACUUM FULL 指令會將即時元組複製到新檔案,因此必須有足夠的空間可供新檔案及其索引使用。

後續步驟