PostgreSQL接続プール枯渇の実戦対処:再発防止までつなげる調査・改善プレイブック
本番障害でよくあるのが、too many clients already や remaining connection slots are reserved です。アプリ側から見ると「急にDBに繋がらない」、ユーザー側から見ると「全機能が遅い・失敗する」という最悪の体験になります。
厄介なのは、接続枯渇が「DBサーバー性能不足」だけで起こるわけではない点です。リーク、タイムアウト設定、長時間トランザクション、プールサイズ不整合など、複数要因が重なって起きます。
この記事では、接続枯渇に対して 発生時の初動 → 根本原因の特定 → 恒久対策 の順で、手順を実務レベルでまとめます。
1. まず初動:サービス継続を優先する
障害対応では、完璧な原因究明より「止血」が先です。以下を順番に実施します。
- 直近リリース有無を確認(機能フラグ含む)
- アプリの接続数・待機数・エラー率を確認
- DB側で
pg_stat_activityを取得 - 長時間実行クエリを必要に応じて停止
- 一時的にアプリ Pod 数を制限して雪だるま増幅を止める
pg_stat_activity の基本クエリ:
|
|
ここで見るべきは、state='idle in transaction' と異常に長い xact_duration です。これがあるとコネクションを握ったまま解放されず、枯渇の引き金になります。
2. 典型原因を4パターンで切り分ける
パターンA: アプリ接続プールサイズが過大
よくあるのが、以下のような構成です。
- Pod 20個
- 各Podのプール max 20
- 理論最大接続 400
- PostgreSQL
max_connections=300
この時点で設計破綻です。さらに管理接続やメンテ接続を引くと余裕ゼロになります。
対策:
- プールサイズ設計は「全インスタンス合計」で管理
max_connectionsの70〜80%以内に通常運用を収める- ピーク時はワーカ数/Pod数で制御
パターンB: コネクションリーク
finally で close していない、ORMセッションの寿命が長い、例外時に返却されない、といった実装ミスです。
Python(SQLAlchemy)の悪い例:
|
|
改善例:
|
|
FastAPI なら dependency でセッションスコープを統一し、endpointごとに確実に返却させるのが安全です。
パターンC: 長時間トランザクション
バッチ処理で 1トランザクションに大量更新を詰め込むと、ロック競合と接続占有が同時発生します。
対策:
- バッチをチャンク分割(例: 500件単位)
statement_timeout、idle_in_transaction_session_timeoutを設定- 長時間処理はキュー化して非同期実行
パターンD: プールとDBのタイムアウト不一致
アプリの接続再利用時間が長すぎると、DB側で切断済み接続を使って失敗し、リトライで更に接続圧を上げます。
対策:
- プールの
maxLifetimeを DB/NLB timeout より短く - 接続取得待ち時間(acquire timeout)を短くし、早めに失敗させる
- 失敗時リトライは指数バックオフ + ジッター
3. 具体的な設定例(PgBouncer + PostgreSQL)
高トラフィック環境では、アプリ直結より PgBouncer を挟むのが安定します。
pgbouncer.ini の例:
|
|
PostgreSQL 側の最小設定例:
max_connections = 300
shared_buffers = 4GB
idle_in_transaction_session_timeout = 60000
statement_timeout = 30000
log_min_duration_statement = 1000
pool_mode=transaction は接続効率が高い一方、セッション依存機能(一時テーブルや session variable)の扱いに注意が必要です。導入前に該当クエリを洗い出してください。
4. Kubernetes運用での落とし穴
Kubernetes では、HPA がスケールアウトすると同時に接続数が急増しやすいです。
4.1 設計式を最初に決める
最大接続見積もり = maxPods × perPodPoolMax + 管理余白
例:
- maxPods=30
- perPodPoolMax=8
- 管理余白=30
- 合計 270 →
max_connections=320なら許容
この計算を IaC へコメントで残しておくと、後任が壊しにくくなります。
4.2 readiness に DB接続必須チェックを入れすぎない
Pod起動時に全Podが同時にDBへ接続テストすると、再起動時にスパイクが起きます。readiness は軽量化し、重い初期化はバックグラウンドへ逃がすのが無難です。
5. 監視項目:再発防止の最低ライン
以下を可視化し、閾値アラートを設定します。
numbackends / max_connections- 接続待ち時間(アプリメトリクス)
idle in transactionセッション数- 95/99パーセンタイルのクエリ時間
- DBエラー率(connection refused, timeout)
推奨アラート例:
numbackends > 85%が 5分継続で Warningnumbackends > 92%が 2分継続で Criticalidle in transaction > 10が 3分継続で調査開始
6. 障害後レビュー(ポストモーテム)で必ず決めること
「接続が足りませんでした」で終えると再発します。次の観点を明文化します。
- なぜ早期検知できなかったか
- どの設定が設計値と乖離していたか
- コード修正・設定修正・監視修正の担当と期限
- 次回同様インシデント時の runbook 更新点
運用改善は「学びをコード化する」ことです。ドキュメントだけでなく、Terraform や Helm values に反映して初めて再発率が下がります。
7. 実務向けチェックリスト
最後に、現場で使いやすいチェックリストを置いておきます。
-
max_connectionsと総プール上限の関係を計算済み - 接続リーク防止(close/return保証)が実装されている
- 長時間トランザクションに timeout が設定済み
- PgBouncer の pool_mode が要件に適合している
-
numbackendsと接続待ち時間のアラートがある - 障害時 runbook に SQL コマンドと判断基準が書かれている
まとめ
PostgreSQL 接続枯渇は、単なるパラメータ不足ではなく、アプリ実装・スケーリング設計・監視不足が重なって起こる複合障害です。だからこそ、
- 初動で止血する
- パターン別に切り分ける
- 設定と実装を同時に直す
- 監視と runbook に落とし込む
この流れを徹底するだけで、同じ障害の再発率は大きく下げられます。次に障害が起きたとき、慌てず順番に潰せる状態を今日作っておくのが最善です。