PostgreSQLデッドロック調査プレイブック:再現・可視化・恒久対策までの実践手順
本番運用で厄介なのは、エラーが「たまに」しか出ない障害です。PostgreSQL のデッドロックはその代表で、発生頻度は低くてもビジネス影響が大きいことが多いです。決済や在庫更新で発生すると、リトライが雪だるま式に増え、アプリ全体の遅延を引き起こします。
本記事では、デッドロック発生時に現場でそのまま使える手順を、初動対応・再現・恒久対策の順で整理します。
1. まず理解すべき前提
デッドロックは「どちらかが悪い」ではなく、ロック順序が循環したときに必ず起きる現象です。PostgreSQL は循環を検出すると、どちらか一方のトランザクションを強制中断します。
典型的な症状:
ERROR: deadlock detected- API の一部がランダムに 500 を返す
- リトライ実装により DB 負荷が上振れ
ここで重要なのは、単純なタイムアウトと混同しないことです。タイムアウトは待ち時間超過、デッドロックは循環待ちです。対策が違います。
2. 初動でやること(5〜15分)
2-1. エラーログの採取
まず、DB 側ログに詳細を出す設定があるか確認します。
|
|
推奨設定(本番):
log_lock_waits = on
deadlock_timeout = '1s'
log_min_error_statement = error
deadlock_timeout を短めにすることで、待ちが長引いたケースの追跡がしやすくなります。
2-2. 現在のロック状況を確認
|
|
見るべき点は「長く生きているトランザクション」と「granted = false が連鎖している箇所」です。
3. 再現手順を作る(原因特定の最短ルート)
デッドロックは再現しないと直せません。以下のような単純ケースをまず作ります。
セッション A
|
|
セッション B
|
|
更新順序が逆なので、容易に循環が起きます。
この再現が取れたら、アプリコード上でも「同じテーブルを複数行更新する順序」が一定かどうかを調べます。
4. 原因の8割は「更新順序の不一致」
多くのプロダクトで見つかるのは次のパターンです。
- バッチ処理は
id ASCで更新 - API リクエストは受信順で更新
- 並行処理時に順序が逆転
この場合、解決策は明確で、全経路でロック取得順序を統一します。
例(Node.js / TypeScript):
|
|
FOR UPDATE を先に順序付きで取得することで、アプリ層の揺らぎを DB で吸収できます。
5. 実装レベルの対策パターン
5-1. トランザクションを短くする
デッドロックは「ロック保持時間」が長いほど発生しやすくなります。トランザクション内で外部 API 呼び出しをしていないか確認してください。これは最優先で排除します。
5-2. 失敗時のリトライを制御する
無制限リトライは障害増幅器です。指数バックオフ + 上限回数で制御します。
|
|
5-3. 楽観ロックの導入
更新競合が多い領域では version カラムを使った楽観ロックが有効です。衝突時にアプリで再計算できます。
6. 監視・運用面の改善
恒久対策を完了させるには、再発を検知できる状態を作る必要があります。
推奨メトリクス:
- deadlock 発生回数 / 分
- lock wait 時間 p95
- 失敗リトライ回数
- 長時間トランザクション件数
SQL 例(長時間 tx 検知):
|
|
この結果を可視化し、閾値超過で通知するだけでも再発時の初動が速くなります。
7. 障害対応時の意思決定フレーム
デッドロックが継続しているとき、現場は次の順序で判断すると迷いません。
- 影響範囲(どの API / どの機能か)を確定
- 失敗処理を一時停止できるか判断(バッチ停止、機能フラグ)
- ロック順序統一のホットフィックス可否
- デプロイまでの間、リトライ制御で被害抑制
「根本修正が間に合わない」ケースでも、リトライと負荷制御でユーザー影響を減らせます。
8. よくあるアンチパターン
アンチパターン1: SERIALIZABLE に上げて解決した気になる
隔離レベルを上げるだけでは、設計上の競合は消えません。むしろリトライ増加で負荷が悪化することがあります。
アンチパターン2: SELECT ... FOR UPDATE を乱用する
広範囲ロックは別の待ちを生みます。最小対象だけをロックし、順序を統一することが本質です。
アンチパターン3: アプリログだけ見て DB ログを見ない
デッドロックの循環情報は DB 側ログにしか出ないことが多いです。必ず DB ログを一次情報として扱ってください。
9. すぐ使えるチェックリスト
-
deadlock detectedの実例ログを保存した - ロック状況を
pg_stat_activityとpg_locksで取得した - 再現 SQL を作成し、原因パターンを確認した
- 更新順序を全経路で統一した
- リトライ回数とバックオフを制限した
- 監視メトリクスを追加した
まとめ
PostgreSQL デッドロック対策は、魔法の設定値を探す作業ではありません。ポイントは一貫しており、
- ログで事実を取る
- 再現を作る
- ロック順序を統一する
この 3 ステップで大半の問題は改善します。障害対応時は焦って設定を増やすより、まず「どの順序で誰がロックを取ったか」を可視化することが最短ルートです。
10. チーム運用に落とし込むためのルール化
技術対策ができても、運用ルールがないと再発します。特に効果が高いのは「PR テンプレートにロック観点を入れる」ことです。たとえば 複数行更新の順序は統一されているか、トランザクション内で外部I/Oをしていないか をチェック項目にするだけで、設計時点で多くの問題を防げます。
さらに、負荷試験シナリオに競合ケース(同時更新)を追加してください。通常の性能試験は平均応答時間を見るだけで終わりがちですが、デッドロックは並行競合を作らないと検出できません。QA と開発が協調して「再現しにくい障害を再現するテスト」を用意できると、運用品質が一段上がります。