ガベージコレクション:MVCC バージョンのvacuumとpurge
テーブルが原因不明に膨らむ謎を、不要バージョンを回収する仕組みから解けます。PostgreSQL の VACUUM・HOT と InnoDB の purge スレッドの判断基準を押さえれば、長時間トランザクションが招く膨張を予防できます。
- 1.MVCC は古い版を即座には消さず、誰からも見えなくなった版を後追いで回収する。回収可能ラインは最古の読み手が辿りうる位置で決まる。
- 2.PostgreSQL は VACUUM が死んだタプルを再利用可能化し、HOT で更新を局所化、freeze で XID 周回を防ぐ。InnoDB は purge スレッドが不要 undo を非同期解放する。
- 3.長時間トランザクションは回収可能ラインを過去に固定し、bloat や undo 膨張・purge 遅延を招く。短いトランザクションが唯一の根本対策。
なぜ「回収」が独立した問題になるのか
MVCC は更新を上書きではなく新しい版の追加で表現します(→ MVCC の内部実装)。この設計の必然的な代償が、誰からも見えなくなった古い版が物理的に残り続けることです。これを片付けるのが MVCC のガベージコレクション、すなわち PostgreSQL の VACUUM と InnoDB の purge です。ここではその回収器の内部、いつ・何を・どの基準で消すのかを掘り下げます。
回収の中核は1つの問いに尽きます。「この版は、現在および将来の読み手の誰からも二度と参照されないか」。一度でも参照しうる読み手が残っていれば消せません。この境界を PostgreSQL では xmin horizon、InnoDB では read view の最古点と呼びます。回収器の賢さも限界も、この境界の計算精度で決まります。
回収可能ラインの計算
回収できるのは、削除(または旧版化)した XID が「全ての生きたスナップショットから見てコミット済み」になった版だけです。判定の擬似コードは次のとおりです。
removable(dead_version):
killer = dead_version を消した XID
# killer が、現存する最古スナップショットより前にコミット済みなら
# どの読み手にとっても「既に消えた版」= 回収可能
return committed_before(killer, oldest_live_snapshot)
ここで oldest_live_snapshot(最古の生きたスナップショット)が鍵です。PostgreSQL はクラスタ全体で最も古い実行中トランザクションの xmin を集計し、それより前に消えた死タプルだけを回収対象とします。hot_standby_feedback を有効にしたレプリカや、準備済みの2相コミットトランザクションもこの境界を後ろに引っ張ります。InnoDB も同様に、最古の read view が参照しうる undo より新しいものは解放しません。
どれだけ死んだ版が積み上がっても、最古の読み手が前進すれば一気に回収可能になります。逆に死んだ版が1件でも、最古の読み手が固まっていれば永遠に回収できません。問題は版の量ではなく境界の位置、という視点が運用判断の軸になります。
PostgreSQL:VACUUM・HOT・freeze の三層
PostgreSQL の回収は性格の異なる3つの仕事を兼ねます。
| 仕事 | 対象 | 効果 |
|---|---|---|
| 死タプル回収 | xmin horizon より古い dead tuple | 領域を再利用可能化し bloat を抑制 |
| インデックス整理 | 死タプルを指す不要なインデックスエントリ | インデックス側の肥大化を解消 |
| freeze | 十分に古い生きたタプルの XID | XID 周回(wraparound)を防止 |
通常の VACUUM はファイルを OS に返さず、死タプルが占めた領域を同じテーブル内の空きリストに載せ、次の挿入や更新へ回します。ファイルを実際に縮めるには VACUUM FULL(テーブル全体を書き直し、排他ロックを取る)が必要で、運用では避けたい操作です。
autovacuum はテーブルごとの死タプル推定数がしきい値を超えると起動します。おおまかな式は次のとおりです。
threshold = autovacuum_vacuum_threshold
+ autovacuum_vacuum_scale_factor * 行数
死タプル数 > threshold なら autovacuum を起動
scale factor の既定は 0.2 なので、大きなテーブルほど起動が遅れ、bloat が表面化しやすくなります。巨大テーブルでは scale factor を個別に小さくするのが定石です。
HOT:そもそも版チェーンを短くする
HOT(Heap-Only Tuple)は更新がインデックス対象列を変えず、かつ同一ページ内に新版を置ける場合の最適化です。新版はインデックスから直接は指されず、旧版からページ内のポインタで辿られます。これによりインデックス更新を省け、さらに通常の SELECT 時にも不要になった中間版をその場で外す HOT pruning(軽量な版回収)が働きます。HOT が効くかどうかで bloat の進み方が大きく変わるため、頻繁に更新される列はインデックスから外す設計が有効です(→ B-Tree の内部構造)。
freeze:有限な XID を守る
XID は約42億で一周する有限値です。古い行の XID を放置すると、周回した新しい XID が「未来」と誤認され可視性判定が壊れます。これを防ぐため VACUUM は十分に古い行を「凍結(freeze)」し、常に見える版として印を付けます。
freeze が間に合わず周回の危険水位に達すると、PostgreSQL はデータ破損を避けるため強制的に anti-wraparound VACUUM を走らせ、最悪の場合は新規トランザクションの受付を停止します。大量更新を続けるシステムで autovacuum を止めるのは禁忌です。age(relfrozenxid) の監視が予防になります。
InnoDB:purge スレッドと history list
InnoDB は本体(クラスタ化インデックス)に最新版だけを置き、旧版を undo ログから再構成する方式です(→ ロックと MVCC)。回収対象は本体の死タプルではなく、不要になった undo ログレコードで、これを専任の purge スレッドが非同期に処理します。
purge の仕事は2つです。第一に、どの read view からも参照されなくなった undo レコードを解放すること。第二に、削除マーク付きのレコードを本体とセカンダリインデックスから実際に物理削除すること(InnoDB の DELETE はまず削除マークを付け、purge が後で実体を消す遅延削除です)。
未解放の undo の長さは history list length という指標に表れ、SHOW ENGINE INNODB STATUS で確認できます。これが増え続けるのは purge が最古 read view に阻まれて前進できないサインです。
| 観点 | PostgreSQL(VACUUM) | InnoDB(purge) |
|---|---|---|
| 膨らむ場所 | テーブル/インデックス本体(bloat) | undo テーブルスペース |
| 実行主体 | autovacuum / 手動 VACUUM | purge スレッド(既定で複数) |
| 遅延の指標 | 回収できない死タプル数・age(relfrozenxid) | history list length |
| 削除の扱い | dead tuple として回収 | 削除マーク後に物理削除(遅延削除) |
| 固有の責務 | XID freeze による wraparound 防止 | セカンダリの purge も担当 |
長時間トランザクションが回収を止める
両エンジンに共通する最大の落とし穴が、長時間トランザクションです。読みっぱなしの BEGIN が1本でもあると最古スナップショットがそこに固定され、回収可能ラインが前進しません。
最も危険なのは BEGIN 後に何もせず開いたままのアイドルトランザクションです。本人は1行も読んでいなくても、回収器は「将来古い版を参照するかもしれない」と判断し続けます。結果、PostgreSQL では死タプルが回収されず bloat が進行し、InnoDB では undo が解放されず history list length が膨張し、purge 全体が停滞します。PostgreSQL は idle_in_transaction_session_timeout で、アプリ側は接続プールのトランザクション境界の見直しで対処します(→ トランザクション分離レベル)。
兆候の見つけ方を押さえておきます。PostgreSQL では pg_stat_activity の古い xact_start と backend_xmin、肥大化したテーブルの dead tuple 数。InnoDB では history list length の継続的な増加。どちらも「最古の読み手は誰か」を突き止め、それを終わらせれば回収が一気に進みます。
まとめ
- MVCC の回収器が消せるのは「最古の読み手から見て確実に消えた版」だけで、回収の成否は版の量ではなく回収可能ラインの位置で決まる。
- PostgreSQL の VACUUM は死タプル回収・インデックス整理・freeze による wraparound 防止の三役を担い、HOT が版チェーンの伸びそのものを抑える。
- InnoDB の purge スレッドは不要 undo の解放と削除マークの遅延物理削除を非同期に行い、停滞は history list length に表れる。
- 両者に共通する根本対策は、最古スナップショットを固定する長時間・アイドルトランザクションを排除すること。直列化可能性との関係は 直列化可能スナップショット分離 も参照。
データベース Article
ガベージコレクション:MVCC バージョンのvacuumとpurgeを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
データベース
比較で見る軸
難易度: advanced / カテゴリ: データベース / タグ数: 6
導入後に効く点
PostgreSQL は VACUUM が死んだタプルを再利用可能化し、HOT で更新を局所化、freeze で XID 周回を防ぐ。InnoDB は purge スレッドが不要 undo を非同期解放する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データベース
- タグ数
- 6
判断チェックリスト
- 自社の用途が「データベース / MVCC」に近いか確認する。
- 強みである「MVCC は古い版を即座には消さず、誰からも見えなくなった版を後追いで回収する。回収可能ラインは最古の読み手が辿りうる位置で決まる。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。