Claude Codeでレプリケーション遅延の原因を切り分ける — pg_stat_replicationの読み方
PostgreSQLのレプリカ遅延を、pg_stat_replicationのlag列と時刻の突き合わせで「送れていない・書けていない・適用できていない」のどこかに絞り、Claude Codeに読み取り専用で調べさせる手順です。
レプリカの遅延は、「遅れている」と分かった時点では何も絞れていません。PostgreSQLのレプリケーションでは、WALがプライマリから送られ、レプリカのディスクに書かれ、フラッシュされ、最後に適用されます。遅延はこの4つの段のどこかで溜まります。Claude Codeには、段を見分ける材料と、ラグが増えた時刻のジョブ履歴を並べて読ませるのが近道です。ここではPostgreSQLの物理レプリケーションを対象に、読み取り専用の接続で原因を絞る手順をまとめます。
遅延は4つの段のどこで溜まっているかを先に見る
プライマリでpg_stat_replicationを引くと、接続しているレプリカごとに1行返ります。この行には、WALの位置と遅延時間が段ごとに並んでいます。
| 段 | 位置の列 | 遅延時間の列 | 意味 |
|---|---|---|---|
| プライマリが送った | 位置の列sent_lsn | 遅延時間の列なし | 意味接続に送り出した最後のWAL位置 |
| レプリカが書いた | 位置の列write_lsn | 遅延時間の列write_lag | 意味OSに渡して書き込んだ位置 |
| レプリカがフラッシュした | 位置の列flush_lsn | 遅延時間の列flush_lag | 意味ディスクへ確実に書いた位置 |
| レプリカが適用した | 位置の列replay_lsn | 遅延時間の列replay_lag | 意味データベースに反映した位置 |
遅延時間の3列は、ローカルでWALをフラッシュしてから、各段の完了通知を受け取るまでの経過時間です。非同期のレプリカでは、replay_lagが「直近のトランザクションが参照できるようになるまでの遅れ」の目安になります。
読み方の要点は、隣り合う段の差です。
差が開いている段で原因の当たりが変わる
sent と write の差が大きい
WALがレプリカに届いていないか、書き込みが追いついていません。ネットワークの帯域、レプリカ側のI/O、受信プロセスの状態を疑います。
write と flush の差が大きい
レプリカのディスクへのフラッシュが遅い状態です。ストレージの性能や、同じディスクを使う別の負荷を見ます。
flush と replay の差が大きい
WALは届いているのに適用が進んでいません。レプリカ上の参照クエリとの競合や、適用そのものの重さが候補です。
この3つは並列の候補で、順番に潰す性質のものではありません。差が最も開いている段を1つ決めるだけで、その後に調べる範囲が半分以下になります。
注意点が1つあります。レプリカが追いつき、WALの動きも止まると、最後に測った遅延時間はしばらく表示され、そのあとNULLになります。つまりNULLは「遅れていない」と「測れていない」の両方で起こりえます。1回引いて終わりにせず、時系列で残す必要があります。
Claude Codeに読み取り専用でDBを見せる
Claude CodeからDBを引く方法は2つあります。psqlをBashツールで直接叩く方法と、MCPサーバーを介する方法です。MCPのドキュメントにはPostgreSQLの例があり、@bytebase/dbhubパッケージのDBHubを、--dsnに渡した接続文字列で使います。ドキュメントは、実行されるクエリがデータを変更できないよう、読み取り専用のユーザーで接続するよう書いています。
claude mcp add --transport stdio db -- npx -y @bytebase/dbhub \
--dsn "postgresql://monitor:pass@primary.example.com:5432/postgres"--より後ろは、Claude Codeではなくサーバーにそのまま渡されます。登録後は/mcpを実行し、dbの行にチェックが付いていれば起動できています。
遅延の調査に使うユーザーは、データを書けない監視用ロールで十分です。PostgreSQLの定義済みロールpg_monitorは、監視用のビューと関数を読めるロールで、pg_read_all_statsを含みます。pg_read_all_statsは、本来スーパーユーザーにしか見えないpg_stat_*ビューの内容も読めるようにします。
-- 例: 遅延調査用の監視ロール(パスワードは各自で管理する)
CREATE ROLE monitor LOGIN PASSWORD '...';
GRANT pg_monitor TO monitor;MCPで結果を受け取るときは出力サイズにも注意が要ります。ツールの出力が10,000トークンを超えると警告が出て、既定では25,000トークンで打ち切られます。上限はMAX_MCP_OUTPUT_TOKENSで変えられます。pg_stat_activityを全列・全行で引くとこの枠をすぐ使い切るので、列と行数を絞るようClaudeに指示しておきます。
長く走るクエリには、サーバーごとのtimeout(ミリ秒)を.mcp.jsonの該当エントリに足せます。1000未満の値は無視される点だけ覚えておけば足ります。
位置の差をバイトで見て、時系列に残す
プライマリで引くのは、段ごとの差をバイトで出す次のクエリです。pg_wal_lsn_diffは2つのWAL位置の差をバイトで返す関数で、ドキュメントでもレプリケーション遅延の算出に使えると説明されています。
SELECT application_name,
state,
pg_wal_lsn_diff(sent_lsn, write_lsn) AS send_to_write_bytes,
pg_wal_lsn_diff(write_lsn, flush_lsn) AS write_to_flush_bytes,
pg_wal_lsn_diff(flush_lsn, replay_lsn) AS flush_to_replay_bytes,
write_lag, flush_lag, replay_lag,
now() AS sampled_at
FROM pg_stat_replication;stateがstreamingなら、レプリカが追いついた後の通常のストリーミング中です。backupはベースバックアップ送信中、stoppingは停止処理中を表します。ここがstreamingでない行は、遅延以前に接続の状態を先に調べます。
1回の値は、その瞬間の写真にすぎません。遅延は波があるので、10秒おきに数十分から数時間ぶん、ファイルに残します。
# 10秒おきに1行ずつCSVへ追記する(停止はCtrl+C)
while true; do
psql -X -At -F, -h primary.example.com -U monitor postgres \
-c "SELECT now(), application_name, replay_lag FROM pg_stat_replication" \
>> lag_samples.csv
sleep 10
doneこの方法なら、Claudeに渡すのは数千行のCSVです。SQLを何度も往復させるより、トークンも接続数も節約できます。
レプリカ側でも1つだけ引いておきます。
SELECT pg_is_in_recovery(),
pg_last_xact_replay_timestamp(),
now() - pg_last_xact_replay_timestamp() AS replay_age;pg_last_xact_replay_timestampは、最後に適用したトランザクションがプライマリで確定した時刻を返します。注意点は、プライマリに書き込みがない時間帯は値が進まないことです。replay_ageが伸びていても、書き込みが止まっているだけの可能性があります。遅延の判断は、プライマリ側のreplay_lagとあわせて行います。
ラグが増えた時刻に何が走っていたかを突き合わせる
段が決まったら、次はその時刻に何があったかです。サンプルCSVでreplay_lagが増え始めた時刻を拾い、プライマリ側の次の情報と並べます。
- 長く開いたままのトランザクション。
pg_stat_activityのxact_startは、現在のトランザクションの開始時刻です idle in transactionのまま止まっているセッション。stateとstate_changeで分かります- 夜間バッチ、集計ジョブ、大量更新のデプロイ。ジョブスケジューラの開始時刻と終了時刻です
SELECT pid, usename, state, xact_start, state_change,
left(query, 120) AS query_head
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start
LIMIT 20;ここでClaude Codeの出番です。CSVとジョブ履歴を渡し、時刻の突き合わせを機械的にやらせます。たとえば次のような指示です。
lag_samples.csv の replay_lag が 5 秒を超えた区間を一覧にして。
区間ごとに、開始時刻の前後 5 分に開始した処理を batch_jobs.log から拾い、
区間と処理の重なりを表にして。因果は断定せず、重なりの有無だけ書いて。最後の一文が大事です。時刻が重なっていても、そのジョブが原因とは限りません。重なりは仮説の候補にとどめ、確かめる手段は次の節のとおり別に持ちます。
調査の約束事は、プロジェクトのCLAUDE.mdに書いておくと毎回の指示を省けます。
## DB調査のルール
- DBに接続するときは monitor ロール(読み取り専用)だけを使う
- INSERT / UPDATE / DELETE / DDL / pg_terminate_backend は実行しない
- pg_stat_activity は pid, state, xact_start, query の先頭120字だけ取る
- 遅延の原因は「重なり」と「確認済みの事実」を分けて書くデッドロックのログを読むときの渡し方は、PostgreSQLとMySQLのログから原因を調べる手順にあります。長く開いたトランザクションがロック待ちの形で見えている場合は、そちらも並べて読めます。
flushからreplayで開く差はレプリカの参照クエリを疑う
flush_to_replay_bytesだけが大きいときは、適用側で止まっています。ホットスタンバイでは、レプリカ上で動く参照クエリと、これから適用するWALが衝突することがあります。
衝突したときの挙動は設定で決まります。max_standby_streaming_delayの既定は30秒です。ストリーミングで届いたWALを適用するために、衝突する参照クエリを最大でこの時間まで待ち、超えるとクエリを取り消します。この値は個々のクエリの最大実行時間ではなく、受信済みのWALを適用し終えるまでの総待ち時間です。1本の長いクエリで遅れが出ると、後続の衝突クエリに残る猶予は短くなります。
つまり、この値を大きくすると取り消しは減る代わりに、遅延は大きくなります。ドキュメントは、HA用途ならこの値を小さく、さらにはゼロにすることもあり、意思決定支援クエリを流すレプリカなら数時間や-1(無期限に待つ)にもなりうると述べています。どちらが正しいかは、そのレプリカの役割で決まります。
レプリカで確かめる項目は次の3つです。
SHOW max_standby_streaming_delay;
SHOW hot_standby_feedback;
SHOW recovery_min_apply_delay;
SELECT datname, confl_snapshot, confl_lock, confl_bufferpin
FROM pg_stat_database_conflicts;pg_stat_database_conflictsは、リカバリとの衝突で取り消されたクエリを理由別に数えます。ここが増えているなら、参照クエリと適用の衝突が起きています。
recovery_min_apply_delayが0以外なら、遅延は仕様どおりです。この設定は適用を意図的に遅らせます。遅延の切り分けを始める前に、設定ミスや過去の運用で入っていないかを先に見ておくと、原因探しに時間をかけずに済みます。
hot_standby_feedbackは、レプリカの実行中クエリの情報をプライマリに返し、VACUUMが消す行を制限して取り消しを減らす設定です。既定はoffです。onにすると、プライマリのVACUUMによる不要行の掃除が遅れ、テーブルの肥大化を招く場合があります。取り消しを減らす代わりに、プライマリ側へ負担を移す設定だと理解しておきます。
症状から当たりをつける早見表
| 差が開く段 | 先に見るもの | 動かす前に確かめること |
|---|---|---|
| sentからwrite | 先に見るものネットワーク、レプリカのI/O | 動かす前に確かめることstateがstreamingか。接続が切れ直していないか |
| writeからflush | 先に見るものレプリカのストレージ | 動かす前に確かめること同じディスクを使う別の負荷がないか |
| flushからreplay | 先に見るもの参照クエリとの衝突、recovery_min_apply_delay | 動かす前に確かめることpg_stat_database_conflictsの増加、設定値 |
| 全段で同時に増える | 先に見るものプライマリのWAL量 | 動かす前に確かめること同時刻の大量更新、バッチの開始時刻 |
| どの差も小さいのに遅れて見える | 先に見るもの測り方 | 動かす前に確かめることレプリカ側のreplay_ageだけで判断していないか |
最後の行は、pg_last_xact_replay_timestampの落とし穴です。書き込みの少ない環境では、遅延ゼロでも経過時間が伸びます。
Claudeの診断を鵜呑みにしないための確認
Claudeが挙げた原因は、仮説として扱います。確かめる方法は原因ごとに決まっています。
- 参照クエリとの衝突: 疑わしいクエリの停止後に
replay_lagが縮むか、pg_stat_database_conflictsの該当列が増えているか - 大量更新バッチ: バッチの停止、または実行時間帯の移動後に、同じ時間帯のラグが消えるか
- ネットワーク: 同じ経路の別の指標で、帯域や再送が同時刻に悪化しているか
設定の変更は、Claudeに実行させず、提案をレビューしてから人が適用します。ここで扱った調査に必要なのは読み取りだけです。monitorロールに書き込み権限を与えない構成なら、誤操作はそもそも起きません。
DBのクエリ自体が遅い場合は、別の切り分けが要ります。実行計画から絞る手順はEXPLAIN ANALYZEの実行計画を読む手順が扱っています。マイグレーションがロックを取ってWALを増やす心配があるときは、ロックレベルで無停止かを確かめる手順で事前に洗えます。
まとめ
レプリカ遅延の調査は、まずpg_stat_replicationの隣り合う段の差で、遅れている段を1つに絞ることから始まります。その後に、ラグが増えた時刻とジョブ履歴を重ね、段ごとの原因候補を確かめます。Claude Codeは、10秒おきのCSVと履歴ログの突き合わせを読ませるのに向いています。接続はpg_monitorロールの読み取りに限り、原因の断定は、止めて縮むかを見る確認で裏づけます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。