Query Go
コネクションプール — PgBouncer / RDS Proxy / プールサイズの決め方
チューニング

コネクションプール — PgBouncer / RDS Proxy / プールサイズの決め方

DB 接続コスト、アプリ側プール・PgBouncer・RDS Proxy、プールサイズの見積もりと pooling 方式。

コネクションプール — PgBouncer / RDS Proxy / プールサイズの決め方 diagram

なぜプールが要るのか

DB への接続は意外と高コストです。TCP ハンドシェイク、TLS 交渉、認証、セッション初期化… 合計 10〜100 ms 掛かることも珍しくありません。しかも PostgreSQL は 1 接続 = 1 プロセス、メモリを 5〜15 MB 消費します。

アプリが「リクエストごとに接続→切断」を繰り返すと:

  • 各リクエストの最初の 10〜100 ms が接続待ちで消える
  • DB サーバーがプロセス / メモリで溢れる
  • ピーク時に too many connections エラーが多発

これを防ぐのがコネクションプール。起動時にあらかじめ数本繋いでおき、リクエストごとに貸し出し / 返却する仕組みです。

アプリ側プール vs 外部プーラ

プールは大きく 2 種類あります:

  • アプリ側プール: 言語/FW に組み込まれたプール。HikariCP (Java), pgx (Go), SQLAlchemy, ActiveRecord, Prisma など。1 プロセスに 1 プール
  • 外部プーラ: PgBouncer(PostgreSQL 用、軽量・高速)、Pgpool-IIProxySQL(MySQL 用)、RDS Proxy(AWS マネージド)。アプリとは独立したプロセス

サーバーレス(Lambda / Cloud Functions)や、プロセス数が多いアプリ(Ruby / Python の worker 多数)では外部プーラがほぼ必須。アプリ側プールは 1 プロセスずつなので、500 プロセス × 10 本 = 5,000 接続、などすぐ上限に達します。外部プーラを挟めばアプリ側 5,000 ↔ DB 側 100のような圧縮ができます。

transaction pooling と session pooling

PgBouncer 等にはプーリングモードがあり、共有の単位が変わります:

  • session pooling: クライアント接続中はずっと同じ DB 接続を占有。機能互換性が高いが、接続削減効果は小さい
  • transaction pooling: トランザクション単位で DB 接続を貸し出し。トランザクションが終わったらすぐ返却。圧縮効果が大きいが、セッション状態(SET, 準備済み文、一時テーブル)が使えない制約
  • statement pooling: 1 ステートメントごと。さらに厳しい制約

サーバーレスや高同時実行のアプリでは transaction pooling 一択。ただし ORM が準備済みステートメントを暗黙に使うと壊れるので、「準備済み無効化」設定や prepareThreshold=0 などのチューニングが必要です(PgBouncer 1.22 からは transaction pooling でも名前付き prepared statement が使えるようになりました)。

プールサイズの決め方

「プールサイズを大きくすればするほど速い」は誤解です。DB の CPU コア数を超えると、コンテキストスイッチで逆に遅くなるのが定説。HikariCP の著名なドキュメント「About Pool Sizing」の経験則:

connections = ((core_count * 2) + effective_spindle_count)

例: 8 コア・SSD(スピンドル 1 扱い)なら 8*2 + 1 = 17 本。SaaS 時代の典型は 10〜30 本で十分なことが多いです。

決め方のポイント:

  • アプリ側の worker 数 × 1 プールあたり本数 がDB の max_connections を超えないこと
  • 足りなければ外部プーラで圧縮
  • 待ち時間(acquire time)を監視し、頻繁に長時間待つなら増やす、常にガラガラなら減らす
  • ロングトランザクションや長時間クエリが滞留するとプールが枯渇するので、statement_timeout / idle_in_transaction_session_timeout 併用を
sql
-- PostgreSQL: 上限確認
SHOW max_connections;

-- 長時間アイドルのトランザクションを殺す
ALTER SYSTEM SET idle_in_transaction_session_timeout = '60s';
ALTER SYSTEM SET statement_timeout = '30s';
SELECT pg_reload_conf();

マネージドな選択肢(RDS Proxy / Cloud SQL)

クラウドを使っているなら、マネージドなプーラを使うのが運用的にも楽です:

  • AWS RDS Proxy: RDS / Aurora の前段に入るプーラ。Lambda と相性が良い。接続多重化 + フェイルオーバ補助
  • Google Cloud SQL Auth Proxy / AlloyDB Language Connectors: 認証+TLS 補助。ただし純粋なプーリングは限定的で、アプリ側 or PgBouncer 併用が推奨
  • Supabase / Neon: プラットフォームに PgBouncer が組み込まれている

PgBouncer を自前運用するか、マネージドに任せるかは運用コスト次第。可用性要件が高ければ自前でも HA 構成が必要になるので、マネージドに寄せたほうが割に合うことも多いです。