PostgreSQL論理レプリケーションのWAL保持とスロット運用

目次
  1. 🎯 結論:WALの保持は「時間」ではなく「LSN」で決まる
  2. 🤔 なぜスロットがあるとWALが削除されないのか
  3. 🔍 切り分け手順:どのスロットが保持の下限を握っているか
  4. 🛠️ 対処:WAL肥大化を止める/レプリケーションを復旧させる
  5. 🧪 検証ログ:3スロットで restart_lsn とWAL保持量はこう動いた
  6. 🧐 よくある誤解を3つ整理する
  7. ❓ よくある質問
  8. 🛡️ 再発防止:監視項目と、放置したスロットが招くもの
このテーマのまとめPostgreSQL 運用・トラブル解決ガイド|「遅い・詰まる・肥大化」を仕組みから切り分けるこのテーマのまとめRDS・Aurora 運用・トラブル解決ガイド|仕組み・障害切り分け・性能監視の全体像
本記事にはプロモーション(広告)が含まれています。

論理レプリケーションを組んだ直後に「ディスク使用量が減らない」「WAL が溜まり続ける」という相談は、運用の現場でよく聞かれます。そして同時に、設計段階では「適用済みの WAL を一定期間だけ残したい」「過去の時点からレプリケーションを再開したい」「同じテーブルに複数のスロットを作っても大丈夫か」といった質問が出てきます。

この2種類の疑問は、実は同じ1つの仕組み、つまりレプリケーションスロットが restart_lsn を握っていることを理解すれば、まとめて答えが出ます。本記事ではコミュニティ版 PostgreSQL と Aurora PostgreSQL の論理レプリケーションを対象に、WAL 保持が何で決まるのか、いま誰が保持の下限を握っているかをどう特定するか、肥大化を止める操作とその副作用、そして放置したスロットが何を壊すのかを順に整理します。

🎯 結論:WALの保持は「時間」ではなく「LSN」で決まる

PostgreSQL のレプリケーションスロットによる WAL の保持は、時間軸ではなく LSN(ログシーケンス番号)で制御されています。

各レプリケーションスロットは restart_lsn という値を個別に持ちます。これは「このスロットのために保持しておく必要がある、最も古い WAL の位置」です。そして WAL セグメントを削除してよいかどうかの判定は、すべてのスロットの restart_lsn のうち最も古いものを基準に行われます。

ここからいくつかの帰結が導かれます。

  • あるスロットで既に適用(消費)済みの WAL であっても、別のスロットがまだその位置を必要としていれば、その WAL は削除されません。保持の下限は「一番遅れているスロット」が決めます。
  • 「適用済みの WAL を3日分だけ残す」といった時間ベースの保持機能は存在しません。時間で指定する設定項目がそもそも無いので、「○日分残す」という設計は LSN の進み方(=更新量)に翻訳して考える必要があります。
  • ⚠️ 逆に言えば、スロットが1つでも止まっていれば、更新量に比例して WAL は無限に増えます。ディスクフルは「いつか起きるかもしれない事象」ではなく「放置すれば起きる事象」です。

WAL が消えない問題の切り分けは、「どのスロットの restart_lsn が一番古いか」を特定する作業に還元できます。逆はできません。時間を指定して過去に戻る、という操作の余地はありません。

困っているときに最初に流す3つのクエリ

現に WAL が溜まっている状況なら、Publisher 側でこの3つを順に流します。

まず各スロットの状態です。

SELECT slot_name, restart_lsn, confirmed_flush_lsn, wal_status
FROM pg_replication_slots
ORDER BY slot_name;

💡 ここで見るのは3点です。restart_lsn が他より明らかに古いスロットがないか、confirmed_flush_lsn が現在の WAL 位置から大きく離れていないか、wal_status が reserved 以外になっていないか。

次に、データベース全体としての WAL 保持下限を出します。

SELECT min(restart_lsn) FROM pg_replication_slots;

この1点が、いまの WAL 削除の足かせです。この値が現在の WAL 書き込み位置からどれだけ離れているかが、そのまま「スロットのせいで余分に抱えている WAL 量」の目安になります。距離を見るなら、現在位置との差分を取ります。

SELECT slot_name, active, wal_status,
       pg_current_wal_lsn() AS current_lsn,
       restart_lsn,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS behind
FROM pg_replication_slots
ORDER BY pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) DESC;

3つめは wal_status の監視です。lost になっていないかを必ず確認します。lost は「そのスロットが必要としていた WAL がすでに削除された」状態で、後述するようにスロット自体が使えなくなっています。ディスク容量が急に回復していた場合、喜ぶ前にこれを見てください。容量が減ったのではなく、レプリケーションが壊れて WAL が捨てられた可能性があります。

今すぐWALを減らしたいときに取れる選択肢と、その副作用

取れる手は、実質的に次の3つです。

1つめは不要なスロットの削除です。最も確実で副作用が読みやすい手です。pg_drop_replication_slot() でスロットを落とせば、そのスロットが握っていた restart_lsn の制約が外れます。他のスロットには影響しません。ただし当然、そのスロットを使っていたレプリケーションは継続不能になります。「もう使っていない」ことの確認が前提です。

2つめは、遅れているスロットの消費を再開させることです。Subscriber 側が止まっているだけなら、そちらを復旧させれば confirmed_flush_lsn が進み、やがて restart_lsn も進みます。ただし後述のとおり restart_lsn の前進はチェックポイントのタイミングに依存するため、即座に WAL が消えるとは限りません。

3つめは max_slot_wal_keep_size で上限を設けることです。スロットのために保持する WAL 量に上限をかけるパラメータで、「今すぐ減らす」というより「二度とディスクフルまで行かせない」ための恒久対策です。副作用は明確で、上限を超えたスロットは必要な WAL を失い、wal_status が lost になって利用不能になる可能性があります。ディスクを守る代わりにレプリケーションを切り捨てる設定なので、どちらを守るのかを先に決めておく必要があります。

避けたいのは、切り分けをせずに pg_wal 配下のファイルを手で消すことです。PostgreSQL はそれを前提にしていません。

この記事のテーマをさらに学べる本

PostgreSQLの内部構造と照らし合わせて設計・運用計画を解説しており、WAL周りの理解を深められます。

[改訂3版]内部構造から学ぶPostgreSQL-設計・運用計画の鉄則
[改訂3版]内部構造から学ぶPostgreSQL-設計・運用計画の鉄則上原 一樹/勝俣 智成/佐伯 昌樹/原田 登志
楽天ブックスで詳しく見る Yahoo!ショッピングで見る

🤔 なぜスロットがあるとWALが削除されないのか

「なぜそうなるのか」が分かっていないと、しきい値の決め方も復旧手順の選択も勘に頼ることになります。

論理レプリケーションのスロットは、単に「どこまで送ったか」を覚えているブックマークではありません。再開可能性を保証するための予約です。サーバーは「このスロットがいつ戻ってきても、そこから続けてデコードできる」状態を維持しなければならず、そのために WAL を消せなくなります。

restart_lsn と confirmed_flush_lsn は何を指しているのか

pg_replication_slots にはよく似た2つの LSN が並んでいて、ここを混同すると切り分けを間違えます。

confirmed_flush_lsn は、コンシューマ(Subscriber や pg_recvlogical などのクライアント)が「ここまでは確かに受け取って永続化した」と確認応答してきた位置です。論理レプリケーションの進捗、すなわち「どこまで届いているか」を表す値はこちらです。レプリケーション遅延を見たいならこの値を使います。

restart_lsn は、「このスロットのデコードを再開するために、WAL をここから読み直す必要がある」位置です。保持の下限を決めているのはこちらです。

なぜ2つが別なのか。論理デコードは WAL レコードを1件ずつ独立に解釈できるわけではなく、トランザクション単位で組み立てる必要があります。あるトランザクションのコミットを出力するには、そのトランザクションの最初の変更レコードまで遡って読めなければなりません。さらに、デコードを開始するには「どのトランザクションが実行中だったか」の情報を含む一貫したスナップショットが必要です。この情報は WAL に定期的に記録される実行中トランザクションの情報(running xacts)を手がかりに構築されます。

結果として、restart_lsn は「最も古い実行中トランザクションの開始位置」あたりに引っかかり、confirmed_flush_lsn よりかなり後ろに留まります。そして restart_lsn が前に進むのは、チェックポイントで実行中トランザクション情報が WAL に書かれた後、そのスロットが再度消費されたときです。これは実測でもはっきり出るので、後段の検証ログで確認します🔍

運用上の判断としては、次のように使い分けます。

  • 「レプリケーションが遅れているか」→ confirmed_flush_lsn と現在の WAL 位置の差
  • 「WAL が消せない原因は誰か」→ restart_lsn、とくに min(restart_lsn)
  • 「システムカタログの vacuum が抑止されていないか」→ catalog_xmin

catalog_xmin は、そのスロットのデコードのために保持が必要なシステムカタログの行バージョンを示す値です。論理デコードは過去の WAL を「当時のテーブル定義」で解釈しなければならないため、古いカタログのタプルを回収されると困ります。だからスロットはカタログの古いバージョンを守ります。この副作用は最後のセクションで詳しく扱います。

WALは1系列しかない:スロットごとにコピーは作られない

💡 ここが設計時に最も誤解されるポイントです。

WAL は物理的に単一のストリームとして保持されます。スロットを作ったからといって、そのスロット専用の WAL のコピーが作られるわけではありません。複数のスロットは、同じ1本の WAL をそれぞれ別の位置から読んでいるだけです。

したがって「同じテーブルに対して複数のスロットを作れるか」という問いの答えは「作れる」ですし、「スロットを3つ作ると WAL 保持量が3倍になるか」の答えは「ならない」です。保持される範囲は min(restart_lsn) から現在位置までの1区間で、スロットの本数には比例しません。

ただしこれは「スロットを増やしてもコストがゼロ」という意味ではありません。増えるのはリスクです。スロットが増えるほど「一番遅れている1本」になる候補が増え、そのうち1本でも止まれば全体の保持下限が固定されます。3つのスロットは容量を3倍にしませんが、ディスクフルの引き金を3つに増やします。加えて、論理デコードそのものの CPU とメモリの消費はスロットごとに発生します。

「時間を指定して過去から再開」ができない理由

「昨日の 10:00 時点からレプリケーションをやり直したい」という要望は設計段階でよく出ますが、論理レプリケーションにその機能はありません。

理由は上で見た仕組みから明らかです。スロットが保証しているのは「restart_lsn から先の再開」だけで、それより前のデータはサーバーにとって不要であり、削除対象です。confirmed_flush_lsn より前のデータは、そのスロットから見ればすでに配信完了として扱われており、取得しようとしても「is not available anymore」という扱いになります。任意の時点を指定して巻き戻すインターフェースも、そのための保持機構も用意されていません。

WAL の保持は LSN 基準であって時刻基準ではない、という最初の結論がここに効いてきます。「時刻」は WAL の削除判定に一切登場しないため、時刻で指定できる復旧点も存在しないのです。

⚠️ 過去の状態から作り直したいなら、論理レプリケーションのスロットを巻き戻すのではなく、別の仕組みを使います。すなわちバックアップからのポイントインタイムリカバリ(PITR)でその時点のデータを復元するか、あるいは後述するようにサブスクリプションを再作成して初期コピーからやり直すかです。この線引きを設計時に握っておかないと、障害時に「巻き戻せる前提」で組んだ手順が使えません。

🔍 切り分け手順:どのスロットが保持の下限を握っているか

ここからは実際の切り分けです。順番に意味があります。

wal_status が reserved / extended / unreserved / lost のどれかを見る

pg_replication_slots.wal_status は、そのスロットが要求している WAL がまだ手元にあるかを4段階で示します。

reserved なら必要な WAL は max_wal_size の範囲内に収まっており、正常な状態です。extended は max_wal_size を超えてスロットのために追加で WAL を抱えている状態で、「スロットが原因で WAL が増え始めている」サインです。ここで気付ければ理想的です。unreserved は、必要な WAL はまだ残っているものの上限(max_slot_wal_keep_size)を超えているため、次のチェックポイントで削除される見込みという状態で、猶予はほぼありません。lost は必要な WAL が既に削除された状態で、このスロットは使えません。

💡 判断基準はシンプルです。extended が出たら容量の増加要因として調査に入る、unreserved が出たら失う前提で動く、lost が出たらそのスロットは復旧不能なので再構築を検討する。lost からスロットを「戻す」方法はありません。

なお max_slot_wal_keep_size を設定していない(既定の無制限の)環境では、unreserved や lost には遷移せず、代わりに extended のまま WAL が増え続けてディスクを食い潰します。「lost を監視していれば安全」ではなく、「lost が出ない設定なら容量そのものを監視する」という対応関係になります。

安全余裕を数値で見たい場合は safe_wal_size 列(該当バージョンで利用可能な場合)も併せて確認します。あと何バイト書き込まれるとこのスロットが危ういか、の目安になります。

min(restart_lsn) で保持下限を出し、原因スロットを1つに絞る

wal_status で異常が見えたら、次は原因の特定です。

SELECT min(restart_lsn) FROM pg_replication_slots;

この値を持っているスロットが、WAL 削除を止めている当事者です。最初のクエリの結果と突き合わせて、restart_lsn がこの min と一致するスロットを探します。

ここで大事なのは、原因が基本的に1本に絞られるということです。10本のスロットのうち9本が健全でも、1本が止まっていれば min(restart_lsn) はその1本の値になり、WAL は消えません。逆に言えば、その1本を何とかすれば状況は動きます。「スロットが多いから WAL が多い」ではなく「最も古い1本が原因」という視点で見てください。

絞り込んだら、そのスロットについて次を確認します。

active が false なら、コンシューマが接続していません。Subscriber が停止しているか、ネットワークが切れているか、あるいはそもそも使われていない放置スロットです。active が true なのに confirmed_flush_lsn が進んでいないなら、接続はあるが適用が進んでいない状態です。

スロットは生きているのに進まないときに疑うポイント

active = true で confirmed_flush_lsn が動かないケースは、Publisher 側だけ見ていても答えが出ません。疑う順に見ていきます。

まず Subscriber 側の適用エラーです。論理レプリケーションの適用(apply)は、競合が起きるとその位置で停止し、リトライを繰り返します。典型は一意制約違反、対象テーブルの不存在、列の型不一致です。Subscriber のログに競合の内容が出るので、そこを見ます。適用が止まっている間 confirmed_flush_lsn は進まず、Publisher 側の WAL は増え続けます。Subscriber 側では pg_stat_subscription で適用ワーカーの状態と受信位置を確認します。

次に長時間実行トランザクションです。論理デコードはトランザクション単位で出力するため、巨大なトランザクションがコミットされるまで下流には何も流れません。さらに restart_lsn は最も古い実行中トランザクションに引きずられるので、Publisher 側に長時間開いたままのトランザクションがあると、それだけで保持下限が固定されます。pg_stat_activity で state = 'idle in transaction' のセッションと xact_start の古いものを探します。これは論理レプリケーション固有の話ではなく、restart_lsn の意味から直接出てくる帰結です。

初期同期の途中という線もあります。サブスクリプション作成直後の初期データコピー中は、そのテーブル用の一時的なスロットも含めて進捗が止まって見えることがあります。大きなテーブルのコピー中は、その間の更新分の WAL が保持されます。

最後に適用のボトルネックです。Subscriber 側の書き込み性能が Publisher の更新量に追いつかない場合、遅延は一方的に開きます。confirmed_flush_lsn が「進んではいるが差が縮まらない」ならこちらです。止まっているのか遅れているのかは、同じクエリを時間差で2回流して差分の変化を見れば判別できます。

🛠️ 対処:WAL肥大化を止める/レプリケーションを復旧させる

使わないスロットを pg_drop_replication_slot() で落とす

原因スロットが「もう使っていないもの」だと判明したなら、削除が最短です✨

SELECT pg_drop_replication_slot('<スロット名>');

前提と注意点を整理します。

  • 削除しても他のスロットには影響しません。WAL は共有の1系列なので、1本落とせばその分だけ保持下限が前に動き、残りのスロットは自分の restart_lsn に基づいて引き続き保護されます。
  • active = true(コンシューマが接続中)のスロットは削除できません。まず Subscriber 側を切り離すか、該当の接続を終了させる必要があります。
  • ⚠️ 削除したスロットは復元できません。そのスロット経由のレプリケーションを再開したい場合は、初期コピーからのやり直しになります。

検証やテストで作ったスロット、廃止した Subscriber の残骸、失敗したサブスクリプション作成の残りかす。こうした「誰も使っていないが生きているスロット」は、WAL を抱えるだけでなく後述するカタログ肥大化の原因にもなります。使わないと判断した時点で速やかに落とすのが原則です。

max_slot_wal_keep_size を設定する前に決めておくこと

max_slot_wal_keep_size は、スロットのために保持する WAL 量の上限を決めるパラメータです。これを設定すれば、スロットが止まってもディスクフルまでは行きません。

ただし、設定する前に方針を決めておく必要があります。このパラメータは「レプリケーションを犠牲にしてデータベースを守る」ためのものです。上限を超えると必要な WAL が削除され、スロットの wal_status は unreserved を経て lost になり、そのスロットは利用不能になります。復旧手段は再構築だけです。

そこで、設定値を決める前に次を握っておきます。

  1. どちらを優先するか。Publisher が業務の本番系で、停止が許されないなら上限を設けるべきです。逆に、レプリケーション先が重要な監査・分析系で欠損が許されないなら、上限を設けずに容量アラートで人が介入する設計もあり得ます。両方は守れません。
  2. 想定する許容ダウンタイムを WAL 量に換算する。時間ベースの保持機能はないので、「Subscriber が N 時間止まっても復旧できる」という要件は「その N 時間で生成される WAL 量」に翻訳して上限値を決める必要があります。ピーク時の WAL 生成量(pg_current_wal_lsn() の差分やチェックポイント頻度から見積もる)が前提データになります。バッチや夜間の一括更新がある環境では、平常時の平均で決めると足りません。
  3. lost を検知して再構築に入る運用を先に作る。上限を設けるなら、lost になったことに気付けなければ意味がありません。気付くのが遅れると、レプリカが静かに古くなり続けます。監視とセットで導入します。

なお、このパラメータの既定値や単位、有効化に再起動が必要かどうかは環境やバージョンで異なるため、適用前に使用中のバージョンのドキュメントとパラメータグループの設定を確認してください。マネージドサービスではパラメータグループ経由での変更になります。

この記事のテーマをさらに学べる本

内部構造からレプリケーション、バックアップ、モニタリングまで運用視点で体系的に扱っています。

PostgreSQL実践入門──アーキテクチャ、運用監視、性能改善
PostgreSQL実践入門──アーキテクチャ、運用監視、性能改善堀口 恭太郎/細谷 柚子/渡 佑也/山田 達朗/白石 裕輝/須賀 啓敏
楽天ブックスで詳しく見る Yahoo!ショッピングで見る

再同期はサブスクリプション再作成(copy_data=true の負荷を見積もる)

スロットを失った、あるいは差分をたどれなくなった場合の復旧は、サブスクリプションの再作成です。

CREATE SUBSCRIPTION は既定で copy_data = true です。つまり対象テーブルの既存データをすべてコピーしてから、その続きとしてレプリケーションが始まります。「差分だけ追いつく」という選択肢は、必要な WAL が消えている以上、取れません。

再作成を計画する際に見積もるべき負荷は次の通りです。

Publisher 側の読み取り負荷。初期コピーは対象テーブルの全件読み取りです。大きなテーブルではその間バッファキャッシュが押し流され、通常業務のクエリの実行計画は変わらなくても物理読み取りが増えて体感が悪化することがあります。

初期コピー中の WAL 保持。コピーが終わるまでの間に発生した更新は、あとで適用するために保持されます。コピーが長引けばその分 WAL が積み上がります。容量に余裕のない状態で再作成すると、復旧作業そのものがディスクを圧迫します。

Subscriber 側の書き込み負荷。初期コピー後にインデックスや制約が効いた状態で大量投入が走ります。

テーブル単位の切り分け。全テーブルを一度に再同期するのではなく、ALTER SUBSCRIPTION ... REFRESH PUBLICATION や対象テーブルを分けた publication を使って、影響を分割できる場合があります。実行可能な操作はバージョンによって差があるため、手元のバージョンで確認してください。

💡 再作成は軽い操作ではありません。だからこそ、lost に至らせないための監視とスロット整理のほうが優先度が高くなります。

🧪 検証ログ:3スロットで restart_lsn とWAL保持量はこう動いた

ここまでの説明を、実際の挙動で裏付けます。以下はコミュニティ版 PostgreSQL 16 の Docker 環境で実施したもので、Aurora PostgreSQL そのものではありません。wal_level = logical、出力プラグインは test_decoding を使用しています。Aurora では基盤のストレージ構成やパラメータの既定値が異なるため、数値をそのまま当てはめることはできません。確認したいのは数値そのものではなく、restart_lsn と confirmed_flush_lsn の動き方の性質です。

手順は次の通りです。同一テーブルに対して3つの論理レプリケーションスロット(slot_a、slot_b、slot_c)を作成し、40万行を投入したうえで、slot_a だけ変更を消費して LSN を進め、slot_b と slot_c は据え置きにしました。

slot_a だけ消費したときの各スロットの位置

 slot_name | restart_lsn | confirmed_flush_lsn | wal_status | catalog_xmin
-----------+-------------+---------------------+------------+--------------
 slot_a    | 0/1519B48   | 0/5F02190           | reserved   |          732
 slot_b    | 0/1519B10   | 0/1519B48           | reserved   |          732
 slot_c    | 0/1519B48   | 0/1519B80           | reserved   |          732

 retention_floor(min(restart_lsn)): 0/1519B10    WAL: 6ファイル / 96MB

読み取れることを整理します。

各スロットの位置は独立して管理されています。slot_a の confirmed_flush_lsn は 0/5F02190 まで進んでいますが、slot_b は 0/1519B48、slot_c は 0/1519B80 に留まっています。1つのスロットを消費しても、他のスロットの進捗には影響しません。

✅ WAL 保持の下限は最も古い restart_lsn で決まります。この時点の min(restart_lsn) は slot_b の 0/1519B10 です。slot_a は大量の変更を消費済みですが、slot_b が古い位置を握っているため、WAL はそこから先が保持され続けます。「一番遅れている1本が全体を決める」という原則が、そのまま数字に出ています。

そして保持量は 6ファイル / 96MB です。スロットは3本ありますが、WAL が3系列に分かれて 3倍抱えられているわけではありません。0/1519B10 から現在位置までの1区間が保持されているだけです。

confirmed_flush_lsn が大きく進んでも restart_lsn が動かない理由

この検証で最も示唆的なのは slot_a の行です。confirmed_flush_lsn は 0/1519B48 付近から 0/5F02190 へと大きく前進しているのに、restart_lsn は 0/1519B48 とほとんど動いていません。

つまり slot_a は「40万行分の変更をすべて受け取り済み」であるにもかかわらず、WAL の保持要求としては依然としてほぼ最初の位置を指しています。もしこのとき slot_b と slot_c を削除して「これで WAL が消える」と期待しても、slot_a の restart_lsn が動いていないので保持下限はほとんど変わりません。

理由は前述の通りで、論理スロットの restart_lsn は、チェックポイント(実行中トランザクションの情報の記録)の後に再度消費したときに進む性質を持っています。デコードの再開点はスナップショット構築に必要な情報の位置に依存するため、データを受け取った位置とは別のタイミングで更新されるのです。

運用上の含意は明確です。「Subscriber が追いついたのに WAL が減らない」は、多くの場合バグでも設定ミスでもありません。消費の進捗(confirmed_flush_lsn)と保持の下限(restart_lsn)は別物であり、後者はチェックポイントを挟んでから遅れて追随します。したがって、WAL 削減の効果を確認するときは、遅延解消の直後ではなく、チェックポイントを1〜2回またいだあとに min(restart_lsn) を再測定するのが正しい手順になります。

スロットを進めても古いWALが即座に消えなかった話

続けて slot_b と slot_c を削除し、チェックポイントを挟んでから slot_a を再度消費しました。

 slot_name | restart_lsn | confirmed_flush_lsn
-----------+-------------+---------------------
 slot_a    | 0/6000028   | 0/6000270

✅ restart_lsn が 0/1519B48 から 0/6000028 へ前進しました。チェックポイント後の再消費で restart_lsn が進む、という先ほどの性質が確認できます。

ただし、ここで注目すべき挙動があります。今回の検証では、restart_lsn が進んだ直後のチェックポイントでは古い WAL(セグメント 01〜05)がすぐには消えませんでした。再起動後のチェックポイントで、restart_lsn を含むセグメント(06)より前が削除されました😇

WAL セグメントの削除はチェックポイント時の判定で行われ、その判定にはスロットの保持要求以外の要素(min_wal_size による再利用のための保持、リサイクル対象としての再利用など)も絡みます。保持の制約が外れた瞬間にファイルが消えるわけではありません。

ここから導ける運用上の判断基準は2つです。

  1. 対処の効果判定を「即時のディスク使用量」で行わない。スロットを削除したり進めたりしたあと、数分でファイル数が変わらないことは十分あり得ます。効果は min(restart_lsn) が前進したかどうかで判定し、ファイルの削減はそのあとに追随するものとして扱います。
  2. 容量のしきい値には余裕を持たせる。スロットを適切に運用していても、削除は即時ではありません。「保持下限から計算した理論値」ぴったりで容量を設計すると、削除の遅れ分でアラートが鳴ります。

なお、この削除タイミングの挙動はバージョンや設定(max_wal_size、min_wal_size、チェックポイント関連パラメータ)、およびマネージドサービスの実装によって差が出る部分です。上記は特定環境での観測であり、普遍的な仕様として扱わないでください。

🧐 よくある誤解を3つ整理する

設計レビューの場でよく出てくる誤解を、ここまでの内容と対応させて整理します。

誤解1:適用済みWALを「○日分だけ」残す設定がある

ありません。PostgreSQL / Aurora PostgreSQL に、WAL を時間範囲で保持し続ける機能は存在しません。保持は LSN 基準であり、スロットの restart_lsn と、wal_keep_size や max_slot_wal_keep_size といったサイズ基準のパラメータで制御されます。時間という軸は削除判定に登場しません。

この誤解が厄介なのは、「7日分残す設定にしてあるから大丈夫」という前提で設計が進んでしまうことです。実際には、更新量が増えれば同じサイズで保持できる時間は短くなり、更新が止まれば長くなります。要件が時間で与えられている場合は、必ず WAL 生成量に換算して、しかもピーク時を基準に見積もる必要があります😇

なお「過去の任意時点からレプリケーションを再開する」という機能も同様に存在しません。confirmed_flush_lsn より前のデータは取得できません。時点指定の復旧が要件なら、それは論理レプリケーションではなくバックアップ/PITR の領分です。

誤解2:スロットを3つ作るとWAL保持量が3倍になる

なりません。WAL は物理的に1系列で、スロットごとにコピーは作られません。保持される範囲は min(restart_lsn) から現在位置までの1区間で決まり、スロット数には比例しません。検証ログでも、3スロットで保持されていたのは 6ファイル / 96MB という単一区間でした。

⚠️ ただし、この事実を「スロットは何本作っても安全」と読み替えるのは誤りです。正しい理解はこうです。容量はスロット数に比例せず、最も古い1本で決まります。一方でリスクはスロット数に比例して増えます(止まる候補が増えるため)。そしてデコード処理のコストはスロットごとに発生します。

同じテーブルに複数のスロットを向けること自体は可能です。しかし本数を増やすほど、監視対象と「放置される可能性のあるスロット」が増えます。必要性を説明できないスロットは作らない、というのが現実的な方針です。

誤解3:論理スロットがユーザーテーブルのvacuumを止めている

これは半分正しく、半分間違っています。

論理レプリケーションスロットが autovacuum によるクリーンアップを妨げる対象は、システムカタログ(内部のカタログテーブル)です。通常のユーザーテーブルの dead tuple の回収が、論理スロットによって直接ブロックされるわけではありません。

なぜカタログなのかは catalog_xmin の意味から説明できます。論理デコードは過去の WAL レコードを解釈するために、「そのレコードが書かれた時点のテーブル定義」を知る必要があります。そのため、スロットはデコードに必要な範囲のカタログの行バージョンを保護します。検証ログの3スロットで catalog_xmin がいずれも 732 と同じ値になっているのは、この保護が働いていることの表れです。

したがって、ユーザーテーブルのテーブル膨張に悩んでいるときの容疑者は、論理スロットよりもまず長時間トランザクション、hot_standby_feedback 経由のスタンバイの影響、prepared transaction の放置、autovacuum の設定などです。切り分けを間違えると、消す必要のないスロットを落として無関係な問題を残すことになります。

逆に、pg_class、pg_attribute といったカタログが肥大化している、あるいはカタログを参照する処理が重い、という症状のときには論理スロットを疑う価値があります。

OSS-DB Silver 対策テキスト(OSS教科書)

PostgreSQLの仕組みを体系的に学ぶなら、OSS-DB Silverの公式対応テキストが近道です。資格を取らなくても基礎固めに役立ちます。

OSS教科書 OSS-DB Silver Ver.3.0対応 (EXAMPRESS) [ 福岡 博 ]

OSS教科書 OSS-DB Silver Ver.3.0対応 (EXAMPRESS) [ 福岡 博 ]

❓ よくある質問

適用済みのWALを「3日分だけ残す」ような時間ベースの保持設定はできる?

できません。PostgreSQL / Aurora PostgreSQL のWAL保持はLSN基準で、スロットのrestart_lsnとwal_keep_size・max_slot_wal_keep_sizeといったサイズ基準のパラメータで決まります。時間という軸は削除判定に登場しないため、「N時間止まっても復旧できる」という要件は、その間に生成されるWAL量に換算して上限値を決める必要があります。更新量が増えれば同じサイズで保持できる時間は短くなるので、見積もりはピーク時を基準にします。

同じテーブルに対して複数のレプリケーションスロットを作っても大丈夫?

作ること自体は可能で、WAL保持量がスロット数に比例して増えることもありません。WALは物理的に1系列で、保持されるのはmin(restart_lsn)から現在位置までの1区間だけです。ただし増えるのはリスクで、スロットが多いほど「一番遅れている1本」になる候補が増え、1本止まれば全体の保持下限が固定されます。論理デコードのCPU・メモリ消費もスロットごとに発生するため、必要性を説明できないスロットは作らない方針が現実的です。

Subscriberが追いついたのにWALが減らないのは設定ミス?

多くの場合はバグでも設定ミスでもありません。消費の進捗を表すconfirmed_flush_lsnと、WAL保持の下限を決めるrestart_lsnは別物で、論理スロットのrestart_lsnはチェックポイントで実行中トランザクション情報が記録された後、再度消費されたときに進みます。そのためWAL削減の効果を確認するときは、遅延解消の直後ではなくチェックポイントを1〜2回またいだあとにmin(restart_lsn)を再測定します。ファイル自体の削除はさらに遅れて追随することがあるため、容量のしきい値には余裕を持たせてください。

wal_statusがlostになったスロットは元に戻せる?

戻せません。lostは「そのスロットが必要としていた WAL がすでに削除された」状態で、スロット自体が利用不能になり、復旧手段は再構築だけです。再構築はCREATE SUBSCRIPTIONが既定でcopy_data = trueのため全件コピーからのやり直しになり、Publisher側の読み取り負荷や初期コピー中のWAL保持が発生します。ディスク使用量が急に回復したように見えたときは、容量が減ったのではなくレプリケーションが壊れてWALが捨てられた可能性を先に確認します。

🛡️ 再発防止:監視項目と、放置したスロットが招くもの

pg_replication_slots に対する定期チェックとしきい値の決め方

監視すべきは次の4項目です。

wal_status は、reserved 以外が出たらアラートの対象です。段階を踏むなら extended で警告、unreserved と lost で重大として分けます。lost はすでに手遅れの状態なので、それより手前で拾える extended を見逃さないことが実質的な防御線になります。

active は、false のスロットが一定時間続いていないかを見ます。想定外の非アクティブは、Subscriber の障害か放置スロットのどちらかです。active_pid が空のまま変わらないものは棚卸しの対象です。

min(restart_lsn) と現在位置の差分が、「スロットに起因する余分な WAL 量」です。pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) をバイト数で取り、スロット別にグラフ化しておくと、どの時点から特定のスロットが止まったかが後から追えます。

confirmed_flush_lsn の差分はレプリケーション遅延の指標です。restart_lsn の差分とは別に取ります。両方を持っていれば、「遅れている」のか「保持だけ引っかかっている」のかを区別できます。

しきい値の決め方は、逆算で考えます。

  • まず、pg_wal に割り当てられたストレージのうち、スロット起因で使ってよい量を決める
  • そこから「この量に達したら人が介入する」警告値を設定する。検証ログで見たとおり WAL の削除は即時ではないので、警告値は理論上の限界からある程度の余裕を引いた位置に置く
  • max_slot_wal_keep_size を設定するなら、警告値はその上限より確実に低い位置に置く。上限に到達する前に気付けなければ、パラメータは「静かにレプリケーションを切る仕掛け」として機能してしまう

加えて、restart_lsn の差分が「増え続けている」ことの検知も有効です。絶対値のしきい値だけだと、更新量が少ない時間帯に閾値を割らないまま停止が長時間続くことがあります。

非アクティブスロットがシステムカタログの肥大化を招く仕組み

放置された(active = false のまま消費されない)論理レプリケーションスロットは、2つの害を同時に与えます。

1つめは WAL の保持です。これはここまで見てきた通りで、restart_lsn が固定されたままなので、更新が続く限り WAL は増え続けます。

2つめが見落とされがちなもので、システムカタログの autovacuum によるクリーンアップを妨げることです。スロットは catalog_xmin を通じて古いカタログの行バージョンを保護します。スロットが進まなければ catalog_xmin も進まず、その分のカタログのタプルは回収できません。

結果として起きるのは、カタログテーブルの肥大化と、それに伴うパフォーマンス低下です。カタログは実行計画の作成やリレーションの解決など、あらゆるクエリの裏側で参照されます。ここが膨れると、個別のクエリチューニングでは説明のつかない、全体的な劣化として現れます。DDL が多い環境や一時テーブルを多用する環境では、カタログの更新頻度が高いため影響が出やすくなります。

この観点から、運用ルールとして決めておきたいのは2点です。スロットの棚卸しを定期的に行うこと。用途の説明できないスロット、廃止済みの Subscriber に対応するスロット、検証で作ったままのスロットを一覧化し、pg_drop_replication_slot() で削除します。削除しても他のスロットには影響しません。もう1点は、スロットを作った時点で誰が使うものかを記録することです。名前だけで用途が分かる命名規則にしておくと、棚卸しのコストが下がります。「消してよいか分からないので残す」が肥大化の最大の原因です😇

max_replication_slots とAurora・バージョン差で確認しておきたい点

最後に、環境差として確認しておくべき点です。

スロット数の上限は max_replication_slots パラメータで制御されます。これに達すると新しいスロットを作成できず、サブスクリプションの作成や初期同期が失敗します。放置スロットが枠を埋めていて、いざ必要なときに作れない、というのは避けたい事態です。棚卸しはこの観点でも意味があります。変更には再起動が必要なパラメータであり、マネージドサービスではパラメータグループ経由の変更とメンテナンスのタイミングを考える必要があります。

Aurora PostgreSQL 特有の確認事項もあります。論理レプリケーションの有効化方法(wal_level を直接設定するのではなく、専用のパラメータで有効化する形式になっている点)、パラメータの変更がクラスターパラメータグループかDBパラメータグループのどちらに属するか、max_slot_wal_keep_size に相当する設定が使用中のバージョンで変更可能かどうか。これらは Aurora のバージョンによって差があるため、実際のパラメータグループの画面と Aurora のドキュメントで確認してください。本記事の検証はコミュニティ版 PostgreSQL 16 の Docker 環境で行ったものであり、Aurora の挙動をそのまま示すものではありません。

バージョン差も見ておきます。論理レプリケーション周辺は更新が活発な領域です。wal_status や safe_wal_size の利用可否、max_slot_wal_keep_size の導入バージョン、論理デコードのメモリ管理や大きなトランザクションのストリーミング、フェイルオーバー時のスロット引き継ぎに関する挙動などは、メジャーバージョンによって差があります。移行やアップグレードの前には、対象バージョンのリリースノートで論理レプリケーション関連の変更を確認するのが安全です。


💡 押さえるべきは次の4点です。WAL 保持は時間ではなく LSN で決まり、min(restart_lsn) 1点が全体の下限を握る。confirmed_flush_lsn と restart_lsn は別物で、後者はチェックポイントを挟んで遅れて進む。スロット数に比例して容量が増えるわけではないが、リスクは増える。そして放置スロットは WAL とシステムカタログの両方を膨らませる。

なお、パラメータの既定値や利用可能な列、Aurora 側の仕様は変更されることがあります。設定変更や運用手順に落とす前に、必ず使用中のバージョンの PostgreSQL 公式ドキュメントおよび Amazon Aurora のドキュメントで最新の内容を確認してください。