エラーの内容
PostgreSQLスタンバイサーバー上でクエリを実行すると、1分ほどで次のエラーが発生します:
ERROR: canceling statement due to conflict with recovery
DETAIL: User query might have needed to see row versions that must be removed.
このエラーはスタンバイでのみ発生し、プライマリでは起きません。本番環境のリードレプリカで発生している場合、以下に原因と対処法を詳しく説明します。
根本原因
PostgreSQLのストリーミングレプリケーションは、プライマリからスタンバイへWAL(Write-Ahead Log)レコードを再生します。WALレコードの処理中に、スタンバイが古い行バージョン(デッドタプル)を削除する必要が生じることがありますが、その行をクエリがまだ読み取っている可能性があります。
PostgreSQLはここで選択を迫られます。WAL再生を遅らせてレプリケーションラグを増やすか、クエリをキャンセルするかです。デフォルトではクエリがキャンセルされます。キャンセルまでの猶予期間はmax_standby_streaming_delayで制御されており、デフォルト値はわずか30秒です。
主な原因として、長時間実行されるレポートクエリ、夜間の分析バッチ処理、スタンバイ上で数百万行をスキャンするOLAPクエリなどが挙げられます。
修正方法1:コンフリクト遅延の延長(最も手軽な方法)
コンフリクトが発生したクエリをキャンセルするまでの時間を延ばします。スタンバイサーバーのpostgresql.confを編集します:
# postgresql.conf on standby
max_standby_streaming_delay = 300s # default: 30s — bump to 5 minutes
max_standby_archive_delay = 300s # also raise if you use archive recovery
再起動なしで設定を反映します:
sudo -u postgres psql -c "SELECT pg_reload_conf();"
またはsystemd経由で:
sudo systemctl reload postgresql
-1に設定すると、クエリが完了するまでWAL再生を無期限に待機させることができます。数分程度のレプリケーションラグが許容できる場合は問題ありませんが、スタンバイをフェイルオーバー用途にも使っている場合は推奨しません。
修正方法2:hot_standby_feedbackの有効化(リード負荷の高いスタンバイに最適)
この方法は症状ではなく根本原因に対処します。hot_standby_feedback = onを設定すると、スタンバイはまだ使用中のトランザクションIDをプライマリに継続的に通知します。プライマリはスタンバイが処理を完了するまで、該当行のバキュームを保留します。
スタンバイサーバーのpostgresql.confを編集します:
# postgresql.conf on standby
hot_standby_feedback = on
設定を反映します:
sudo -u postgres psql -c "SELECT pg_reload_conf();"
**注意点:**スタンバイが参照している行のオートバキュームがプライマリで遅延されます。スタンバイで30分以上かかるクエリを実行している場合、プライマリのテーブルブロートが急速に増加する可能性があります。有効化後はプライマリのpg_stat_user_tables.n_dead_tupを監視し、デッドタプルの急増に注意してください。
修正方法3:アプリケーション側でのタイムアウト設定
トランザクションを開いたまま放置されている接続(クエリを実行後、アプリケーションロジックの待機中に停止した接続)が原因でコンフリクトが発生することがあります。重い処理を実行する前に制限を設定しておきます:
-- At the start of your session
SET idle_in_transaction_session_timeout = '5min';
SET statement_timeout = '10min';
-- Then run your query
SELECT * FROM large_analytics_table WHERE created_at > NOW() - INTERVAL '30 days';
これはWALコンフリクト自体を防ぐものではありませんが、放置されたトランザクションによる問題の悪化を防げます。
修正方法4:アプリケーションコードでのリトライ処理
スタンバイに直接クエリを送るアプリケーションでは、エラーをキャッチしてリトライする方法が有効です。コンフリクトによるキャンセルのほとんどは一時的なものであり、少し待てば解消します:
import psycopg2
from psycopg2 import OperationalError
import time
def query_with_retry(conn, sql, retries=3, delay=2):
for attempt in range(retries):
try:
cur = conn.cursor()
cur.execute(sql)
return cur.fetchall()
except OperationalError as e:
if 'canceling statement due to conflict with recovery' in str(e):
conn.rollback()
if attempt < retries - 1:
time.sleep(delay)
continue
raise
raise Exception("Max retries exceeded")
修正方法5:長時間クエリをスタンバイから切り離す
根本的な解決策はトポロジーの見直しです。重い分析クエリは、ホットスタンバイプールから切り離した専用レプリカにルーティングするか、オフピーク時間帯にプライマリに対して実行するようにスケジュールします。
数時間かかる集計処理などの本格的なレポートワークロードには、ロジカルレプリカや RedshiftやBigQueryのような専用分析データベースの利用を検討してください。ストリーミングスタンバイはそのようなクエリパターンを想定した設計ではありません。
確認手順
修正を適用したら、設定が正しく反映されているか確認します。
スタンバイで現在の設定を確認:
sudo -u postgres psql -c "SHOW max_standby_streaming_delay;"
sudo -u postgres psql -c "SHOW hot_standby_feedback;"
プライマリでレプリケーションラグを監視 — 特にhot_standby_feedbackを有効にした後や遅延を延長した後は注意が必要です。スタンバイが10分以上遅れないように確認します:
-- Run on PRIMARY
SELECT
application_name,
state,
sent_lsn,
write_lsn,
flush_lsn,
replay_lsn,
write_lag,
flush_lag,
replay_lag
FROM pg_stat_replication;
**失敗していたクエリを再実行します。**正常に完了すれば修正成功です。
スタンバイのログを確認して、コンフリクトメッセージが残っていないことを確認します:
sudo tail -f /var/log/postgresql/postgresql-*.log | grep -i conflict
どの修正方法を選ぶべきか
- 最も手軽でリスクが低い:
max_standby_streaming_delayを300〜600秒に延長する。5分の猶予期間でほとんどのクエリキャンセルは解消されます。 - リード負荷の高いスタンバイ:
hot_standby_feedbackを有効にし、プライマリのテーブルブロートを監視します。 - 最も効果的な組み合わせ:
hot_standby_feedback = onとmax_standby_streaming_delay = 300sを併用する。フィードバックシグナルが間に合わないエッジケースにも対応できます。 - **重い分析処理:**専用レプリカまたは別の分析DBを使用し、レプリケーションモデルの制約と戦わないようにします。
多くの環境では、スタンバイでhot_standby_feedback = onとmax_standby_streaming_delay = 300sを設定することで、このエラーを根本的に解消できます。

