データベース接続の設計 — 接続数上限とプールをどこに置くか

データベース接続設計で最初に問われる判断は「プールをどの層に置くか」です。アプリサーバーを増やすたびに接続数が膨らみ、DBが行き詰まる構造を見落とすと、水平スケールそのものが機能しなくなります。本記事では接続数上限の仕組みから、アプリ内プール・中間プーラーの使い分け、参照系と更新系の分離判断、プール過多による逆効果まで、設計の根拠となる数字と境界線を順に整理します。

アプリを増やした瞬間に接続数上限で落ちる構造

接続消費の構造を把握する

当初1台のアプリサーバーで動いていたシステムが、負荷増を理由に3台・5台と水平展開した途端にDBへの接続エラーが急増する、という状況は多くの現場で報告されています。原因はシンプルで、アプリサーバーを増やすと各サーバーが独自に接続プールを保持するため、DBが受け付ける接続数の合計が台数に比例して増加します。PostgreSQLのデフォルトmax_connectionsは100であり、MySQLも同様に接続総数に上限を設けています。アプリサーバーが5台でそれぞれ20接続のプールを持てば、それだけで合計100接続に達します。さらにデプロイ時の一時的な並走や監視ツールの接続が加われば、通常運用でも上限を超えます。

スケールアウトが裏目に出る典型パターン

オートスケールと組み合わさると状況はさらに深刻になります。負荷増に応じてアプリサーバーが10台・20台と増えた局面で、接続数の合計がDBの受け入れ上限を超え、DB自体が新規接続を拒否し始めます。アプリを増やすほどDBが落ちるという逆説的な状態です。この問題が厄介なのは、アプリサーバー単体では異常が検知されないため、アプリ側の監視だけでは発見が遅れやすい点にあります。DB側の接続数メトリクスを常時観測する体制がなければ、障害が発生してから初めて原因を突き止める流れになります。現場では「スケールアウト後の初日にサービス全体が停止した」という話をよく聞きます。

問題が顕在化するタイミング

開発環境やステージング環境では問題が表面化しないケースが大半です。ステージングがアプリサーバー1台構成であれば接続数の問題は隠れたまま本番に持ち込まれ、本番展開後の初回スパイク時やリリース直後の並列デプロイ中に顕在化します。接続設計はスケールした状態を前提に設計段階で考慮しなければならず、運用開始後に修正しようとすると影響範囲がアプリ全体に広がります。「本番でのみ再現する問題」として扱われがちですが、原因は設計時の見積もり不足であることが大半です。早期に接続数の上限試算を行っておくことが、後の手戻りを防ぐ最大の対策です。

アプリ内プールと中間プーラーの使い分けの判断

アプリ内プールが適しているケース

接続プールをアプリプロセス内に保持する方式は、実装がシンプルで余分なネットワークホップが発生しない点が強みです。アプリサーバーが3台以内で、DBのmax_connectionsに対して十分な余裕がある構成なら、アプリ内プールだけで設計が完結します。JavaのHikariCP、PythonのSQLAlchemy、Node.jsのpgといったライブラリはこの方式をデフォルトで採用しており、プールサイズ・最小アイドル接続数・取得タイムアウト・接続有効期間の4項目を適切に設定するだけで運用できます。ただし「念のため大きめに設定する」という運用はアンチパターンで、後述するプール過多の問題を引き起こします。

中間プーラーが必要になる判断軸

アプリサーバーが4台を超える見込みがある、あるいはコンテナやサーバーレス関数のように短命プロセスが多数並走するアーキテクチャでは、中間プーラーの導入を検討します。PgBouncer(PostgreSQL向け)やProxySQL(MySQL向け)が代表的な選択肢です。中間プーラーはアプリからの接続要求を受け取り、DBへの物理接続を集約・再利用します。PgBouncerのtransactionモードではトランザクションが終了した瞬間に物理接続を解放して次のリクエストに割り当てるため、接続効率が大幅に向上します。アプリサーバーが20台に増えてもDB側の物理接続数は中間プーラーが管理するサイズに収まります。

両者を組み合わせる場合の数字の整合

中間プーラーを導入した場合でも、アプリ内プールを完全に廃止するわけではありません。アプリ→中間プーラー間の接続もカウントされるため、アプリ内プールのサイズ設定は依然として必要です。設計のポイントは「全アプリサーバーのプール合計 ≦ 中間プーラーの最大接続数 × 余裕係数(0.8程度)」という関係を守ることです。アプリ内プールを絞りすぎると中間プーラーへの接続待ちが発生し、多すぎると中間プーラーの上限を突破します。最終的な数字は負荷試験で検証しながら決めます。多段プール構成は運用が複雑になるため、設定値の管理を設定ファイルに一元化しておくことが保守性の観点から重要です。

比較表: アプリ内プール/中間プーラー/直接接続の比較

3方式を選択する際の判断軸を以下にまとめます。スケール耐性・運用複雑度・適した構成規模・主な注意点の観点から整理することで、どの方式を選ぶ根拠が明確になります。

方式 スケール耐性 運用複雑度 適した構成規模 主な注意点
アプリ内プール 低〜中(アプリ台数に依存) 低 アプリサーバー3台以内 台数増加で接続数が線形増加する
中間プーラー(PgBouncer等) 高(接続数をDB側で抑制) 中 4台以上・コンテナ・Lambda transactionモード時のプリペアドステートメント制限
直接接続(プールなし) 非常に低 低 バッチ・単発ツール 接続確立コストが毎回発生する

最大接続数とアイドル数をどの数字から決めるか

DBエンジンの上限から逆算する

設計の出発点はDBエンジン側のmax_connectionsです。PostgreSQLでは各接続がバックエンドプロセスとして動作するため、接続数が増えるとメモリとCPUのコンテキストスイッチコストが直接上昇します。参考値として、CPUコア数の2〜4倍程度が実効的な同時クエリ処理の上限とされており、接続数がこれを大きく超えてもスループットは伸びません。メモリ16GBのDBサーバーで1接続あたり約5MBのメモリを消費するとすれば、300接続では約1.5GBがプロセス管理に取られます。この計算から許容最大接続数を設定し、監視ツール・レプリカ接続・マイグレーションツール分を差し引いた残りをアプリに配分するのが基本手順です。

アイドル接続の扱いと設定の落とし穴

アプリ内プールの設定でよく見落とされるのがアイドル接続数(minimum idle / min pool size)の扱いです。リクエストが少ない時間帯でもアイドル接続がDB側に張り続け、リソースを無用に消費します。アイドル接続数は最大プールサイズの20〜30%を目安にしておき、接続の有効期間(maxLifetime・connection-timeout)も設定して長期間使われなかった接続を自動解放します。この設定を怠ると、夜間バッチ後にアイドル接続が残り続け、翌朝のピーク時に接続枯渇が起きる事例が報告されています。接続の生存時間はDBサーバー側のタイムアウト設定と整合を取ることも必要です。

環境ごとの接続数設計の分離

本番・ステージング・開発で同じ接続設定を使い回すと、ステージングでの検証が本番の問題を再現しません。プールサイズを環境変数で切り替えることと、本番相当のアプリ台数を想定した接続数負荷試験をステージングで実施する体制が必要です。複数のサービスが同一DBを共有するマルチサービス構成では、サービスごとに接続割り当て上限を設ける設計も判断の分岐点になります。環境間の設定差分は設定ファイルの差分として管理し、意図せず本番設定がステージングに混入しないよう構成管理の仕組みで担保します。

参照系と更新系の接続をどこで分けるか

リードレプリカへの分散の判断軸

参照クエリと更新クエリを同じプライマリに集中させると、参照負荷がプライマリのCPU・I/Oを圧迫し、書き込みレイテンシに悪影響を与えます。リードレプリカを設けて参照系を分散させるのは、アプリサーバーが増えた段階での定番対処です。判断の目安として、参照クエリが全クエリの70%以上を占め、かつプライマリのCPU使用率が50%を超える状況が継続するなら分離を検討します。ただし複製遅延(レプリケーションラグ)を許容できる参照にのみ分散を適用するのが前提で、書いた直後に読み返す処理はプライマリ接続を使わないと整合性が崩れます。

接続先を切り替える設計パターン

アプリ層でプライマリとレプリカを使い分けるには、接続先を環境変数・設定ファイルで明示的に切り替えるシンプルな方法と、ProxySQLのようなプロキシ層でクエリ内容(SELECT/INSERT/UPDATE)を自動ルーティングする方法があります。プロキシ層での自動ルーティングは設定ミスがあると意図しないプライマリへのフォールバックが発生するため、ルーティングログとメトリクスを整備したうえで本番適用します。アプリ層での明示的な切り替えは実装がシンプルな反面、実装漏れで更新クエリがレプリカに飛ぶリスクがあるため、読み書きの接続オブジェクトを別々に管理する設計にします。

分離が裏目に出るケース

参照と更新の接続を分離した後に問題として浮上するのが、フェイルオーバー時の対応です。プライマリが落ちてレプリカが昇格する際、参照用と更新用の接続設定を両方切り替える必要が生じます。構成管理が不十分だと片方だけが古いプライマリを向いたままになります。フェイルオーバー自動化ツールと接続設定の自動更新を組み合わせるか、仮想IPを接続先として使い接続設定の更新漏れを防ぐ設計が必要です。分離の恩恵はレプリカが1台でもあれば得られますが、フェイルオーバー設計とセットで整備しなければ可用性を下げるリスクがあります。

プールを大きくして逆にDBが詰まる境界

コネクション過多がDB自体を圧迫するメカニズム

「プールサイズを大きくするほど応答が速くなる」という誤解は根強く、「念のため接続数を200に設定した」という事例が報告されています。実際には接続数が増えるとロック待ちのプロセスが積み重なり、スロークエリが連鎖します。PostgreSQLでは接続数が多いほどロック管理のオーバーヘッドが増え、一定数を超えるとスループットが低下する現象が観測されています。接続数を100から20に削減してDB応答時間が半分以下に改善した、という報告も珍しくありません。プールサイズを増やす前にDB側のクエリ最適化とインデックス見直しを先に実施するのが基本方針です。

プール過多が誘発するカスケード障害

アプリがDB応答を待機している間に新規リクエストが届き続けると、接続プールの空き待ちが積み上がります。接続取得タイムアウトが長ければHTTPレイヤーの接続も詰まり始め、最終的にアプリサーバーのスレッドが枯渇してサービス全体が行き詰まります。この連鎖を断ち切るには、接続取得タイムアウトを短く設定して早期にエラーを返す設計が有効です。サーキットブレーカーパターンを組み込んでDB障害時の流量を制御する方法も、カスケード障害の防止策として現場で採用されています。プールサイズの拡大は症状の先送りであり、根本はDB側のボトルネック解消にあります。

プールサイズの見極め方

適切なプールサイズを決めるには負荷試験が不可欠です。同時リクエスト数・クエリのレイテンシ・プールの待機キュー長を計測し、プールサイズを段階的に増やしながらスループットと応答時間の変化を記録します。スループットの向上が頭打ちになる接続数が実効的な上限の目安です。HikariCPの作者が提唱する計算式「(コア数 × 2)+ 有効ディスクスピンドル数」はアプリ内プールサイズの出発点として参照されることが多く、これに実際の負荷試験結果を重ねて最終値を決めるアプローチが合理的です。実際に手を動かす負荷試験の手順はハンズオン資料で確認してください。

接続設計チェックリスト

以下は接続設計の見直しや新規構築時の確認事項です。設計フェーズと設定フェーズの両方でチェックします。

  • アプリサーバーの最大台数 × 最大プールサイズ の合計がDBのmax_connectionsを超えていないか
  • 監視ツール・マイグレーションツール・レプリカ接続分をmax_connectionsから差し引いているか
  • アプリ台数が4台以上になる見込みがある場合、中間プーラーの導入を設計段階で検討しているか
  • 中間プーラーを導入した場合、全アプリのプール合計が中間プーラーの最大接続数の80%以内に収まるか
  • アイドル接続数(minimum idle)と接続有効期間(maxLifetime)を設定しているか
  • DBサーバー側のタイムアウト設定とアプリ側の接続有効期間が整合しているか
  • プールサイズが環境変数で本番・ステージング・開発ごとに切り替えられているか
  • 参照クエリと更新クエリの接続先を明示的に分けているか(または分離不要と判断したか)
  • リードレプリカ分散時、レプリケーションラグを許容できない参照はプライマリに向けているか
  • フェイルオーバー時に参照用・更新用両方の接続設定が自動更新される仕組みがあるか
  • 接続取得タイムアウトを短く設定して、DB障害時に早期エラーを返す設計になっているか
  • 本番相当のアプリ台数を想定した接続数負荷試験をステージングで実施しているか

▶ 手を動かして本番まで到達する

その設計判断を、RHEL10の本番環境で手を動かして固めませんか

ここまでの設計判断は、読めば腹に落ちます。ですが「自分の手で本番まで通しきる自信」は、それとは別の話になります。

最大6名の少人数で、構築実務者の到達点まで引き上げます。座学で終わらせず、本番相当の環境で判断を検証する2日間です。

データベース接続設計を実装で体験する上級Linuxハンズオンへ →

Linuxセミナー上級編(RHEL10)|最大6名の少人数ハンズオン