Tomcat が2週間でフリーズする問題を解決した話
目次
はじめに
Spring Boot + Spring Data JPA で構築したバッチ処理が、テストフェーズ(ロングランテスト)で約2週間稼働させると Tomcat がフリーズするという問題に遭遇しました。
調査した結果、IN 句を使ったクエリが原因でメモリを圧迫していることがわかり、Hibernate の設定を1つ追加することで改善しました。同じような問題で困っている方の参考になればと思います。
環境
| 項目 | バージョン / 構成 |
|---|---|
| Java | 8 |
| Spring Boot | 2.7.4 |
| Spring Data JPA | 2.7.3 |
| サーバー | AWS EC2 (Amazon Linux 2) |
| AP サーバー | Tomcat |
| DB | AWS Aurora MySQL |
発生した問題
症状
- バッチ処理を開発環境で稼働させていると、約2週間で Tomcat がフリーズする
- フリーズ後はバッチ処理が動かなくなる
- Tomcat を再起動すると復旧するが、また2週間程度で同じ現象が発生
症状をもとにした仮説
徐々にメモリリークしていき、最終的にヒープメモリが枯渇してフリーズしたものと考えられました。
試したこと
メモリリークが原因という仮説を立てたものの、具体的なメモリリーク発生個所の特定がなかなかできませんでした。
- 処理の見直し:不要なオブジェクトを保持していないか、ループ内でのオブジェクト生成を減らせないかなど確認
- EC2 のスペック変更:メモリを増やして様子を見たが、フリーズするまでの時間が延びただけで根本解決にはならず
- GC のチューニング:ヒープサイズやGCの設定を調整したが改善せず
いろいろ試しても解決しなかったため、ウェブ検索で情報を探しました。「Spring Boot メモリリーク」「Tomcat フリーズ 原因」など検索キーワードを変えながら調べ続けて、ようやく IN 句のパラメータ数が原因になりうるという情報にたどり着きました。
原因
調査の結果、IN 句を含むクエリの種類が多すぎることが原因でした。
何が起きていたか
バッチ処理で Spring Data JPA の findByIdIn() のようなメソッドを使っていました。
// 例:IN句のサイズが毎回異なる
List<Entity> result1 = repository.findByIdIn(List.of(1, 2, 3)); // 3個
List<Entity> result2 = repository.findByIdIn(List.of(1, 2, 3, 4)); // 4個
List<Entity> result3 = repository.findByIdIn(List.of(1, 2, 3, 4, 5)); // 5個
このとき、IN 句に渡すリストのサイズが毎回異なると、サイズごとに別の SQL 文として扱われます。
-- 3個の場合
SELECT * FROM entity WHERE id IN (?, ?, ?)
-- 4個の場合
SELECT * FROM entity WHERE id IN (?, ?, ?, ?)
-- 5個の場合
SELECT * FROM entity WHERE id IN (?, ?, ?, ?, ?)
Hibernate はクエリごとにパース結果をメモリ上に保持します。IN 句のサイズが 1〜1000 まで変動するような処理だと、1000種類のクエリがメモリに溜まっていくことになります。
今回のバッチ処理では IN 句に渡す件数がバラバラだったため、これが積み重なってメモリを圧迫していました。
解決策
in_clause_parameter_padding を有効にする
Hibernate には、IN 句のパラメータ数を揃えてくれるオプションがあります。
application.properties に追加
spring.jpa.properties.hibernate.query.in_clause_parameter_padding=true
このオプションの動き
IN 句のパラメータ数を、次の切りのいい数に揃えてくれます。
| 実際のパラメータ数 | 揃えた後 |
|---|---|
| 1〜2 | 2 |
| 3〜4 | 4 |
| 5〜8 | 8 |
| 9〜16 | 16 |
| 17〜32 | 32 |
たとえば、3件渡すと4件用の SQL になり、5件渡すと8件用の SQL になります。足りない分は同じ値で埋めてくれます。
このようにすることで、パラメータ数がバラバラでも生成される SQL の種類が限定され、メモリの消費を抑えられます。
結果
この設定を追加した後、2週間以上稼働させても Tomcat がフリーズしなくなりました。
コードの修正は不要で、設定ファイルに1行追加するだけで解決できたのは助かりました。
注意点
- このオプションは Hibernate 5.2.18 以降で使えます
- 足りないパラメータは同じ値で埋めてくれるので、SQL のログを見ると少し違和感があるかもしれません
- 適用前後でクエリの実行結果が変わることはありません
まとめ
- Spring Data JPA で IN 句を使う処理が多いと、クエリの種類が増えてメモリを圧迫することがあります
in_clause_parameter_padding=trueを設定すると、クエリの種類を減らせます- 設定1行で改善できるので、似たような症状が出たら試してみてください

