Kubernetes上でSpring BootアプリのHPAを有効にしたら、急にDBが「too many connections」で悲鳴を上げ始めた、という経験はありませんか。単一インスタンスで動かしていたときは何の問題もなかったのに、レプリカを増やした途端にエラーが出るのは典型的なスケールアウトの落とし穴です。

この記事では、DBのmax_connectionsとレプリカ数からPodあたりの maximumPoolSize を逆算する考え方と、HPAでレプリカ数が変動する環境での安全マージンの取り方をまとめます。

レプリカを増やした瞬間にDB接続エラーが増える

よくあるのは、HPAでPod数が2から5に増えたタイミングでエラーが急増するパターンです。アプリ側のログにはこんな内容が出ます。

[HikariPool-1] Connection is not available, request timed out after 30000ms.

DB側(PostgreSQLの場合)はこう言ってきます。

FATAL:  remaining connection slots are reserved for non-replication superuser connections

MySQLなら次のようなエラーです(本記事の逆算式・参考ドキュメントはPostgreSQLを軸にしていますが、考え方はMySQL環境でも同じです)。

ERROR 1040 (HY000): Too many connections

厄介なのは、各Podのメトリクスを見ても特に異常が見当たらない点です。カスタムHealthIndicator でDB接続を監視していても1Podあたりは正常範囲内なのに、全Podの合計で見るとDBの上限を超えてしまっている、という構図に気づきにくいのが原因です。

プールサイズはPodごとに独立して確保される

HikariCPのプールサイズチューニング記事 で紹介した「コア数 × 2 + 1」という式は、あくまで単一インスタンスを前提にした計算です。maximumPoolSize はJVMプロセスごとの上限であり、Kubernetes上にデプロイしたDeployment はこの設定をPod数分だけ複製しているだけに過ぎません。

つまりDB全体で見た合計接続数は、次のようになります。

DB合計接続数 = レプリカ数 × maximumPoolSize + 管理ツール・監視・バッチ等の接続数

Deployment数を1から5に増やすということは、暗黙のうちにDBへの最大接続数も5倍にしているということです。ここを意識せずにプールサイズだけを個別チューニングしていても、いつまで経っても根本原因にはたどり着けません。

max_connectionsからPodあたりのmaximumPoolSizeを逆算する

考え方はシンプルで、次の不等式を満たすように maximumPoolSize を決めます。

レプリカ数 × maximumPoolSize + 予約接続数 ≤ max_connections

予約接続数には、監視ツールやマイグレーションジョブ、管理者が手動で張る接続なども含めておきましょう。まずはDB側の現在値を確認します。

-- PostgreSQL
SHOW max_connections;
SELECT count(*) FROM pg_stat_activity;

例えば max_connections=200、予約接続数を20、レプリカ数を5とすると、Podあたりの上限は次のように計算できます。

(200 - 20) / 5 = 36

ここで出てきた数値が小さすぎる場合(例えば5や6になってしまうような場合)は、レプリカ数を絞るかDB側のmax_connectionsの見直しを検討したほうが健全です。無理にプールサイズを削ってもリクエストの取りこぼしが増えるだけなので、根本的なキャパシティ設計に立ち返りましょう。

HPAのレプリカ数変動を考慮した安全マージンを取る

固定台数で運用しているなら上の計算だけで十分ですが、HPAを使っている場合は「今のレプリカ数」ではなく maxReplicas を基準に逆算するのが鉄則です。現在のレプリカ数を基準にしてしまうと、スケールアウトした瞬間に上限を超えてしまいます。

さらに厄介なのが、スケールの過渡期です。

  • スケールアウト時: 新しいPodが起動してから実際にDBへ接続を張るまでの間も、既存Podは通常どおり接続を張り続けている
  • スケールイン時: 終了するPodのアイドル接続がすぐには切断されず、しばらく残留することがある

こうした過渡期の揺れを吸収するために、計算結果に対して10〜20%程度の余裕を持たせておくと安心です。ただし maxReplicas を安易に大きくすると、その分Podあたりに割り振れる接続数が減ってしまうトレードオフがあるので、闇雲に上げるのは禁物です。

application.ymlでの実装例

逆算した数値は、環境ごとに分けて application.yml に反映します。

spring:
  datasource:
    hikari:
      maximum-pool-size: 30
      minimum-idle: 10
      connection-timeout: 20000
      validation-timeout: 5000

connection-timeout はプールが枯渇したときにリクエストを待たせる上限時間です。レプリカ環境では1Podあたりのプールが小さめになりやすいぶん枯渇しやすいので、呼び出し元のタイムアウト設定と矛盾しない範囲で値を決めておきましょう。接続設定の基礎部分は MySQL接続設定のガイド も参考にしてください。

PgBouncerなどコネクションプーラーで緩和する

アプリ側の逆算だけではどうしても余裕がない、という場合はDBの手前にPgBouncerのようなコネクションプーラーを挟む選択肢もあります。PgBouncerを使うと、アプリ側の合計プールサイズとDBへの実接続数を分離できるため、Pod数を増やしてもDB側の実接続数を一定範囲に抑えられます。

ただしトランザクションプーリングモードを使う場合は、prepared statementのキャッシュがコネクションをまたいで無効になる可能性があるなど、HikariCP側でも注意が必要です。PgBouncerを挟んだからといって maximumPoolSize を無制限にしていいわけではなく、あくまで緩和策として位置づけて、引き続きアプリ側でも適切な上限を設定しておきましょう。

まとめ

Kubernetesでレプリカを増やしてから発生する「too many connections」は、単一インスタンス前提のプールサイズ計算をそのまま横展開してしまったことが原因であるケースがほとんどです。

見直すときは、次の順番でチェックしてみてください。

  • DBのmax_connectionsを確認したか
  • 現在のレプリカ数ではなくHPAの maxReplicas を基準に逆算したか
  • 予約接続数(監視・管理ツール等)を計算に含めたか
  • 過渡期の揺れを吸収する安全マージンを確保したか
  • それでも厳しければPgBouncerのようなプーラーを検討したか

単一インスタンスでの基本的なチューニングについては HikariCPプールサイズチューニングガイド を、アプリケーション側のスレッド数とプールサイズの整合を取りたい場合は Tomcatスレッドプールのチューニングガイド を、接続検証エラーで悩んでいる場合は HikariCPのfailed to validate connectionトラブルシューティング も合わせて確認してみてください。