克服交易 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;

這項查詢會傳回造成問題的資料表。

適用於 TEMPORARY 資料表

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

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

如果這是唯一的阻礙,大約一分鐘後,自動清除程序就會擷取這項變更,並在 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 萬。這個值是 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 必須在 100 萬的環繞保護限制內。在這種情況下,您必須先移除 VACUUM 的封鎖程式,才能繼續操作。

移除 VACUUM 阻斷器

長時間執行的交易

在上述查詢中,如果 oldest_running_xact 等於 oldest_prepared_xact,請前往「Orphaned prepare transaction」(孤立的準備交易) 部分,因為「latest running」(最新執行) 值也包含準備交易。

您可能需要先以 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 或 Trusted 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 值為健康 (年齡 == 59)。如果年齡接近 20 億,您會想要刪除該時段。如果查詢傳回多筆記錄,您無法輕易判斷哪個是哪個副本。因此,請檢查所有副本,以免有任何副本上的交易執行時間過長。

副本上長時間執行的交易

使用 hot_standby_feedback 檢查最舊的執行中交易副本,並在副本上停用該交易。on

pg_stat_replication 檢視表中的 backend_xmin 資料欄含有副本上最舊的必要 TXID

如要繼續執行,請停止備用資源上阻礙作業的查詢。如要找出造成延遲的查詢,請在「Long running transactions」(長時間執行的交易) 中使用該查詢,但這次請在副本上執行。

你也可以選擇重新啟動副本。

設定 VACUUM

設定下列兩個標記:

  • autovacuum_vacuum_cost_delay = 0
  • autovacuum_work_mem = 1048576

第一個指令會停用 PostgreSQL 的任何磁碟節流,讓 VACUUM 以全速執行。根據預設,自動清除程序會受到節流,因此不會用盡最慢伺服器上的所有磁碟 IO。

第二個標記 autovacuum_work_mem 會減少索引清除作業的次數。如果可以,這個緩衝區應夠大,可儲存 VACUUM 要清除的資料表中所有已刪除資料列的 ID。設定這個值時,請注意這是每個執行的 VACUUM 可分配的本機記憶體最大量。請確認您允許的用量未超過可用量,且保留部分用量。如果資料庫持續以唯讀模式執行,請一併考量用於唯讀查詢的本機記憶體。

在大多數系統中,請使用最大值 (1 GB 或 1048576 KB,如範例所示)。 這個值最多可容納約 1.78 億個無效元組。如果超過這個數量,系統仍會多次掃描索引。

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

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

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

如要找出下一個需要 VACUUM 的資料庫和資料表,請使用「最佳化、監控 PostgreSQL 的 VACUUM 作業並進行疑難排解」中的查詢。如果伺服器 VM 的效能足夠,且頻寬可支援的平行 VACUUM 程序數量超過 autovacuum 啟動的程序,您可以手動啟動一些 VACUUM 程序。

檢查 VACUUM 速度

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

檢查正在執行的自動清除作業

系統檢視區塊 pg_stat_progress_vacuum 會顯示所有執行 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 一次只能針對一個資料庫進行調查。如果這個資料庫中只執行一個自動清除作業,且每個資料庫有多個資料列,這個數字就足夠。額外的篩選條件 ('AND relid= …'') 需要新增至 WHERE,以指出單一自動真空列。下一個查詢也是如此。

儲存基準值後,即可執行下列查詢:

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。您可以在Trusted 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% 的可用記憶體。這是每個自動啟動的 VACUUM 程序上限。如要繼續執行唯讀工作負載,請減少記憶體用量。

其他提升速度的方式

避免清除索引

如果是大型資料表,VACUUM 大部分的時間會用於清理索引。

如果系統有發生迴繞的風險,PostgreSQL 14 會進行特別最佳化,避免清除索引。

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

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

在 11 之前的版本中,您可以DROP索引,然後再重新建立。

如果自動清除作業已在資料表上執行,您必須先取消執行中的清除作業,然後立即執行 drop index 指令,才能捨棄索引,否則自動清除作業會再次開始清除該資料表。

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

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

然後執行下列陳述式,終止正在執行的 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 指令會將即時元組複製到新檔案,因此必須有足夠空間存放新檔案及其索引。

後續步驟