本番環境のダッシュボードを見ていたら、HikariCPのアクティブコネクション数がじわじわ増えていって、ある日ついに Connection is not available, request timed out で落ちた、という経験はありませんか。

maximumPoolSize を増やしても一時的に延命するだけで、結局また同じペースで増えていくなら、それはプール不足ではなくコネクションリークです。この記事では leakDetectionThreshold を使ってリーク箇所を特定し、try-with-resources で直すまでの手順をまとめます。プールサイズ自体のチューニングは HikariCPチューニング記事 に、Failed to validate connection のような接続検証エラーは 別のトラブルシューティング記事 に譲ります。

この症状はコネクションリークかもしれない

見分け方はシンプルです。アクティブコネクション数のグラフが右肩上がりで、負荷が下がっても戻らないなら、それはリークです。単純な高負荷によるプール不足なら、トラフィックが落ち着けば数値も自然に下がります。

  • アクティブコネクション数が一方向に増え続ける
  • 深夜の低負荷時間帯でも数値が減らない
  • 最終的に HikariPool-1 - Connection is not available, request timed out が出る
  • プールサイズを上げても「いつか枯渇する」が先に延びるだけ

ここまで当てはまるなら、次はコードのどこかでコネクションを返却し忘れている可能性を疑いましょう。

leakDetectionThresholdとは何か

leakDetectionThreshold は、コネクションがプールに返却されずに一定時間以上保持され続けた場合に警告ログを出す仕組みです。単位はミリ秒で、デフォルトの 0 は無効を意味します。

あくまで「返却が遅い」ことの検知であり、実際にはリークしていなくても、長めのバッチ処理やレポート生成のような長時間トランザクションで誤検知されることがあります。閾値を決める際はその点も踏まえておきましょう。

本番でleakDetectionThresholdを設定する

設定自体は application.yml に1行追加するだけです。

spring:
  datasource:
    hikari:
      leak-detection-threshold: 30000 # 30秒

2000ms未満の値は無視される点に注意してください。目安としては、想定している最大トランザクション時間の数倍を設定するのが安全です。たとえば通常のトランザクションが数秒で終わるなら30秒〜60秒あたりから始めて、様子を見ながら調整するとよいでしょう。

スタックトレースの取得にはわずかにオーバーヘッドがかかりますが、本番でリークに気づけない方がダメージが大きいので、常時有効にしておく価値は十分にあります。

警告ログの読み方

閾値を超えると、次のようなログが出力されます。

WARN  com.zaxxer.hikari.pool.ProxyLeakTask - Connection leak detection triggered for
org.postgresql.jdbc.PgConnection@4d4a8e5f on thread http-nio-8080-exec-3,
stack trace follows
java.lang.Thread.getStackTrace(Thread.java:1610)
com.zaxxer.hikari.pool.ProxyLeakTask.<init>(ProxyLeakTask.java:62)
com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:197)
com.example.order.OrderRepository.findPendingOrders(OrderRepository.java:42)
...

ここで記録されるスタックトレースは、 コネクションを取得した時点 のものです。「その後どこで止まっているか」ではなく「どこで getConnection() を呼んだか」が分かるログだという点を押さえておいてください。

もしリーク検出後にコネクションが返却されると、Previously reported leaked connection ... was returned というフォローアップのログが出ます。これが出ればそのコネクションは最終的に解放されているので、単に閾値設定が厳しすぎた誤検知の可能性も考えましょう。

このログだけでは「取得後にどこで止まっているか」までは分からないので、次のステップでスレッドダンプを取得します。

スレッドダンプを取得する

リーク検出ログが出た直後、コネクションがまだ保持されている間にスレッドダンプを取るのが理想です。

jstack <pid> > threaddump.txt

コンテナ環境で jstack が使いづらい場合は jcmd でも同じことができます。

jcmd <pid> Thread.print > threaddump.txt

一回だけでなく、数秒おきに複数回取っておくと、次のステップでの突き合わせがやりやすくなります。

ログとスレッドダンプを突き合わせてリーク箇所を特定する

手順はこうなります。

  1. リーク検出ログのスタックトレースから、自社コードのクラス・メソッド名(例: OrderRepository.findPendingOrders)を見つける
  2. スレッドダンプの中から、同じスレッド名、あるいは同じ処理を実行しているスレッドを検索する
  3. そのスレッドが何で止まっているかを確認する。外部API呼び出し、ロック待ち、別のI/O待ちでブロックしているケースが多い
  4. 複数回のダンプを比較して、同じ箇所に留まり続けているスレッドを探す

コネクションを握ったまま外部APIのレスポンスを待っている、というパターンは特によく見かけます。この場合、外部API側のタイムアウト設定も合わせて見直す必要があります。

典型的な原因パターン

実際のコードでよく見るパターンは次の4つです。

  • finally 句を書き忘れていて、正常系でしか close() が呼ばれない
  • 例外が発生するパスだけ close() がスキップされる
  • JdbcTemplate を使わず、素の DataSource.getConnection() を手動で取得している
  • メソッド中の早期 return や break で、後続の close() 処理を飛ばしてしまう
// リークするコード
public void leakyMethod() throws SQLException {
    Connection conn = dataSource.getConnection();
    PreparedStatement stmt = conn.prepareStatement("SELECT * FROM orders WHERE id = ?");
    stmt.setLong(1, orderId);
    ResultSet rs = stmt.executeQuery();
    if (!rs.next()) {
        return; // ここでconnが閉じられないままreturnしてしまう
    }
    // ...
    conn.close();
}

try-with-resourcesによる修正

finally句を手書きするより、try-with-resources を使う方が安全です。閉じ忘れが構造的に起こらなくなります。

// 修正後
public void fixedMethod() throws SQLException {
    try (Connection conn = dataSource.getConnection();
         PreparedStatement stmt = conn.prepareStatement("SELECT * FROM orders WHERE id = ?")) {
        stmt.setLong(1, orderId);
        try (ResultSet rs = stmt.executeQuery()) {
            if (!rs.next()) {
                return; // ここでreturnしてもConnection/Statement/ResultSetは自動でcloseされる
            }
            // ...
        }
    }
}

早期returnや例外が発生しても、リソースは必ずcloseされます。素のJDBCを書くなら、これを標準の書き方にしてしまうのがいちばん確実です。

Spring管理下のデータアクセスではリークが起きにくい理由

そもそも JdbcTemplate や Spring Data JPA を使っていれば、この手のリークはかなり起きにくくなります。

  • JdbcTemplate はコネクションのライフサイクルをフレームワークが管理していて、都度 close() を書く必要がありません
  • Spring Data JPA / Hibernateの EntityManager も、トランザクション境界でコネクションが解放されます
  • MyBatisの SqlSessionTemplate も同様に、コネクション管理をフレームワーク側に委譲しています

どうしても素のJDBCを使う必要がある場面、たとえばバルク処理や独自のバッチ処理などでは、必ず try-with-resources を使う方針にしておきましょう。

再発防止と関連トピックへの誘導

リーク検出自体は対処できても、「どのくらいプールサイズに余裕を持たせるか」「平常時からどう監視するか」は別の話です。

まとめ

アクティブコネクション数が増え続ける症状を見たら、まずプールサイズを増やすのではなく leakDetectionThreshold を設定してみてください。ログに出るスタックトレースは「取得時点」のものなので、スレッドダンプと突き合わせて「今どこで止まっているか」を確認するのが特定の近道です。原因の多くは地味な close() 漏れなので、見つかったら try-with-resources で機械的に直してしまいましょう。