1. 概要
本プロジェクトでは、船舶のセンサーデータの異常値を検知してアラームを発生させる機能の導入が必要でした。これを実装するため、船舶ごとのセンサーデータを取得し、アラーム発生条件を検知したうえでアラームを発生させる日次バッチロジックを新たに開発しました。
これは開発環境の Kubernetes クラスターに Pod 形式でデプロイされたサービス上で実行され、1日に1回実行することで船舶ごとのアラーム発生統計データを蓄積できると考えていました。しかし予想に反して、ロジックの実行中に Pod が再起動する問題が繰り返し発生しました。
2. 問題の状況
ロジックが実行されるたびに Pod が再起動する原因を確認したところ、OOMKilled(Out Of Memory Killed)、つまりコンテナがメモリ制限を超過し、OOM Killer によって強制終了されていることが分かりました。船舶の大容量データを処理するには Memory Limit の設定が小さすぎるのかと思い、Pod の Memory Limit を増やしてみましたが、それでも問題は解決しませんでした。
結局、Pod が再起動する状況の明確な原因は OOMKilled でしたが、Limit の設定とは直接的な関連がありませんでした。Memory Limit が不足していたわけでもないのに、なぜ Pod は OOMKilled によって強制終了されたのでしょうか。
3. JVM のメモリ:Heap と Non-Heap
JVM メモリは、大きく Heap 領域と Non-Heap 領域に分けて説明できます。
3.1 Heap 領域
Java オブジェクトが格納されるメモリ領域で、Java アプリケーションで new キーワードを使ってオブジェクトを生成すると、そのほとんどが Heap 領域に格納されます。これは GC(Garbage Collector)の主な管理対象であり、参照されなくなったオブジェクトは GC が整理します。
Heap はさらに大きく Young Generation と Old Generation に分けられます。
1) Young Generation
新しく生成されたオブジェクトが最初に格納される領域です。ほとんどのオブジェクトは、生成されてから短時間で使用されなくなります。例えば、リクエスト処理中に一時的に作成される DTO、文字列、コレクションなどが該当します。
Young Generation は Eden 領域と Survivor 領域に分けられ、オブジェクトは通常、最初に Eden 領域で生成されます。Eden 領域がいっぱいになると Minor GC が発生し、Minor GC 後も生き残ったオブジェクトは Survivor 領域へ移動します。
複数回の Minor GC 後も生き残り続けるオブジェクトは長期生存オブジェクトと判断され、Old Generation へ移動します。このプロセスを昇格(Promotion)と呼びます。
2) Old Generation
長期間生存するオブジェクトが格納される領域です。Spring Bean、キャッシュデータ、複数回の Minor GC を通過したオブジェクトなどが Old Generation に残ることがあります。
この領域が不足すると、Full GC または Major GC が発生する可能性があります。Full GC は Young Generation と Old Generation を広範囲に検査するため、Minor GC より負荷が高く、アプリケーションが一時停止する Stop-The-World の時間が長くなる可能性があります。
3.2 Non-Heap 領域
Java オブジェクトが格納される Heap の外部にある JVM メモリ領域です。この領域は GC の主な対象となる一般オブジェクトの格納領域ではありませんが、JVM が Java アプリケーションを実行するために必ず使用するメモリです。Metaspace、Code Cache、Thread Stack、JVM Native Memory などが Non-Heap 領域に含まれます。
4. Heap Size と GC の関係
JVM の Heap Size は、GC の動作方式と密接な関係があります。アプリケーションで生成されたオブジェクトのほとんどは Heap に格納され、GC はこの Heap 領域を対象に、参照されなくなったオブジェクトを整理してメモリを回収します。そのため、Heap Size の設定方法によって、GC が発生するタイミング、頻度、実行時間などが変わる可能性があります。
4.1 Heap Size が小さく設定されている場合
Heap Size が小さく設定されていると、オブジェクトを格納できる領域が早く不足し、JVM はより頻繁に GC を実行するようになります。特に Young Generation の Eden 領域が早くいっぱいになると Minor GC が頻繁に発生し、Old Generation の空き領域が不足すると Full GC が発生する可能性があります。Heap が小さいと GC が頻繁に発生するというデメリットがありますが、1回の GC で検査・整理するメモリ範囲が相対的に小さいため、GC の実行時間は短くなる可能性があります。
4.2 Heap Size が大きく設定されている場合
一方、Heap Size が大きく設定されていると、より多くのオブジェクトを格納できるため、GC の発生頻度が低くなる可能性があります。特に Old Generation 領域が大きくなると、長時間生存するオブジェクトが大量に蓄積しても空き領域不足の状態に達するまで時間がかかり、その結果、Full GC が頻繁に発生しない場合があります。このとき、Heap が大きい状態で Full GC が発生すると、整理すべきメモリ範囲が広くなるため、1回の Full GC にかかる時間が長くなる可能性があります。また、複数の GC Thread を使用して並列処理できる Parallel GC を使用していても、Full GC 中にはアプリケーション Thread が停止する Stop-The-World が発生する可能性があります。
Heap を大きく設定すると Full GC の発生頻度は低くなる可能性がありますが、GC が遅れて発生する間、不要なオブジェクトが Heap に長く残ることがあります。また、Full GC が発生した際には、より広いメモリ領域を対象に整理する必要があるため、GC にかかる時間が長くなる可能性があります。反対に、Heap を小さく設定しすぎると GC が過度に頻繁に発生し、アプリケーションの処理性能が低下する可能性があります。したがって、Heap Size は大きければよい、小さければよいというものではなく、アプリケーションの状況に応じて適切に調整する必要があります。
5. 問題が発生した Pod の JVM 実行オプション
Pod の Memory Limit は 2GB で、JVM 実行オプションでは -XX:MaxRAMPercentage=75 により、コンテナメモリを基準とした Heap の最大比率を 75% に設定していました。単純計算では、Heap は 2GB の 75% にあたる約 1.5GB まで拡大でき、残りの約 0.5GB を Non-Heap および Native 領域に使用できる構成でした。Kubernetes 環境では、これは比較的高い Heap 比率です。Heap 領域が必要以上に大きく設定されたことで Old Generation 領域も相対的に大きく確保され、その結果 Full GC の発生頻度が低下し、OOMKilled が発生したのではないかと考えました。
そこで -XX:MaxRAMPercentage=60 に下げたうえで日次バッチロジックの実行と Pod の状態をモニタリングした結果、Pod が再起動することなくロジックが正常に実行されることを確認しました。JVM が使用できる最大メモリ、割り当てられたメモリのうちの空きメモリ、実際に使用中のメモリをアプリケーションログで確認したところも、以前とは異なり Full GC が正常に実行されていることを確認できました。-XX:MaxRAMPercentage=75 のときは、ロジックの実行中に使用中のメモリが最大メモリに達しても GC が発生せず、Pod が再起動する現象がありました。しかし -XX:MaxRAMPercentage=60 に下げた後にアプリケーションログを確認した結果、以前とは異なり、使用中のメモリが最大メモリに達した際に Full GC が実行され、使用中のメモリが大幅に減少することを確認できました。
6. まとめ
今回の事例を通じて、Kubernetes Pod の OOMKilled に関しては、Memory Limit の設定だけでは問題を解決できないことを学びました。もちろん、適切な Memory Limit を設定することは非常に重要ですが、それに加えて JVM Heap Size の設定も必ず考慮する必要があることを新たに理解しました。
したがって、Kubernetes 環境で Java アプリケーションを運用する際に OOMKilled の問題に直面した場合は、むやみに Memory Limit を増やすのではなく、Heap Size と Non-Heap 領域が適切に設定されているか、GC が正常に発生しているかを併せて確認する必要があります。これにより、不要なリソースの浪費を抑え、より安定した効率的なアプリケーション運用環境を構築できるでしょう。
deeenee