Claude Media
Claude Codeでpgbouncerの接続プール上限をPostgreSQLに合わせる

Claude Codeでpgbouncerの接続プール上限をPostgreSQLに合わせる

接続枯渇のエラーから、max_connections・pgbouncer・HikariCPの上限を突き合わせる手順です。Claude Codeには読み取り専用ロールで集計させ、設定の変更は人が確認して入れます。

PostgreSQLの接続枯渇は、どれか1つの設定が小さいから起きるのではありません。max_connections、pgbouncerのdefault_pool_size、アプリ側プールのmaximumPoolSizeが、別々のファイルで別々の人に決められ、合計が噛み合わなくなったときに起きます。

この突き合わせは、数字を集めて足し算と比較をする作業です。Claude Codeに任せやすい一方、DBに書き込める権限を渡したまま「直しておいて」と頼むと、設定変更やpg_terminate_backendまで実行されかねません。ここでは、読み取り専用の経路で現状を集計させ、上限の再設計案だけを出させる進め方を、設定とSQLの断片つきで説明します。

枯渇は「どの層の上限に当たったか」で切り分ける

接続の通り道は、アプリ、pgbouncer、PostgreSQLの3層です。エラーが出た層を先に決めないと、上限を上げる先を間違えます。

切り分け

接続が詰まる3つの層

  • PostgreSQL本体

    max_connectionsの上限に達した状態です。既定は通常100接続で、カーネル設定によってはそれより少なくなります。

  • pgbouncer

    クライアント側のmax_client_conn、またはサーバー側のプール(default_pool_sizeなど)が埋まり、待ち行列が伸びた状態です。

  • アプリ側プール

    HikariCPなら、プールがmaximumPoolSizeに達し、getConnection()がconnectionTimeoutまで待って例外になる状態です。

接続時にremaining connection slots are reservedで始まるエラーが出た場合は、PostgreSQL本体の層で枠が尽きています。superuser_reserved_connections(既定は3)とreserved_connections(既定は0)で確保された枠を除いた空きが尽き、通常のロールでは新規接続を受けられなくなっています。公式の説明では、空きスロットがsuperuser_reserved_connectionsとreserved_connectionsの合計以下になると、一般ロールの接続は受け付けられなくなります。

一方、アプリ側の例外メッセージはドライバーや言語で違います。メッセージの文言ではなく、どの層の数字が上限に張り付いているかで判断するほうが確実です。

Claude Codeには読み取り専用のロールだけを渡す

集計を任せる前に、権限を決めます。psqlの引数を許可ルールで絞る方法は、期待どおりに効きにくい構造になっています。

Claude Codeの権限ルールは、Bashコマンドのテキストに対して*で照合します。公式のドキュメントも、引数を制約しようとするBashのパターンは壊れやすいと注意しています。psqlに-cで渡すSQLの中身を、許可ルールだけで「SELECTのみ」に制限するのは現実的ではありません。

確実なのは、権限の源をDB側に置く方法です。SELECTだけを持つ専用ロールを作り、そのロールの接続情報だけをClaude Codeの環境に渡します。許可ルールは、実行してよいコマンドの範囲を広げすぎないための補助にとどめます。

{
  "permissions": {
    "allow": [
      "Bash(psql *)"
    ],
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)"
    ]
  }
}

この設定では、psqlが承認なしで動くかわりに、本番の接続情報を含む.envの読み取りを拒否します。Bash(psql *)の末尾の*は、スペースを挟んだ形で書くとpsql単体にも一致します。接続先は環境変数のPGHOSTやPGUSERなどで読み取り専用ロールに向けておき、パスワードはコマンドラインに書かせません。

deny、ask、allowの順で評価され、denyに一致すれば、より狭いallowがあっても拒否されます。本番DBを触れる環境では、書き込み系のコマンドをdenyに足す前に、そもそも書き込めないロールにしておくほうが堅実です。

pg_stat_activityで実際の接続を集計する

現状の把握には、pg_stat_activityを使います。1接続につき1行で、接続先のデータベース(datname)、ロール(usename)、アプリ名(application_name)、状態(state)、バックエンドの種類(backend_type)を持ちます。

次のクエリは、どのアプリがどの状態で枠を使っているかを数えます。読み取り専用で、結果は数行から数十行に収まります。

SELECT datname, usename, application_name, state, count(*) AS conns
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY datname, usename, application_name, state
ORDER BY conns DESC;

stateの値はactive、idle、idle in transactionなどです。idle in transactionは「トランザクションの中にいるが、いま問い合わせを実行していない」状態で、プールの接続を握ったまま動かない代表的な原因になります。idleが大半なら、プールが大きすぎるか、アプリ側が返却せずに持っている可能性があります。

もう1本、上限との距離を見るクエリを用意します。

SELECT
  current_setting('max_connections')::int AS max_conn,
  current_setting('superuser_reserved_connections')::int AS su_reserved,
  current_setting('reserved_connections')::int AS reserved,
  count(*) FILTER (WHERE backend_type = 'client backend') AS in_use
FROM pg_stat_activity;

お使いのPostgreSQLのバージョンにreserved_connectionsの設定が無い場合は、current_setting('reserved_connections')の行がエラーになります。その行を消して実行してください。

Claude Codeに渡すのは、この2本の結果です。接続情報やクエリ全文(query列)は、SQLに個人情報が含まれることがあるため集計に含めません。

pgbouncer側の数字を取る

pgbouncerには、管理用のコンソールがあります。SHOW POOLSは、(データベース, ユーザー)の組ごとに1行を返します。

列意味
cl_active意味サーバー接続に紐づいている、またはクエリ待ちのないクライアント接続
cl_waiting意味クエリを送ったが、まだサーバー接続を得ていないクライアント接続
sv_active意味クライアントに紐づいているサーバー接続
sv_idle意味すぐ使える未使用のサーバー接続
maxwait意味キュー先頭のクライアントが待っている秒数

見るべき点は2つです。cl_waitingが0でなく、maxwaitが伸び続けているなら、プールが小さすぎるか、サーバー側が遅くて接続が返ってこない状態です。公式は、maxwaitが増え続けるときの原因を「サーバーの過負荷、またはpool_sizeの設定が小さすぎること」と説明しています。

SHOW CONFIGは現在の設定値を返します。SHOW POOLSとSHOW CONFIGの出力を、そのままClaude Codeに貼って構いません。管理コンソールには接続のための認証情報が要るので、コンソールに入るコマンドはこちらで実行し、結果だけを渡すと、Claude Codeに管理権限を渡さずに済みます。

上限を掛け算で突き合わせる

材料が揃ったら、次の関係を満たしているかを見ます。

手順

上限を突き合わせる順序

  1. 1

    PostgreSQLが受けられる数を出す

    max_connectionsからsuperuser_reserved_connectionsとreserved_connectionsを引いた値が、通常のロールが使える枠です。既定の100とsuperuser用の3だけなら、97になります。

  2. 2

    pgbouncerが開くサーバー接続の最大値を出す

    default_pool_sizeは「ユーザーとデータベースの組」ごとの上限で、既定は20です。組が3つあれば、最大で60のサーバー接続になります。reserve_pool_sizeを設定していれば、その分が組ごとに加わります。

  3. 3

    pgbouncerを通らない接続を足す

    マイグレーション、バッチ、管理ツールなどがPostgreSQLへ直接つなぐなら、その数も同じ枠を使います。ここを見落とすと、pgbouncer経由の合計は上限内なのに枯渇します。

  4. 4

    クライアント側の合計と比べる

    アプリのインスタンス数にmaximumPoolSizeを掛けた値が、pgbouncerへのクライアント接続の見込みです。これをmax_client_conn(既定は100)と比べます。

数字を置いて確かめます。次は仮の構成です。

項目値
max_connections値100(既定)
superuser_reserved_connections値3(既定)
アプリのインスタンス数値12
HikariCPのmaximumPoolSize値10(既定)
pgbouncerのmax_client_conn値100(既定)
pgbouncerのdefault_pool_size値20(既定)
接続する組(ユーザー × DB)値1

クライアント側の合計は12 × 10 = 120で、max_client_connの100を超えます。一方、PostgreSQLに届くのはプールの上限である20接続までなので、max_connectionsの97には十分収まります。この構成では、PostgreSQL本体は余裕があるのに、pgbouncerの入口で詰まるという、直感と逆の結果になります。

直す方向は2つあります。入口のmax_client_connを増やすか、各インスタンスのmaximumPoolSizeを減らすかです。max_client_connを増やすなら、ファイルディスクリプタの上限も確認が要ります。公式は、使われるディスクリプタがmax_client_connより多くなりうると説明しています。

Claude Codeに依頼する文面

数字を渡して、計算と比較だけをさせます。設定ファイルの編集は、案を見てから別の依頼にします。

次の3つの出力から、接続の上限を突き合わせてください。
- pg_stat_activity の集計結果
- pgbouncer の SHOW POOLS と SHOW CONFIG
- アプリのインスタンス数と maximumPoolSize
 
出力は次の形にしてください。
1. PostgreSQLが受けられる数と、pgbouncerが開く最大数の計算式
2. 上限に当たっている層とその根拠(出力の該当行を引用)
3. 変更案。ファイルは編集せず、変更前後の値だけを表にする
入力にない値は推測せず「不明」と書いてください。

「根拠の行を引用させる」「ファイルを編集させない」「不明と書かせる」の3つが縛りです。これを外すと、それらしい推奨値だけが返ります。負荷試験の数字から原因を切り分ける進め方は、Claudeで負荷試験の結果を分析するにまとめています。

プロジェクトのCLAUDE.mdに、接続まわりの約束を書いておくと、毎回の依頼文が短くなります。

## DB接続の扱い
- 本番DBへの接続は、読み取り専用ロール(接続情報は環境変数)だけを使う
- 実行してよいのは SELECT のみ。設定変更・接続の強制切断は提案までにとどめる
- 接続数の議論では、必ず max_connections・pgbouncer・アプリ側プールの合計を式で示す

変更を入れる前に確認する落とし穴

pool_modeを変えると挙動が変わる

pool_modeの既定はsessionで、クライアントが切断するまでサーバー接続が戻りません。transactionにすると、トランザクションが終わった時点で接続が戻るため、少ないサーバー接続で多くのクライアントを捌けます。

ただし、transactionとstatementでは、セッション単位の状態に依存するアプリが影響を受けます。プリペアドステートメントを使っている場合は、max_prepared_statementsを0以外にすると、pgbouncerがプロトコルレベルのプリペアドステートメントを追跡し、別のサーバー接続でも使えるようにします。モードを切り替える前に、アプリがセッション状態を使っていないかを確かめてください。

上限の合計は組の数で増える

default_pool_sizeは組ごとの上限です。ユーザーを分けた接続や、データベースを増やしたときに、サーバー接続の最大値が掛け算で増えます。全体を抑えたいときは、データベース単位のmax_db_connections、ユーザー単位のmax_user_connectionsで上限を置けます。どちらも既定は0(無制限)です。

上限に当たった直後は、片方のプールのクライアントが切れても、別のプールにすぐ接続が回るわけではありません。サーバー接続がアイドルタイムアウトで閉じてから、待っているプールに新しい接続が開きます。

reserve_poolは待ち時間が長いときだけ使われる

reserve_pool_sizeは、プールに追加できる接続数です。reserve_pool_timeout(既定5秒)のあいだクライアントが処理されなかったときに使われます。既定のreserve_pool_sizeは0で無効です。追加分は上限の計算に足す必要があります。

待たせ続けるかどうかを決める

query_wait_timeoutの既定は120秒で、この時間を超えてサーバー接続を得られなかったクライアントは切断されます。0にすると、クライアントは無期限にキューで待ちます。HikariCPのconnectionTimeoutは既定30秒なので、アプリ側が先に例外を出す構成になっています。どちらで失敗させたいかを決めて、両方の値の大小を揃えておくと、障害時のエラーの出方が読みやすくなります。

アプリ側プールは大きいほど速いとは限らない

HikariCPのmaximumPoolSizeは、アイドルと使用中の両方を含む上限で、既定は10です。READMEは「接続は思われているほど多く要らない」と述べ、多すぎる接続が性能に悪影響を及ぼすと説明しています。上限を上げて枯渇を逃れる前に、idle in transactionで接続を握り続けているコードがないかを、先ほどの集計で確かめます。

スロークエリが接続を長く占有している場合は、プールを増やしても改善しません。その場合はClaudeでスロークエリをEXPLAIN ANALYZEで絞る手順で、占有時間そのものを短くします。ロックの待ち合いが疑わしいときは、デッドロックの原因を調べる手順が使えます。マネージドDBで同じ作業をするなら、Cloud SQL for PostgreSQLプラグインの接続方法も確認してください。

変更後の確かめ方

設定を変えたら、同じ集計をもう一度取り、変更前後で次を比べます。

  • SHOW POOLSのcl_waitingとmaxwaitが0に戻っているか
  • pg_stat_activityのidle in transactionの件数が減っているか
  • in_useがmax_connectionsからsuperuser_reserved_connectionsとreserved_connectionsを引いた値に対して、余裕を保っているか

比較の表づくりはClaude Codeに任せられますが、pgbouncerの再読み込みやPostgreSQLの再起動を伴う変更は、人が実行します。max_connectionsは、サーバーの起動時にしか設定できません。

まとめ

接続枯渇の修正は、上限を1つ上げる作業ではなく、3つの層の掛け算を揃える作業です。Claude Codeには読み取り専用のロールと「ファイルを編集しない」という縛りを渡し、計算式と根拠の行つきで比較させると、変更案を人が確かめやすくなります。

実際の構成で効いてくるのは、pgbouncerを通らない直接接続の数と、idle in transactionで枠を握っているセッションの有無です。この2つを先に数えておくと、上限をどこまで動かすかの判断がぶれにくくなります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. postgresql.org/docs/current/runtime-config-connection.html
  2. postgresql.org/docs/current/monitoring-stats.html
  3. pgbouncer.org/config.html
  4. pgbouncer.org/usage.html
  5. github.com/brettwooldridge/HikariCP
  6. code.claude.com/docs/en/permissions
この記事を共有:XはてブLinkedIn