HikariCPのプールサイズチューニング や Kubernetesレプリカ数からの逆算 は済ませた、という方も多いと思います。でも、プライマリ/レプリカ構成やマルチDB構成でDataSourceが2つ3つに増えた瞬間、この式だけでは足りなくなります。
この記事では、AbstractRoutingDataSourceによる読み書き分離の実装方法 には触れません。あくまで「DataSourceが複数あるとき、各 maximumPoolSize をどう決めるか」という計算の部分に絞って解説します。
複数DataSourceで起きるmax_connections超過というもう一つの罠
プライマリ/レプリカ構成にした途端、HikariPoolが1つから2つに増えます。マルチDB構成ならさらに増えますよね。ここで厄介なのは、「DataSourceが増える」という変化が、実は2つの全く違うパターンを含んでいることです。
1つは、プライマリとレプリカが別々のDBサーバーで動いていて、それぞれ独立した max_connections を持つパターン。もう1つは、同じDBサーバー上に複数のデータベースがあり、max_connections をアプリ全体で共有しているパターンです。この2つを区別せずに「DataSourceが増えた分だけ予算を分け合おう」と考えてしまうと、計算が破綻します。
単一DataSourceの逆算式をおさらいする
単一DataSourceの場合の逆算式はシンプルで、次のようになります。
レプリカ数 × maximumPoolSize + 予約接続数 ≤ max_connections
この式の導出は Kubernetesレプリカスケーリングの記事 に詳しく書いたので、気になる方はそちらを見てください。ここで大事なのは、この式が「対象となるDBサーバーは1台」という前提に乗っているという点です。
複数DataSourceでは「予算を共有しているか」が最初の分岐点
プライマリ/レプリカ構成 (別々のDBサーバー、それぞれ独立した max_connections) の場合、実はそれほど難しくありません。各DataSourceごとに単一DataSourceの式をそのまま個別に適用するだけです。
プライマリ: Podレプリカ数 × primaryPoolSize + 予約分 ≤ primaryのmax_connections
レプリカ : Podレプリカ数 × replicaPoolSize + 予約分 ≤ replicaのmax_connections
たとえばプライマリが max_connections=100、レプリカが max_connections=50 なら、プライマリ用プールは (100-10)/3 ≈ 30、レプリカ用プールは (50-5)/3 ≈ 15 と、それぞれ独立に計算します。両者を足し合わせて1つの予算に収める必要はありません。
一方、マルチDB構成 (同じDBサーバー上の複数データベース) では、max_connections はサーバー単位で設定されるため、そのサーバーに接続する全DataSourceが1つの予算を共有します。ここが、本当に配分計算が必要になる場面です。
Σ(maximumPoolSize) × レプリカ数が予算を超えないようにする
マルチDB構成の核になる式はこうです。
Σ(同じDBサーバーを使う各DataSourceのmaximumPoolSize) × Podレプリカ数 + 予約接続数 ≤ max_connections
単一DataSourceの式との違いは、maximumPoolSize が単一の値ではなく、同じサーバーを共有する全DataSourceの合計値になっている点だけです。まず「合計プールサイズ」という1つの変数として予算を確保し、そのあとで個々のDataSourceに配分する、という2段階で考えます。
DataSourceごとの配分方針を決める
合計プールサイズが決まったら、それをどのDataSourceにどれだけ割り振るかを決めます。
- 用途(トランザクション系DBか集計系DBかなど)ごとのアクセス頻度で重みを決めます
- 重みは勘で決めず、スロークエリログやAPMで実際のクエリ比率・待ち時間を計測してから当てはめましょう
- 一律に等分するのは避けましょう。最も混雑するDataSourceが先に枯渇し、他のDataSourceには余裕が残っているという、いびつな状態になりがちです
予約接続分を正しく見積もる
ここで1点、誤解しやすいポイントがあります。minimumIdle は maximumPoolSize の範囲内で維持される下限値であり、予算に追加で乗ってくるものではありません。公式Wikiでもデフォルトは maximumPoolSize と同値とされていて、プール全体の上限を超えて接続を確保する設定ではないんですよね。
予約分として見積もるべきは、監視エージェントやマイグレーションツール(FlywayやLiquibaseなど)が使う管理用接続です。たとえば監視エージェントが常時3接続、マイグレーション実行時に一時的に2接続、といった具体的な内訳を積み上げます。目安としては、DB予算全体の10%程度を予約分として確保しておくと安心です。
具体的な計算手順をステップで追う
マルチDB構成で、orders_db (トランザクション系) と analytics_db (集計系) が同じDBサーバー上にあり、Podレプリカが3台、max_connections=100 というケースで計算してみます。
Step1: DBの予算を確定する
max_connections = 100
予約接続数(監視エージェント3 + マイグレーション時2) = 5
実質利用可能な予算 = 100 - 5 = 95
Step2: Podレプリカ数で割って1インスタンスあたりの予算を出す
95 / 3 ≈ 31 (1インスタンスあたりのDataSource合計プールサイズ)
Step3: DataSourceごとの重みで配分する
APMで計測したアクセス比率 orders:analytics = 7:3 と仮定
orders_db = 31 × 0.7 ≈ 22
analytics_db = 31 × 0.3 ≈ 9 (22+9=31で予算ちょうど)
重みをかけた結果が小数になる場合は、優先度の高い(トランザクション系など)DataSource側を繰り上げ、優先度の低い側を繰り下げて合計が予算を超えないように調整しましょう。
application.ymlで複数DataSourceのプールサイズを設定する
計算結果は、DataSourceごとに個別の設定ブロックとして反映します。
app:
datasource:
orders:
jdbc-url: jdbc:postgresql://shared-db:5432/orders_db
hikari:
pool-name: orders-pool
# Step3: 95(予算) / 3(レプリカ) × 0.7(orders比率) ≈ 22
maximum-pool-size: 22
minimum-idle: 22
analytics:
jdbc-url: jdbc:postgresql://shared-db:5432/analytics_db
hikari:
pool-name: analytics-pool
# Step3: 95(予算) / 3(レプリカ) × 0.3(analytics比率) ≈ 9
maximum-pool-size: 9
minimum-idle: 9
pool-name を明示しているのは、設定しないと HikariPool-1 のような自動採番名になり、ログを見てもどのDataSourceが詰まっているか識別できないからです。複数DataSourceを扱うなら、この設定はほぼ必須と考えてください。
max_connections超過時に起きる症状と見分け方
配分を誤ると、本番ではこういう形で症状が出ます。
org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection;
nested exception is java.sql.SQLTransientConnectionException: analytics-pool - Connection is not available, request timed out after 30000ms.
ここで注目したいのは、エラーログに pool-name で設定した名前(この例では analytics-pool)が出ている点です。全DataSourceが一律にエラーを出すのではなく、特定のDataSourceだけ接続待ちが発生するケースが多いので、Micrometer/Actuatorでの監視 を入れて hikaricp.connections.pending をDataSourceごとに見比べれば、どちらの予算が圧迫されているかすぐに分かります。
本番投入前のサイジングチェックリスト
実装に移す前に、次の項目を確認しておきましょう。
- プライマリ/レプリカのように別サーバーなのか、マルチDBのように同じサーバーを共有しているのかを区別できているか
- 予算を共有するDataSourceについて、
maximumPoolSize合計 × レプリカ数が予約分を引いた予算に収まっているか - 予約分に
minimumIdleを二重で加算していないか(minimumIdleはmaximumPoolSizeの内側) - 各DataSourceに
pool-nameを設定し、ログで識別できるようになっているか - Podをスケールアウトするときに、この計算を再度やり直す運用手順になっているか
複数DataSourceは「数」ではなく「配分」で考える
固定予算を逆算するという考え方自体は、単一DataSourceのときと変わりません。違うのは、複数DataSourceが同じDBサーバーの予算を共有しているかどうかを見極め、共有している場合だけ配分の重み付けが必要になる、という点です。
単一DataSourceでの基本的なチューニングは HikariCPプールサイズチューニングガイド を、レプリカ数からの逆算式の詳細は Kubernetesレプリカスケーリングの記事 を、読み書き分離そのものの実装方法は 複数DataSourceによる読み書き分離ガイド を合わせて参考にしてみてください。