負荷試験をかけたら突然 Connection is not available, request timed out が大量に出てきた、なんて経験はないでしょうか。原因の多くは HikariCP の設定をデフォルトのまま本番に出してしまったこと にあります。デフォルト値は「動くこと」を優先した保守的な設定なので、本番トラフィックにはチューニングが必要です。この記事では、主要パラメータの意味と適切な値の導き方を順に見ていきましょう。

なお、クエリ側の改善は Spring Data JPAのパフォーマンス最適化 を、複数 Pod で動かす場合の接続数設計は Spring BootアプリをKubernetesにデプロイする方法 を合わせて参照してください。

「とりあえず動く設定例だけ見たい」という方は application.yml への設定例 まで飛んでください。

HikariCP とは

HikariCP は Java 向けの JDBC コネクションプール 実装です。名前は日本語の「光」に由来し、高速・軽量であることが特徴です。Spring Boot 2.0 以降のデフォルトとして採用されており、spring-boot-starter-jdbcspring-boot-starter-data-jpa を依存に加えると com.zaxxer:HikariCP が推移的に取り込まれます。

コネクションプールとは、DB への接続をあらかじめ一定数だけ作成して保持し、アプリケーションに貸し出して使い回す仕組みです。リクエストのたびに TCP 接続と認証をやり直すコストを省けるだけでなく、アプリから DB に張る接続数の上限を制御する 役割も担います。HikariCP における Connection.close() は物理的な切断ではなく「プールへの返却」を意味する、という点を押さえておくと後述のリークの話が理解しやすくなります。

スロークエリや接続リーク で貸し出した接続が返ってこないとプールは空になり、新たなリクエストは connectionTimeout だけ待たされてタイムアウト例外になります。これが冒頭の Connection is not available、いわゆる「DB接続の枯渇」の正体です。

Spring Boot では自動設定される

Spring Boot では spring.datasource.url / username / password を書くだけで HikariDataSource の Bean が自動構成され、プールの挙動は spring.datasource.hikari.* プロパティで調整できます。JDBC ドライバ(mysql-connector-jpostgresql)は別途依存に追加してください。

Spring を使わず素の Java で HikariCP を使う

HikariCP 自体は Spring に依存しないライブラリなので、単体の Java アプリやバッチからも利用できます。

<!-- pom.xml (Spring Boot 外で使う場合はバージョンを明示。Maven Central で最新版を確認) -->
<dependency>
  <groupId>com.zaxxer</groupId>
  <artifactId>HikariCP</artifactId>
  <version>6.3.0</version>
</dependency>
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/app");
config.setUsername("app");
config.setPassword("secret");
config.setMaximumPoolSize(20);
config.setConnectionTimeout(3000);
config.setPoolName("MyAppPool");

HikariDataSource dataSource = new HikariDataSource(config);

try (Connection conn = dataSource.getConnection();
     PreparedStatement ps = conn.prepareStatement("SELECT 1")) {
    ps.executeQuery();
} // close() でプールに返却される(物理切断ではない)

// アプリ終了時にプールごと閉じる
dataSource.close();

HikariConfig のセッター名は Spring Boot のプロパティ名と 1 対 1 で対応しており(setMaximumPoolSizemaximum-pool-size)、パラメータの意味はどちらの使い方でも同じです。

起動ログの「HikariPool-1」とは何か

Spring Boot アプリを起動すると、次のようなログが出力されます。

com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
com.zaxxer.hikari.pool.HikariPool  : HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@1a2b3c
com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.

HikariPool-1pool-name を指定しなかったときに自動採番されるプール名で、DataSource が複数あれば HikariPool-2 と増えていきます。覚えておきたいログは 3 種類です。

  • Start completed. はプール初期化の完了です。プールは 最初に接続が要求された時点 で初期化されるため、構成によっては起動直後ではなく最初のリクエスト到達時に出ることがあります。
  • Exception during pool initialization. は 1 本も接続できなかった状態です。JDBC URL や認証情報の誤り、DB 側の未起動が原因のほとんどで、プールサイズの問題ではありません。
  • Shutdown initiated... はアプリ終了時のログです。この直前で処理中リクエストがエラーになるなら、Spring Bootのグレースフルシャットダウンとゼロダウンタイムデプロイを実現する方法 で graceful shutdown の設定を確認してください。

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

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

maximumPoolSize のデフォルト値と推奨値

デフォルト値: 10。推奨値: (コア数 × 2) + 1 を出発点に、Tomcat の max-threads や DB 側の max_connections を踏まえて 10〜30 程度。プロパティ名は spring.datasource.hikari.maximum-pool-size です。

minimumIdle のデフォルト値と推奨値

デフォルト値: maximumPoolSize と同値。推奨値: maximumPoolSize と同じ値で固定プールとして運用。HikariCP 公式が明示的に「動的に変動させないこと」を推奨しています。プロパティ名は spring.datasource.hikari.minimum-idle

connectionTimeout のデフォルト値と推奨値

デフォルト値: 30000ms (30 秒)。推奨値: 3000〜5000ms。プロパティ名は spring.datasource.hikari.connection-timeout

idleTimeout のデフォルト値と推奨値

デフォルト値: 600000ms (10 分)。推奨値: デフォルトのままで問題ないことが多い。minimumIdle == maximumPoolSize の場合は無視されます。プロパティ名は spring.datasource.hikari.idle-timeout

maxLifetime のデフォルト値と推奨値

デフォルト値: 1800000ms (30 分)。推奨値: DB の wait_timeout より数十秒短く。MySQL の wait_timeout デフォルト 8 時間に対し、30 分は十分短いので基本そのまま。プロパティ名は spring.datasource.hikari.max-lifetime

keepaliveTime のデフォルト値と推奨値

デフォルト値: 0 (無効)。推奨値: ファイアウォール経由で TCP セッションが切られる環境では 30000〜60000ms (30〜60 秒) を設定。maxLifetime より短く、かつ 30 秒以上である必要があります。プロパティ名は spring.datasource.hikari.keepalive-time

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 の max-threads と同じ値にする必要はありません。最大スレッド数 200 に対しプールが 20 でも、DB 側で競合させるよりアプリ側のプールで短く待たせる方がスループットは出る、というのが HikariCP 公式の設計思想です。実測でプールの使用率(hikaricp.connections.active)を見ながら調整しましょう。

もうひとつ忘れてはいけないのが DB 側の max_connections です。インスタンス数 × maximumPoolSize(複数プールがあるなら全プールの合計)が DB の上限を超えると接続拒否されるため、全体の接続数を俯瞰して設計しましょう。

なお Java 21 の仮想スレッドを有効にしても、DB 側の処理能力は変わらないためこの算出式はそのままです。むしろ大量の仮想スレッドがプールで待つ形になるので、connectionTimeout と後述の pending の監視が重要になります。詳しくは Java 21仮想スレッドとSpring Boot を参照してください。

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

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

maxLifetime は DB 側の接続タイムアウト設定(MySQL なら wait_timeout)より長くすると、DB 側がすでに切断した接続を HikariCP がまだ保持してしまい、次に使ったときに SQLException が発生します。DB の wait_timeout より 数十秒短く 設定するのが目安です。ファイアウォールで 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.* で変わりません。パスワードはこの例のように環境変数で渡すのが第一候補ですが、設定ファイルに書かざるを得ない場合は Spring BootでJasyptを使って設定ファイルの機密情報を暗号化する方法 で解説している ENC() 形式の暗号化が手軽です。

設定前後の比較

同時接続 100、うち 20% がスロークエリという負荷シナリオでの比較イメージです。

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

過大にすると DB 側のスレッド競合が増え、逆にスループットが落ちる点にも注目してください。

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

チューニング後は spring-boot-starter-actuator を依存に追加し、Micrometer 経由で自動公開される 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 のように、メトリクス名を URL に付けて確認します。特に見るべきものは次のとおりです。

メトリクス意味見方
hikaricp.connections.active使用中の接続数常に maximumPoolSize に張り付いていればサイズ不足かスロークエリ
hikaricp.connections.idleアイドル接続数常に大きいままならプールが過大
hikaricp.connections.pending接続待ちスレッド数常時 0 より大きいなら要対処
hikaricp.connections.timeout接続取得タイムアウトの累計回数増え続けていれば connectionTimeout 超過が発生中
hikaricp.connections.acquire接続取得にかかった時間p99 が数百 ms なら待ちが常態化
hikaricp.connections.usage接続の借用時間長いほどスロークエリやリークの疑い

待機(pending)が常時発生しているなら maximumPoolSize を増やすサインですが、その前に usage が長い接続がないか、つまりスロークエリやリークが原因でないかを確認してください。複数 DataSource がある場合は pool タグで絞り込めます。Prometheus と Grafana を使った詳細な可視化は Spring BootのObservability設定(Micrometer+Prometheus+Grafana) を参考にしてください。

よくあるエラーとトラブルシューティング

HikariCP まわりで発生する典型的なエラーは、原因のパターンが限られています。

Connection is not available, request timed out after Nms

connectionTimeout の時間内にプールから接続を取得できなかったときに発生します。原因は主に 3 つです。

  1. プールサイズが不足している: hikaricp.connections.active が常に maximumPoolSize に張り付いていれば、サイズ不足です。
  2. スロークエリで接続が長時間占有されている: hikaricp.connections.usage (借用時間) のメトリクスを確認し、SQL ログのスロークエリと突き合わせます。
  3. 接続リーク: アプリケーション側で接続を返却していない可能性。後述の leakDetectionThreshold で検出できます。

接続リークを検出する leakDetectionThreshold

leakDetectionThreshold を設定すると、貸し出された接続が指定時間内に返却されない場合に警告ログを出してくれます。本番では負荷の影響があるため、ステージング環境での利用をおすすめします。

spring:
  datasource:
    hikari:
      # 60秒以上返却されない接続を警告
      leak-detection-threshold: 60000

警告ログにはリークしているコードのスタックトレースが出力されるので、原因コードを特定しやすくなります。

HikariPool-1 - Failed to validate connection

maxLifetime が DB の wait_timeout より長い場合に頻発します。DB 側がすでに切断した接続を HikariCP が再利用しようとして失敗するパターンです。maxLifetime を DB の wait_timeout より短く設定し直してください。ファイアウォールや NAT が一定時間アイドルな TCP セッションを切る環境では、keepaliveTime を 30〜60 秒に設定して切断を事前に検知するのも有効です。

あわせて押さえておきたい設定

最後に、質問されることの多い設定をまとめておきます。

validationTimeout と connectionTestQuery

基本的には 設定不要 です。HikariCP は貸し出し前に JDBC4 の Connection.isValid() で生存確認を行い、その待ち時間の上限が validationTimeout(デフォルト 5000ms)です。connectionTestQuery は JDBC4 非対応の古いドライバ向けの後方互換オプションで、公式も「対応ドライバでは設定しないこと」を推奨しています。validationTimeout を触る場合は必ず connectionTimeout より短くしてください。

initializationFailTimeout で起動順序の制約を緩める

DB が起動していないとアプリが立ち上がらない、という制約は initializationFailTimeout(デフォルト 1ms)で制御できます。0 なら接続に失敗しても起動続行、負の値なら起動時の接続確認をスキップします。DB コンテナの起動を待ちたい場合は -1 が使えますが、本番では設定ミスを早期検知できるデフォルトのままの方が無難です。

MySQL 向けの data-source-properties

HikariCP 自体は PreparedStatement のキャッシュを持たない設計なので、キャッシュは JDBC ドライバ側で有効にします。HikariCP 公式 wiki の MySQL 向け推奨値は次のとおりです。

spring:
  datasource:
    hikari:
      data-source-properties:
        cachePrepStmts: true
        prepStmtCacheSize: 250
        prepStmtCacheSqlLimit: 2048
        useServerPrepStmts: true

内容はドライバにそのまま渡されるため、PostgreSQL では reWriteBatchedInserts: true のように、ドライバ固有のキーを指定します。

複数 DataSource での設定の分け方

DataSource ごとに @ConfigurationProperties のプレフィックスを分け、pool-name も別々に付けます。

@Bean
@ConfigurationProperties("app.datasource.primary")
public DataSourceProperties primaryProperties() {
    return new DataSourceProperties();
}

@Bean
@ConfigurationProperties("app.datasource.primary.hikari")
public HikariDataSource primaryDataSource(DataSourceProperties primaryProperties) {
    return primaryProperties.initializeDataSourceBuilder()
            .type(HikariDataSource.class)
            .build();
}
app:
  datasource:
    primary:
      url: jdbc:mysql://primary:3306/app
      hikari:
        pool-name: PrimaryPool
        maximum-pool-size: 20
    replica:
      url: jdbc:mysql://replica:3306/app
      hikari:
        pool-name: ReplicaPool
        maximum-pool-size: 10

このとき DB 側の max_connections に対しては、全プールの maximumPoolSize の合計 × インスタンス数 で見積もる点に注意してください。

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

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

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

まとめ

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

JPA のクエリ最適化については Spring Data JPAのパフォーマンス最適化 も合わせて読んでみてください。