この記事はAdventカレンダー20日目の記事です。
本記事の内容は以下の環境で確認を行ったものになります。
| 名前 | バージョン |
|---|---|
| Rocky Linux | 9.7 |
| PostgreSQL | 18.1 |
はじめに
勉強目的にソースを眺めながら整理した内容なので、中身は面白くないと思います。
ただ、最近色々あり、特に仕事としてPostgreSQLを触れる機会が減ってしまったこともあり、ちょっとでもPostgreSQLに触れて、理解を深められることができればいいなと思って、これを書いています。
周回防止自動VACUUMって何か?
タイトルで書いている「周回防止自動VACUUM」とはなんぞや?という部分から入ります。
改めて説明する必要もないと思いますが、VACUUMはデータの不要領域の回収をして、データベースを健全な状態にするためのメンテナンスを行っています。
このVACUUM処理ですが、実は不要領域以外にもトランザクションIDの回収を行っています。
トランザクションIDを回収する処理をPostgreSQLでは、FREEZEと呼んでいます。VACUUM FREEZEというコマンドがありますが、これは明示的にトランザクションIDの回収を行うためのものとなります。
トランザクションIDについてはPostgreSQL文書をご確認ください。
非仮想TransactionId(またはxid)、例えば278394は、PostgreSQLクラスタ内のすべてのデータベースが使用するグローバルカウンタからトランザクションに順番に割り当てられます。 この割り当ては、トランザクションがデータベースに最初に書き込みを行ったときに行われます。 これは、低い番号のxidが、より大きな番号のxidよりも前に書き込みを開始したことを意味します。 トランザクションが最初にデータベースに書き込みを行った順序は、トランザクションの開始順序とは異なるかもしれないことに注意してください。 特に、トランザクションがデータベース読み取りのみを実行する文で開始した場合にはそうなります。
内部トランザクションID型xidは32ビット幅で、40億トランザクションごとに周回します。 32ビットエポックは各周回ごとに加算されます。 また、このエポックを含むために、インストールの寿命中には周回しない64ビット型xid8もあり、キャストでxidに変換できます。 表 9.82の関数はxid8値を返します。 XIDはPostgreSQLのMVCC同時実行機構とストリーミングレプリケーションの基礎として使用されます。
(引用元:PostgreSQL文書 66.1. トランザクションと識別子)
XIDとVACUUMの関係をすごくざっくり言うと、PostgreSQLがちゃんとDBとして動作するためにXIDが存在しているが、XIDは有限なので、定期的にメンテナンスして再利用する必要があり、そのメンテナンスをするのがVACUUMということになります。
で、今回はそのVACUUM処理の中でも「DBが壊れないようにするため」強制的に動作する(XIDの)周回防止のために起動する自動VACUUMについて確認してみるというものになっています。
周回防止向けの自動VACUUMの実行契機について
ここからはGUCパラメータや、ソースを見つつ、どういう契機で動作しているのか確認しておきます。
関連するGUCパラメータ
| パラメータ名 | 説明 |
|---|---|
| autovacuum_freeze_max_age | XIDが周回するのを防ぐために強制的に自動VACUUMが起動する閾値 |
| autovacuum_multixact_freeze_max_age | マルチXIDが周回するのを防ぐために強制的に自動VACUUMが起動する閾値 |
参考:https://www.postgresql.jp/document/17/html/runtime-config-autovacuum.html
ポイント
周回防止の自動VACUUM自体は autovacuum機能(デフォルト:有効)を明示的に無効化したとしても、機能しており、上記の設定に基づいてFREEZE処理を行う。
(つまり、どんなシステムであろうと、例外ない。PostgreSQLを使っていて、XIDやマルチXIDが消費されていくと基本的には必ず動く処理であるということ。)
周回防止VACUUM実行時のログ
サンプルとして周回防止の自動VACUUMが起動した際のログを以下に示す。
(autovacuum = offの設定で、XIDを消費させて強引に発火させています。)
2025-12-19 01:10:33.228 JST [2916] LOG: automatic aggressive vacuum to prevent wraparound of table "postgres.public.pgbench_accounts": index scans: 0
pages: 0 removed, 1640 remain, 1 scanned (0.06% of total), 0 eagerly scanned
tuples: 0 removed, 100000 remain, 0 are dead but not yet removable
removable cutoff: 200016091, which was 64 XIDs old when operation ended
new relfrozenxid: 200016091, which is 200015332 XIDs ahead of previous value
frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
visibility map: 0 pages set all-visible, 0 pages set all-frozen (0 were all-visible)
index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers removed
avg read rate: 2.982 MB/s, avg write rate: 1.988 MB/s
buffer usage: 49 hits, 6 reads, 4 dirtied
WAL usage: 5 records, 4 full page images, 28035 bytes, 0 buffers full
system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.01 s
今回は動作確認が目的なので仕込みとして、以下のように1.99億XIDだけ進めた状態にしています。
$ pg_resetwal -x 199000064 -D ~/local/pg18.1/data
Write-ahead log reset
199000064 = pg_xactの1ファイルあたりのXID数:32768 * ceiling(199000000 / 32768) で算出。
関連するソースはどこ?
src/backend/postmaster/autovacuum.c あたりを「wraparound」というキーワードで調べてみると、今回確認したい周回防止に関する処理まで行きつくことができます。マルチXIDも同じ関数内にあります。
static Oid
do_start_worker(void)
{
...
/* Check to see if this one is at risk of wraparound */
if (TransactionIdPrecedes(tmp->adw_frozenxid, xidForceLimit))
{
if (avdb == NULL ||
TransactionIdPrecedes(tmp->adw_frozenxid,
avdb->adw_frozenxid))
avdb = tmp;
for_xid_wrap = true;
continue;
...
return retval;
}
ちなみに、自動VACUUM処理は postgres: autovacuum launcher というプロセスから子プロセスとして、autovacuum workerプロセスがforkされて、workerプロセスが実際のVACUUM処理を行うようになっています。
もう少し具体的に言うと、AutoVacLauncherMain() → launch_worker() → do_start_worker(void)というような流れで必要に応じてworkerプロセスが起動されています。
周回防止の閾値の判定はどのようなロジックで行われているのか?
結論から言うと、
周回防止の自動VACUUMは、FREEZE済みのXIDがxidForceLimitを超えるかどうかで判断しているようです。
該当するソースは以下。
static Oid
do_start_worker(void)
{
...
recentXid = ReadNextTransactionId();
xidForceLimit = recentXid - autovacuum_freeze_max_age;
...
/* Check to see if this one is at risk of wraparound */
if (TransactionIdPrecedes(tmp->adw_frozenxid, xidForceLimit))
{
...
autovacuum_freeze_max_ageは、以下のように postgresql.conf で定義されているパラメータです。
(デフォルト:2億)
このあたりを読んでいくと、周回しそうなデータベースがないか、pg_database.datfrozenxidで確認していることがわかります。実際に現在のXIDの消費状況がxidForceLimitを超えているかどうかは内部関数のTransactionIdPrecedes()を使って判定しています。XIDは32bitの有限のものであり、それが循環して使用されているため、DBの利用が進行すると、数値が一周することになります。このとき、単純に数値の大小では比較してしまうと正しく判定できないので、内部関数で循環している状況でも正しく判定ができるようにしているようです。
余談ですが
ちなみにFREEZEされているか/いないかは、age関数(XIDがどれだけ消費されているか確認する関数)を使うと見やすいです。以下の例はXIDが2億近く消費されていて、しばらくFREEZEがされていないことを示しています。
postgres=# SELECT datname,age(datfrozenxid) FROM pg_database;
datname | age
-----------+-----------
postgres | 198999321
template1 | 198999321
template0 | 198999321
(3 rows)
デフォルトの設定値を改めて見ると、別に2億XID放置しても実際に周回するわけではないので、さすがにデフォルトとは言え、値が小さすぎるような気がしました。ログだけ見たら、もう周回しそうになったのか?と焦ってしまうような気がする。
周回防止の自動VACUUMが動作する頻度は?
これは autovacuum の設定が有効になっているかどうか、DBの利用状況、あとはFREEZEをどれくらい行うか制御するパラメータによっても変わります。
条件次第ではありますが、例えば以下の設定値の場合を前提にして考えてみると、、、
| パラメータ名 | 設定値 |
|---|---|
| autovacuum | off |
| autovacuum_freeze_max_age | 2億 |
| vacuum_freeze_min_age | 5000万 |
周回防止の自動VACUUMが誰にも阻害されることなく動作した場合は、およそ1億5000万分のXIDをFREEZEすることができるので、次回は回収された分の1億5000万のトランザクションが実行されるまでは周回防止の自動VACUUMは起動しないことになります。
改めて整理すると
周回防止の自動VACUUMは、autovacuum_freeze_max_ageの設定値毎に定期的に起動するのではなく、「消費済みXIDが autovacuum_freeze_max_ageを超えたら」時点で起動する。
なので、途中で手動によるFREEZEや、autovacuumが有効になっていて随時XIDの回収が行われていれば、周回防止の自動VACUUMの実行するタイミングはそれだけズレることになり、運用の仕方によっては発火させないことも可能。ということなんだと思います。
さいごに
あまりネタを考える余裕もなく、とりあえず整理しておきたいと思っていた周回防止のVACUUMについて書きました。
一度業務で調べていたのですが、ちゃんと整理できていなかったので、アドベントカレンダーの場を借りて整理させていただきました。もともとこのあたりは雰囲気でしか把握していませんでしたが、こうやって改めて見てみるとどういう動きになっているか勉強になります。
周回させないような運用設計を行い、来年も素敵なPostgreSQLライフをお過ごしください。