Tomcat が2週間でフリーズする問題を解決した話

はじめに

Spring Boot + Spring Data JPA で構築したバッチ処理が、テストフェーズ(ロングランテスト)で約2週間稼働させると Tomcat がフリーズするという問題に遭遇しました。

調査した結果、IN 句を使ったクエリが原因でメモリを圧迫していることがわかり、Hibernate の設定を1つ追加することで改善しました。同じような問題で困っている方の参考になればと思います。


環境

項目バージョン / 構成
Java8
Spring Boot2.7.4
Spring Data JPA2.7.3
サーバーAWS EC2 (Amazon Linux 2)
AP サーバーTomcat
DBAWS 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〜22
3〜44
5〜88
9〜1616
17〜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行で改善できるので、似たような症状が出たら試してみてください

参考

一覧に戻る

contact

お問い合わせ

サービスに関するお問い合わせはこちら

採用・求人に関する情報はこちら