PostgreSQL接続プール枯渇の実戦対処:再発防止までつなげる調査・改善プレイブック

本番障害でよくあるのが、too many clients alreadyremaining connection slots are reserved です。アプリ側から見ると「急にDBに繋がらない」、ユーザー側から見ると「全機能が遅い・失敗する」という最悪の体験になります。

厄介なのは、接続枯渇が「DBサーバー性能不足」だけで起こるわけではない点です。リーク、タイムアウト設定、長時間トランザクション、プールサイズ不整合など、複数要因が重なって起きます。

この記事では、接続枯渇に対して 発生時の初動 → 根本原因の特定 → 恒久対策 の順で、手順を実務レベルでまとめます。

1. まず初動:サービス継続を優先する

障害対応では、完璧な原因究明より「止血」が先です。以下を順番に実施します。

  1. 直近リリース有無を確認(機能フラグ含む)
  2. アプリの接続数・待機数・エラー率を確認
  3. DB側で pg_stat_activity を取得
  4. 長時間実行クエリを必要に応じて停止
  5. 一時的にアプリ Pod 数を制限して雪だるま増幅を止める

pg_stat_activity の基本クエリ:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
SELECT
  pid,
  usename,
  application_name,
  client_addr,
  state,
  wait_event_type,
  wait_event,
  now() - query_start AS query_duration,
  now() - xact_start  AS xact_duration,
  left(query, 120) AS query_head
FROM pg_stat_activity
WHERE datname = current_database()
ORDER BY xact_start NULLS LAST, query_start NULLS LAST;

ここで見るべきは、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)の悪い例:

1
2
3
4
session = SessionLocal()
user = session.query(User).filter(User.id == user_id).first()
# 例外時にcloseされない可能性
return user

改善例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from contextlib import contextmanager

@contextmanager
def get_session():
    session = SessionLocal()
    try:
        yield session
        session.commit()
    except Exception:
        session.rollback()
        raise
    finally:
        session.close()

FastAPI なら dependency でセッションスコープを統一し、endpointごとに確実に返却させるのが安全です。

パターンC: 長時間トランザクション

バッチ処理で 1トランザクションに大量更新を詰め込むと、ロック競合と接続占有が同時発生します。

対策:

  • バッチをチャンク分割(例: 500件単位)
  • statement_timeoutidle_in_transaction_session_timeout を設定
  • 長時間処理はキュー化して非同期実行

パターンD: プールとDBのタイムアウト不一致

アプリの接続再利用時間が長すぎると、DB側で切断済み接続を使って失敗し、リトライで更に接続圧を上げます。

対策:

  • プールの maxLifetime を DB/NLB timeout より短く
  • 接続取得待ち時間(acquire timeout)を短くし、早めに失敗させる
  • 失敗時リトライは指数バックオフ + ジッター

3. 具体的な設定例(PgBouncer + PostgreSQL)

高トラフィック環境では、アプリ直結より PgBouncer を挟むのが安定します。

pgbouncer.ini の例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = md5
pool_mode = transaction
max_client_conn = 5000
default_pool_size = 80
reserve_pool_size = 20
server_reset_query = DISCARD ALL
server_idle_timeout = 30

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分継続で Warning
  • numbackends > 92% が 2分継続で Critical
  • idle in transaction > 10 が 3分継続で調査開始

6. 障害後レビュー(ポストモーテム)で必ず決めること

「接続が足りませんでした」で終えると再発します。次の観点を明文化します。

  1. なぜ早期検知できなかったか
  2. どの設定が設計値と乖離していたか
  3. コード修正・設定修正・監視修正の担当と期限
  4. 次回同様インシデント時の runbook 更新点

運用改善は「学びをコード化する」ことです。ドキュメントだけでなく、Terraform や Helm values に反映して初めて再発率が下がります。

7. 実務向けチェックリスト

最後に、現場で使いやすいチェックリストを置いておきます。

  • max_connections と総プール上限の関係を計算済み
  • 接続リーク防止(close/return保証)が実装されている
  • 長時間トランザクションに timeout が設定済み
  • PgBouncer の pool_mode が要件に適合している
  • numbackends と接続待ち時間のアラートがある
  • 障害時 runbook に SQL コマンドと判断基準が書かれている

まとめ

PostgreSQL 接続枯渇は、単なるパラメータ不足ではなく、アプリ実装・スケーリング設計・監視不足が重なって起こる複合障害です。だからこそ、

  • 初動で止血する
  • パターン別に切り分ける
  • 設定と実装を同時に直す
  • 監視と runbook に落とし込む

この流れを徹底するだけで、同じ障害の再発率は大きく下げられます。次に障害が起きたとき、慌てず順番に潰せる状態を今日作っておくのが最善です。