Tomcat が2週間でフリーズする問題を解決した話
はじめに
Spring Boot + Spring Data JPA で構築したバッチ処理が、テストフェーズ(ロングランテスト)で約2週間稼働させると Tomcat がフリーズするという問題に遭遇しました。
調査した結果、IN 句を使ったクエリが原因でメモリを圧迫していることがわかり、Hibernate の設定を1つ追加することで改善しました。同じような問題で困っている方の参考になればと思います。
環境
項目バージョン / 構成Java8Spring Boot2.7.4Spring Data JPA2.7.3サーバーAWS EC2 (Amazon Linux 2)AP サーバーTomcatDBAWS 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〜223〜445〜889〜161617〜3232
たとえば、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行で改善できるので、似たような症状が出たら試してみてください
参考
https://www.baeldung.com/java-hibernate-in-clause-padding
https://vladmihalcea.com/improve-statement-caching-efficiency-in-clause-parameter-padding/