負荷試験をかけたら突然 Connection is not available, request timed out が大量に出てきた、なんて経験はないでしょうか。原因の多くは HikariCP の設定をデフォルトのまま本番に出してしまったこと にあります。

Spring Boot はデフォルトで HikariCP を使いますが、そのデフォルト値は「動くこと」を優先した保守的な設定です。本番のトラフィックには必ずチューニングが必要になります。この記事では、主要パラメータの意味と適切な値の導き方を順に見ていきましょう。

なぜ HikariCP か

Spring Boot 2.0 以前はデフォルトのコネクションプールが Tomcat JDBC でした。2.0 から HikariCP に変更されたのは、ベンチマーク上の圧倒的な速さと、コードの軽量さが主な理由です。内部的にロックを最小化した設計になっていて、高スループットな環境でも低レイテンシを維持できます。

ただし「速い」からといって「設定不要」ではありません。むしろ デフォルトは本番向けではない という前提で臨むのが正解です。

コネクションプールが枯渇する仕組み

DB 接続の生成は意外とコストがかかります。TCP 接続を張り、認証を通し、セッションを初期化する一連の処理は、SQL 実行そのものより遅いこともあります。コネクションプールはその接続をあらかじめ作っておき、リクエストが来るたびに貸し出す仕組みです。

問題は、貸し出した接続がなかなか返ってこない状況、つまり スロークエリや接続リーク があると、プールが空になってしまうことです。新たなリクエストは connectionTimeout の時間だけ待たされ、それを過ぎるとタイムアウト例外になります。これが「DB接続の枯渇」です。

主要パラメータを押さえる

設定できるパラメータはたくさんありますが、まず以下の6つを理解しておけば大半のケースに対応できます。

パラメータデフォルト値役割
maximumPoolSize10プールが保持する最大接続数
minimumIdlemaximumPoolSize と同値アイドル時に維持する最小接続数
connectionTimeout30000ms接続取得の最大待機時間
idleTimeout600000msアイドル接続を破棄するまでの時間
maxLifetime1800000ms接続の最大生存時間
keepaliveTime0(無効)接続の死活確認間隔

minimumIdle は HikariCP 公式が「maximumPoolSize と同じ値にして固定プールとして使うことを推奨する」と明示しています。アイドル時の接続数を変動させると管理が複雑になるためです。

maximumPoolSize の正しい算出方法

「とりあえず大きくすれば安心」と思いがちですが、これは逆効果になることがあります。HikariCP の公式 wiki「About Pool Sizing」には次の式が掲載されています。

connections = (core_count * 2) + effective_spindle_count

effective_spindle_count は回転ディスクの数で、SSD や RDS の場合は 1 と見なすのが一般的です。たとえば 4 コアのアプリサーバーであれば 4 * 2 + 1 = 9、つまり 10 前後が出発点 です。

ただし、これはあくまで目安です。実際にはアプリの同時実行スレッド数を基準にします。Tomcat のデフォルト最大スレッド数は 200 ですが、すべてのスレッドが同時に DB アクセスするわけではありません。実測してプールの使用率(hikaricp.connections.active)を見ながら調整するのが現実的です。

もうひとつ忘れてはいけないのが DB 側の max_connections です。アプリサーバーを複数台動かしている場合、インスタンス数 × maximumPoolSize が DB の上限を超えると DB 側で接続拒否されます。全体の接続数を俯瞰して設計しましょう。

connectionTimeout と idleTimeout の設定ミスで起きる障害

connectionTimeout が短すぎる場合 は、高負荷時に正常なリクエストまで即タイムアウトしてしまいます。逆に 長すぎる場合 は、スレッドが長時間ブロックされてサーバー全体が詰まります。デフォルトの 30 秒はほとんどのケースで長すぎるので、3〜5 秒程度に絞るのがおすすめです。

idleTimeout と maxLifetime については、DB 側の接続タイムアウト設定(MySQL なら wait_timeout、デフォルト 8 時間)との関係に注意が必要です。maxLifetime を DB の wait_timeout より長くすると、DB 側がすでに切断した接続を HikariCP がまだ保持してしまい、次にその接続を使ったときに SQLException が発生します。

目安として、maxLifetime は DB の wait_timeout より 数十秒短く 設定します。RDS の場合は wait_timeout が 8 時間なので、デフォルトの 30 分でも問題ないことが多いですが、ファイアウォールで TCP タイムアウトが短い環境では keepaliveTime も併用しましょう。

application.yml への設定例

最小限の設定はこれだけです。まず maximumPoolSizeconnectionTimeout だけでも見直しましょう。

# 最小構成
spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      connection-timeout: 3000

本番環境向けの推奨構成はこちらです。環境変数で上書きできるようにしておくと便利です。

# 推奨構成
spring:
  datasource:
    url: ${DB_URL}
    username: ${DB_USER}
    password: ${DB_PASSWORD}
    hikari:
      # コア数・スレッド数・DB max_connectionsを元に算出
      maximum-pool-size: ${HIKARI_MAX_POOL_SIZE:20}
      minimum-idle: ${HIKARI_MAX_POOL_SIZE:20}
      # 高負荷時に正常リクエストを巻き込まない上限
      connection-timeout: 3000
      # DBのwait_timeout(例:28800s)より短く設定
      max-lifetime: 1800000
      # アイドル接続は10分で破棄
      idle-timeout: 600000
      # ファイアウォール環境では切断検知のため有効化
      keepalive-time: 60000
      pool-name: MyAppPool

設定キーは Spring Boot 2系・3系ともに spring.datasource.hikari.* で変わりません。

設定前後の比較

同時接続 100、うち 20% がスロークエリ(平均 2 秒)という負荷シナリオで比較した結果のイメージです。

設定maximumPoolSizeconnectionTimeoutタイムアウトエラー率
デフォルト1030000ms約 40%
調整後203000ms約 3%
過大設定1003000ms約 8%(スループット低下)

過大にすると DB 側のスレッド競合やコンテキストスイッチが増え、逆にスループットが落ちます。「大きければ安全」ではない、というのが HikariCP 公式の主張でもあります。

Actuator でプール使用状況を監視する

チューニング後は、プールが実際にどう動いているかを確認しましょう。spring-boot-actuatormicrometer-core を依存に追加すると、HikariCP のメトリクスが公開されます。

<!-- pom.xml -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
# メトリクスエンドポイントを有効化
management:
  endpoints:
    web:
      exposure:
        include: metrics

/actuator/metrics/hikaricp.connections.active で現在使用中の接続数、hikaricp.connections.pending で待機中のスレッド数が確認できます。待機が常時発生しているようなら maximumPoolSize を増やすサインです。Prometheus と Grafana を使った詳細な可視化は Spring BootのObservability設定(Micrometer+Prometheus+Grafana) を参考にしてください。

設定見直しチェックリスト

自プロジェクトの設定を確認する際は、以下を一通り見直してみてください。

  • maximumPoolSize をデフォルト(10)のまま本番に出していないか
  • maxLifetime が DB の wait_timeout より長くなっていないか
  • connectionTimeout が 30000ms(デフォルト)のままになっていないか
  • Tomcat の max-threadsmaximumPoolSize のバランスは取れているか
  • 複数インスタンス構成で インスタンス数 × poolSize が DB の max_connections を超えていないか

まとめ

HikariCP のデフォルト設定は開発用途には十分ですが、本番トラフィックには必ず見直しが必要です。まず maximumPoolSizeconnectionTimeout を実態に合わせて調整し、Actuator でメトリクスを見ながら少しずつ詰めていくのが安全な進め方です。

JPA のクエリ最適化については Spring Data JPAのパフォーマンス最適化、Virtual Threads との組み合わせについては Java 21仮想スレッドとSpring Boot も合わせて読んでみてください。コネクションプールの設定が落ち着いたら、次はトランザクション管理も整理しておくと障害の切り分けが楽になりますよ。