0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「postgresのxid周回は普通気にしなくても良い」と言いたいまとめ

Last updated at Posted at 2025-03-30

はじめに

postgresを9系の頃に利用し始めて、今はもう16とか17とかも使っている。
でも別に深い機能とか高負荷とかの利用はしていない。
そんなライトな運用でも何となく以前から「xid周回は気を付けろ」という話はあったが、
デフォルトでautovacuumしてるせいか、特に問題になったことはない。
一度、16のデフォルト設定で、「postgresのxid周回は普通気にしなくても良い」と言いたいまとめを行ってみる。


エグゼクティブサマリ

0. エグゼクティブサマリ

PostgreSQL XID周回に関するエグゼクティブサマリ

■ 結論

PostgreSQLでは、XID(トランザクションID、32bit、42億)の周回そのものは問題ではありません。
真に危険なのは「XIDの差分(age)が約21億を超えること」です。
これは PostgreSQL の内部仕様上、可視性判定(MVCC)において重大な誤動作を引き起こすため、
PostgreSQL はその手前で 自動的に書き込みを停止し、整合性を保護します。
しかし、サービス提供中に、DB停止、詰まりサービス停止を発生させてはならないため、
XIDの差分(age)が2億を超えて増加し続けていないか、 定期的に監視を継続してください。


■ なぜ21億が限界なのか?

  • XIDは32bitの符号なし整数(最大値:約42億)
  • しかし内部では int32符号付き )で比較されるため、±2^31(≒21億)が実質のカウンタ範囲
  • この差分(age)が超えると、可視性判定(「行が見えるか」)が破綻する

■ PostgreSQLの整合性担保の安全装置(多段防御)

段階 動作 発動条件
通常 autovacuum テーブルの更新率や age に基づき随時発動
強制 autovacuum age(datfrozenxid) ≥ 2億(デフォルト)
書き込み拒否(FATAL) age(datfrozenxid) ≥ 2^31(約21億)

→ この仕組みにより、XIDは何度でも安全に再利用される


■ 現実的に注意すべきポイント

「XID周回を恐れる必要はない」
実際に注意すべきは、freezeが追いつかず age が積み上がること

よくある原因:

  • autovacuumが無効(デフォルトは有効)
  • worker不足(デフォルトは3)
  • ロングトランザクションの放置
  • 巨大テーブルの更新負荷(1億レコードでSSDでも数十秒の可能性あり)
  • 統計情報の陳腐化
  • 不要なレプリケーション設定残存

■ 推奨される定期監視(週次または月次)

項目 目的 取得例
age(datfrozenxid) 全体のXID進行度 SELECT age(datfrozenxid) FROM pg_database;
age(relfrozenxid) テーブル単位のfreeze遅延確認 SELECT relname, age(relfrozenxid) FROM pg_class ...
ロングTXの有無 freezeブロックの兆候 SELECT * FROM pg_stat_activity WHERE state='idle in transaction';
autovacuumの実行状況 正常稼働の確認 pg_stat_user_tablesautovacuum_countなど

■ まとめ

  • XIDが42億で周回すること自体は仕様であり問題なし(凍結されていれば再利用可能)
  • ageが21億を超えると可視性が壊れ、整合性リスク(=致命的)
  • ✅ 通常はautovacuumで安全に防止されている
  • ❗ autovacuumが阻害され続けると最初の予兆、実害として発生し得るのは、強制autovacuumによる性能劣化
  • 速度感としては、100TPS継続なら約23日で2億のXIDを消費。担当システムの速度感把握が必要

つまり:心配すべきは「周回」ではなく、「凍結が間に合っているかどうか」。
正常な設定と監視さえしていれば、XID周回自体は“心配しなくていい技術的な話”です。


1. PostgreSQLにおける xid・autovacuum・wraparound に関する整理まとめ(概念寄り)


【1】PostgreSQLのxidの基本

項目 内容
xid(Transaction ID) PostgreSQLがトランザクションを一意に識別するための32bit符号なし整数(最大4,294,967,296 ≒ 42億)。MVCCでの可視性の管理に必須
xidのwraparound xidは42億の半分で1周し、古いxidを新しいと誤認してしまう危険(整合性崩壊)がある
xidの消費単位 1トランザクションにつき1つのxidが消費される。更新レコード数には依存しない
xidのスコープ データベースクラスタ単位

参考リンク

postgresにおける「トランザクション」とは?

PostgreSQLにおける「トランザクション」とは、一貫性あるデータ状態を保証するための操作の最小単位です。
明示的な BEGIN ~ COMMIT だけでなく、1SQLごとに自動でトランザクションが生成されることもあります。

  • ユーザーが開始した明示的トランザクション
  • システム内部で実行される自動トランザクション

の両方を含みます。
例えば:

  • 通常のSQLでの BEGIN; ... COMMIT;
  • 自動VACUUMで発行される内部トランザクション
  • インデックス再構築、ANALYZEなど

これらもすべて「トランザクション」として扱われ、それぞれに xid が割り振られます。

更新系と参照系の違い

種類 必ず xid が割り当てられるか? 可視性制御 WAL出力
更新系(INSERT/UPDATE/DELETEなど) はい MVCCで管理 あり(WAL出力)
参照系(SELECTなど) 実際には xid 割り当て無し(xid = 0) スナップショット取得のみ なし
  • 参照系は、読み取り専用トランザクションとして扱われ、xidを消費しません(xid = 0扱い)。
  • そのため、大量のSELECTが発生してもxidの枯渇(wraparound)問題には関係ありません

vxid / xid の関係と動作タイミング

項目 内容
Virtual Transaction ID(vxid) トランザクション開始時に常に割り当てられる(読み取り専用でも)。可視性の確認のため必須
Transaction ID(xid) 実際に行の変更操作が発生した時点で初めて割り当てられる。読み取り専用トランザクションでは xid = 0(無効)
スナップショットの取得 トランザクションの最初のSQL実行時に取得され、そのスナップショットを元に行の可視性を判定
明示的トランザクション中にSELECTだけの場合 vxidはあるが、xidは割り当てられない。途中でUPDATEがあればその時点でxidが付与される
  • xid および vxid は、データベース単位ではなく、クラスタ全体(インスタンス単位)で共有されます。
  • よって、スキーマやデータベースが違っていても、同一クラスタ内であれば共通の xid / vxid カウンタが使われます。
  • vxidの形式 backend_pid/local_xid(例:12345/1)

なぜ vxid に wraparound 問題が起きないのか?

  • 理由1:プロセスごとのローカルID(スコープが限定的)
    • local_transaction_id はバックエンドプロセスごとにカウントされるため、クラスタ全体での一意性は不要
    • 周回しても そのプロセス内の範囲でしか意味を持たない ため、他と衝突することがありません
  • 理由2:vxidの寿命が短く、一時的な識別子
    • vxid はトランザクションの実行中のみ有効
    • トランザクションが終了すれば、vxidの情報も破棄される
    • よって、「古いvxidが残って比較に影響する」ような状況がありません

【2】wraparoundリスクよりこっちの方が近い、危ない

条件 説明
age(datfrozenxid) > 2^31 = 2,147,483,648 PostgreSQLはトランザクションIDがデフォルトで21億以上古くなると、新しいトランザクションを拒否(FATAL) して整合性を守ろうとする。これが「wraparound防止措置」。42億の半分じゃん!
autovacuum_freeze_max_age(デフォルト2億)を超過 PostgreSQLは強制的にautovacuumを起動し、xidのfreezeを試みる(「to prevent wraparound」のログ)。21億の10分の1じゃん!!何か凄く近く感じるじゃん !

→freezeが間に合わないと datfrozenxid が古いままになり、最終的にwraparoundリスクの制御ライン(21億)を突破し、DBが書き込み不能に陥る (300万件手前)

参考リンク

こうした警告も無視し続け、周回するまでのトランザクションが300万より少なくなると、システムは新しいXIDの割り当てを拒絶します。

【3】xidの消費速度と時間の目安

TPS 2億xidに到達する時間
1000 TPS 約2日7時間
100 TPS 約23日
10 TPS 約231日
1 TPS 約6.3年

※ 高負荷の場合xid消費はアプリのDMLが主因であり、システムカタログやpg_statsinfoなどの拡張ツールによる消費は微小

参考リンク


【4】autovacuumの2つの目的とトリガ

種類 トリガ条件 目的 xid消費
通常のautovacuum dead tuple数が (50 + 0.2 × reltuples) を超える 不要な行バージョン(dead tuple)の回収 DELETEやUPDATEありならxid消費あり
freeze目的のautovacuum age(relfrozenxid) > autovacuum_freeze_max_age(デフォルト:2億) xidの凍結(freeze)を行い、wraparoundを防止 xidは消費されない(既存行の内部情報のみ更新)

参考リンク


【5】autovacuumが回避される/freezeできないシナリオ

シナリオ 原因 結果
小規模テーブルを量産 INSERTがしきい値未満(50件未満) vacuum対象にならない。xidだけが進行する
TRUNCATEの繰り返し 行データ自体が消えるため、dead tupleが発生せず、vacuum対象外になる xidは進むが、freezeは発生しない
ロングトランザクション BEGINしたまま放置すると OldestXmin が更新されず、autovacuumが凍結できない xidは進行し、datfrozenxidが古いまま残る
巨大テーブルに大量更新 vacuumに時間がかかる上、worker数(デフォルト3)が不足 他テーブルが後回しになり、freezeが追いつかない
autovacuumの設定制限 worker数・cost_limitが小さい スループット不足でvacuumが遅延・未実行になる
統計情報陳腐化 analyzeしきい値未満 vacuum対象にならない。xidだけが進行する
不要なレプリケーション設定残存 walが削除できない。レプリカ先ではまだ必要なXIDだと判断されてしまう vacuum対象にならない。xidだけが進行する
  • 「create、49レコードinsert、truncate」を毎秒繰り返す
  • 1億レコードテーブル10個を毎秒2000万レコード更新する

参考リンク


【6】TPSとdead tupleの関係

  • xid消費はトランザクション単位(1 TPS = 1 xid)
  • dead tupleは「更新された行数」に比例して増える
  • つまり、TPSが低くても1回のトランザクションで数千万行UPDATEすれば、毎秒数千万のdead tupleが発生可能

参考リンク


【7】運用リスクの整理

リスク 発生ポイント 対策
性能劣化 アプリ動作中に強制autovacuumが発生するとI/Oを使いすぎる(freeze) コスト調整、worker数増加、VACUUM FREEZEの計画実行
wraparoundによるDB停止 xidが21億進行し、凍結が間に合わない pg_stat_database で age を監視、定期的な凍結確認
設計上の落とし穴 TRUNCATE多用、小規模テーブル量産、ロングTX freeze対象から外れやすい構成を避けること
  • autovacuumは排他ロックではないため、性能としては、リソース逼迫が懸念となる

参考リンク

The autovacuum daemon attempts to work this way, and in fact will never issue VACUUM FULL. 

【8】事故再現可能な極端シナリオ(机上実験)

条件 内容
毎秒1000トランザクション × 数日運用 xidが2億以上進行
autovacuum無効 or 強制停止 vacuumが一切動かない or freeze不能
TRUNCATE + INSERTの繰り返し xid進行するがvacuumされない構成
ロングTXでOldestXminが進まない freezeできない構成完成

これらを意図的に組み合わせ、vacuumを阻害し続ければ、事故は再現可能


【9】ロングトランザクションの制御について

項目 内容
PostgreSQLのデフォルト BEGIN〜COMMIT/ROLLBACK しない限り無期限に保持可能
設定値 idle_in_transaction_session_timeout = 0(無効)
対策 明示的にtimeoutを設定(例:60000ミリ秒 = 60秒)

参考リンク


【10】まとめ:安心と警戒のバランス

  • PostgreSQLはデフォルトでautovacuumによるfreezeが有効なため、通常設計でwraparound事故、防止機能でのDB機能停止事故はまず起きません
  • しかし、複数の「意図的な高負荷設計・運用」が組み合わさると、事故は再現可能です
  • 運用的には、「ログ出力、性能劣化(age ≈ 2億)」という初期兆候の時点で気付けば、機能停止事故(age ≈ 21億) はほぼ確実に回避できます
  • 初期兆候としている2億歳になることも、かなり極端な設計、実装、運用と思います
  • 定期的にログの確認、システムカタログやビューの確認を継続しましょう

【11】監視sh案

以下は週次・月次にCSV形式でローテーション出力するbashスクリプト例です。

#!/bin/bash

# 出力先ディレクトリ
OUTDIR="/var/log/pgwrapmon"
mkdir -p "$OUTDIR"

# タイムスタンプ
DATE=$(date +"%Y%m%d_%H%M%S")

# 出力ファイル
OUTFILE="$OUTDIR/pgwrapmon_${DATE}.csv"

# PostgreSQL接続設定(適宜変更)
PGUSER="postgres"
PGDATABASE="postgres"
PGOPTIONS="--tuples-only --csv"

# SQL取得
psql -U "$PGUSER" -d "$PGDATABASE" $PGOPTIONS <<EOF > "$OUTFILE"
-- xid進行とfreeze age(DB単位)
SELECT now() as timestamp,
       datname,
       age(datfrozenxid) as db_age,
       txid_current() as current_xid
FROM pg_database;

-- xid age(テーブル単位:上位20)
SELECT now() as timestamp,
       c.relname,
       n.nspname,
       age(c.relfrozenxid) as rel_age,
       s.n_live_tup,
       s.n_dead_tup,
       s.autovacuum_count,
       s.analyze_count
FROM pg_class c
JOIN pg_stat_user_tables s ON c.oid = s.relid
JOIN pg_namespace n ON c.relnamespace = n.oid
WHERE c.relkind = 'r'
ORDER BY rel_age DESC
LIMIT 20;
EOF

cronへの登録は以下の感じです。

crontab -e

週次(日曜深夜3時)

0 3 * * 0 /usr/local/bin/pg_xid_monitor.sh >> /var/log/pgwrapmon/cron.log 2>&1

月次(1日深夜3時)

0 3 1 * * /usr/local/bin/pg_xid_monitor.sh >> /var/log/pgwrapmon/cron.log 2>&1
  • xid進行が急増していないか → current_xid の増加差分をチェック
  • 特定テーブルのrel_ageだけ上がり続けている → freeze漏れがないか確認
  • autovacuum_countがゼロでn_dead_tupだけ増加しているテーブル → autovacuum設定の閾値未満か、異常か

出力CSVイメージ(例:pgwrapmon_20250330_030000.csv)

Part 1:データベース単位(pg_database + xid進行)

timestamp,datname,db_age,current_xid
2025-03-30 03:00:00,postgres,105230000,389201234
2025-03-30 03:00:00,mydb,98430000,389201234
2025-03-30 03:00:00,template1,98000000,389201234
  • db_age:各データベースの age(datfrozenxid)(= xidがどれだけ古いままか)
  • current_xid:この時点での全体のxid番号(= 進行度確認用)

Part 2:テーブル単位(pg_class + pg_stat_user_tables:上位20件)

timestamp,relname,nspname,rel_age,n_live_tup,n_dead_tup,autovacuum_count,analyze_count
2025-03-30 03:00:00,sensor_log,public,192350000,12483723,238472,120,98
2025-03-30 03:00:00,orders,public,188270000,34567890,10234,86,74
2025-03-30 03:00:00,users,public,170300000,98234,150,21,13
...
  • rel_age:テーブル単位の age(relfrozenxid)(= そのテーブルの最古行がどれだけ古いか)
  • n_dead_tup:dead tuple の蓄積傾向(autovacuumの必要性の指標)
  • autovacuum_count, analyze_count:vacuum/analyzeの実行履歴(動いているかどうかの実績)

2. PostgreSQL における age / xid / frozenxid の関係整理(実装寄り)


【1】xid・凍結・age の関係まとめ

● xid の基本

  • PostgreSQLの各トランザクションには一意の xid(Transaction ID)が割り当てられる(32bit、最大42億)
  • 各レコードには xmin(作成xid)、xmax(削除xid)がシステムカラムとして存在する
  • トランザクションの可視性は xmin / xmax に基づいて判断される(MVCC)

参考:


● 凍結(freeze)の目的と処理

  • 古すぎて全トランザクションから「確実に過去」と判断できる xmin は凍結される
  • 凍結された xmin は特別な値(例:2 = FrozenTransactionId)に置換され、wraparound対象から除外される
  • 凍結されると、その行の xmin は以後 age の対象にならない

参考:


【2】age の種類とスコープ

関数 スコープ 説明
age(xmin) 行単位 その行が作成されてからのトランザクション差分
age(relfrozenxid) テーブル単位 テーブル内で最も古い未凍結行のxidとの差分
age(datfrozenxid) データベース単位 すべてのテーブル中で最も古い未凍結xidとの差分
txid_current() クラスタ全体 現在進行中のxid(ageの基準点)

age(x) = txid_current() - x(内部ではラップアラウンド対応の計算)

参考:


【3】relfrozenxid / datfrozenxid の関係

  • 各テーブル:relfrozenxid により「テーブル内の最古の未凍結行のxid」を記録
  • 各データベース:datfrozenxid により「データベース全体の最古xid」を記録
  • datfrozenxid = データベース内のすべての relfrozenxid の最小値

参考:


【4】システムカタログの扱い

  • pg_classpg_attribute などのシステムカタログも通常のテーブルとして管理される
  • 当然、relfrozenxid を持ち、freeze対象となる
  • つまり、ユーザデータがなくてもxidやageは進行し、wraparoundリスクは存在する

参考:


【5】wraparound予兆監視の意味

  • age(datfrozenxid):データベース全体の安全指標(21億超でFATAL)
  • age(relfrozenxid):テーブル単位の遅延検出・監視に使える
  • これらを組み合わせることで、精度の高いwraparoundリスク管理が可能

参考:


可視性と凍結とageスコープの関係


【6】まとめ

  • xid はクラスタ共通、すべてのageの基準
  • ageはスコープに応じて以下のように使い分ける:
    • 行単位: 可視性判定
    • テーブル単位: freeze遅延の発見
    • データベース単位: wraparound予兆の最前線
  • relfrozenxid をfreezeしていけば datfrozenxid も更新され、wraparoundリスクを抑制できる

3. PostgreSQL における wraparound / xid / freeze の関連ソースコードまとめ

【1】. トランザクションID(XID)の割り当て


【2】. 行の凍結(freeze)処理

  • ファイル: src/backend/access/heap/heapam.c
  • 関数: heap_tuple_freeze
  • 概要:
    HEAP_XMIN_FROZEN フラグが設定され、以降の可視性判断・vacuum 対象外になる。

【3】. 自動バキューム(autovacuum)処理


【4】. テーブルごとの最古のXID(relfrozenxid)の管理


【5】. データベース全体の最古XID(datfrozenxid)の管理


【6】. 統計情報とANALYZE処理


【7】. 可視性判断(MVCC)

  • ファイル: src/backend/access/heap/heapam_visibility.c
  • 関数: HeapTupleSatisfiesMVCC
  • 概要:
    行の xmin, xmax, コミット状態を元に、その行が「現在のトランザクションから見えるか」を判断する。MVCCの根幹処理。

【8】. XID 可視性の判断ロジック(MVCC)

  • ファイル: src/include/access/transam.h
  • 定義例: FrozenTransactionId
  • 概要:
    xid 2 は特別な値として予約されており、凍結済み行に対して割り当てられる。

4. PostgreSQL における有限IDの使い切り(枯渇)と対策の有無まとめ

xid以外にアーキテクチャ上何か気を付けることないの?という確認です。
→結論、設計上のNGあり、となります。


比較表:主要リソースの枯渇リスクと対策の有無

リソース ビット数 枯渇可能性 極端な例 対策の有無 危険度 説明
xid 32bit 〇 現実的 1000TPS × 90日 ≒ xid 21億進行 あり(freeze) 整合性に直結、最重要
OID 32bit △ 理論上可能 大量オブジェクト作成と削除の繰返し × ほぼなし 低〜中 アプリがOIDに依存すると危険
relfilenode 32bit △ 理論上可能 テーブルの作成・削除を大量実行 × なし(内部再利用) 一意IDに使うと危険
sequence 64bit △ 設定次第で可能 MAXVALUE=1000 などの設定ミス 〇 CYCLE / RESET 枯渇時に挙動選択可
LSN(WAL) 64bit × 非現実的 秒間数GB WALを数年続ける × なし 実質気にしなくてOK
ctid - × 枯渇なし ページ内に多数の行 → 再利用される 〇 自動管理 一意IDに使うと危険
VXID 内部 〇 枯渇し得る 並列TX多数 + ロングTX放置 〇 自動クリア ロック競合・スローダウン要因
backend ID 内部 〇 上限あり max_connections を超える接続 〇 パラメータ制御 OS制限に依存

なぜ xid は「特別に」対策されているのか?

可視性管理、DBの機能不全に直結するため

  • PostgreSQL の行ごとの可視性(MVCC)は xmin / xmax によって決まる
  • xidが周回すると、古いトランザクションが「未来の行」を見えてしまう=整合性破壊

そのため、2層の防御策がある:

対策 説明
1.autovacuum_freeze_max_age xid ageが2億超で強制freeze発動
2.最終防衛ライン(age > 2^31) FATALエラーで書き込み停止(wraparound保護)

他リソースは「使い切っても致命傷にはならない」が、設計次第で事故になる

リソース リスク例
OID アプリがOID値に依存 → 再利用で誤動作
sequence 上限設定ミス(MAXVALUE)で即停止。自動採番が動かない
VXID ロングトランザクションで枯渇 → ロック取得できない状況に
WAL/LSN pg_walディレクトリ肥大化 → ディスク枯渇、PITR障害など

極端な机上シナリオ例(検証目的)

リソース シナリオ例
OID テーブル作成→削除を4億回実行(実行には数ヶ月〜年単位)
sequence MAXVALUE=1000 設定で1001回目にINSERT失敗
VXID 10万並列TX + ロングTX放置で競合発生、トランザクション開始不能

運用上の推奨対応

  • ディスク容量不足はpostgresアーキテクチャ上の有限のカウンタ、という今回の主題ではないが、普通に不足し得るので監視すべき
  • pg_wal: 容量監視・保管数制限(archive_cleanup_command 等)
  • walのアーカイブ領域もあれば同様

まとめ

  • xidはPostgreSQLの整合性の心臓部。特別な保護設計がされている
  • 他リソースも「枯渇=停止」になるリスクがあるため、設計段階から意識すべき
  • 長期運用・高TPS環境では、有限リソースの枯渇を前提にした監視と対策が重要

おわりに

間違った記述あれば是非ご指摘お願いいたします。
基本的には、デフォルト設定で、凝ったことしない、極端な高負荷な利用はしない、ロングトランザクション放置とかしない、要らないレプリカ設定残存させない、
ということであれば、
統計情報更新、自動vacuum実行などの履歴を見て、管理対象の速度感を記録していく、
という感じで大丈夫だと思っています。
過度に「気にしなくても良い」。定期的に監視してそのDBの健康状態を把握していきましょう。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?