MATを活用したHeapDump分析

MATを活用したHeapDump分析

性能分析の古典的な定番、Heap Dump

K8s環境でプロジェクトを進めている途中、特定のPodが繰り返し再起動されるケースを発見しました。Podがなぜ再起動されるのかを確認するためLast State属性を調べたところ、原因は当然のようにOOMでした。通常、Podがこのように無限に再起動される場合、その主な原因はOOM Killedです。

この状況でOOMを引き起こした原因を探るため、まずLogを確認します。しかし、このようにLogを確認しても、OOMの原因は簡単には見つかりません。なぜなら、OOMの主な原因はErrorではなく、スレッドのボトルネック、つまりDB I/Oに時間がかかりすぎることで非同期処理中に性能ボトルネックが発生することだからです。エラーログが一行もないままメモリだけが徐々に増加し、Podが停止する状況では、ログだけで原因を特定するのは困難です。

このようにLogからOOMの原因を分析できないときに役立つのが、まさにHeap Dumpです。最も伝統的なプロセス分析手法であり、現在でもバックエンドの性能分析で最もよく使われている技術の一つです。Heap Dumpは、ある時点でJVMヒープに存在するすべてのオブジェクトとその参照関係をスナップショットとして記録するため、どのオブジェクトがメモリを占有しているのか、なぜ回収されないのかを静的な形で確認できます。

しかし、純粋なHeap Dumpをそのまま分析するには、ファイル自体がユーザーにとって親切な形式ではなく、簡単には読めないという問題があります。数十万個のオブジェクトと参照がバイナリ形式で複雑に絡み合った元ファイルを、人が直接解釈するのは事実上不可能に近いでしょう。このような場合に役立つのが、Memory Analyzer Tool(MAT)というオープンソースの分析ツールです。

そもそもOOMとは何か

本格的にMATを見ていく前に、私たちが直面したOOM(Out Of Memory)が正確にはどのような現象なのかを確認しておく必要があります。OOMとは文字どおり、アプリケーションが利用できるメモリが枯渇し、新しいオブジェクトを割り当てるための空間がなくなったときに発生する状況を指します。同じメモリ不足でも、それが発生するレイヤーは大きく2つに分かれます。この2つを区別することが、トラブルシューティングの第一歩です。

image1.png

* 画像出典: https://www.cnblogs.com/java1024/p/12381457.html

1つ目は、JVM内部で発生するOutOfMemoryErrorです。JVMは起動時にヒープ(Heap)の最大サイズを決め、その範囲内でオブジェクトを割り当てます。しかし、回収されなかったオブジェクトが蓄積し続けてヒープが限界に達すると、JVMはjava.lang.OutOfMemoryErrorをスローし、正常な動作を停止します。

image2.jpeg

* 画像出典: https://suneeta-mall.github.io/blog/2021/03/14/wth-who-killed-my-pod---whodunit/

2つ目は、コンテナレベルで発生するOOM Killedです。K8s環境ではPodにメモリのLimitが設定されます。コンテナが使用する実際のメモリがこの上限を超えると、ホストのOOM Killerが該当プロセスを強制終了します。このときPodは終了コード137(128 + SIGKILL 9)とともにLast StateにOOMKilledを残し、再起動ポリシーに従って再び起動することを繰り返します。

問題を難しくしているのは、OOMの多くが特定の瞬間に発生する事故ではなく、徐々に進行する蓄積の結果だという点です。処理されなかったオブジェクトが少しずつヒープに蓄積し、Garbage Collectorが回収を試みても、依然としてどこかから強く参照されているため、最終的に回収に失敗します。このように回収されなかったオブジェクトが限界点を超えた瞬間にOOMが発生するため、実際にエラーが発生した時点のログには、真犯人ではなく、最後にメモリを要求して失敗した無実のコードが記録されることが多くあります。結局、OOMの本当の原因を明らかにするには、エラーが発生した瞬間ではなく、その直前までヒープに何がどのように蓄積されていたのかを調べる必要があります。まさにこの点で、Heap Dumpと、それを分析するMATが必要になります。

Memory Analyzer Tool

Memory Analyzer Tool(以下、MAT)は、Eclipse財団が提供するオープンソースのHeap Dump分析ツールです。人間には読みにくい生のHeap Dump(.hprof)を解析し、どのオブジェクトがどれだけ多くのメモリを占有しているのか、そしてそのオブジェクトがなぜGCの対象にならないのかを視覚的に示します。

MATの核心は、Shallow HeapとRetained Heapという2つの指標です。Shallow Heapはオブジェクト自身が占有するメモリサイズを、Retained HeapはそのオブジェクトがGCされた場合に同時に回収されるメモリの総量を意味します。OOMの原因を探す際には、Retained Heapが異常に大きいオブジェクトを追跡することが重要です。単一のオブジェクトが占めるサイズよりも、そのオブジェクトが保持して解放しないメモリの総量が、実質的なリークの規模を示すからです。

image3.png

* 画像出典: AnyLogic Help, “Memory analyzer”, https://anylogic.help/advanced/debug/memory-analyzer.html

このViewは、Heap DumpファイルをMAT(Memory Analyzer Tool)にImportすると確認できます。Memory Analyzer Toolの中核機能を担うViewであり、MATがHeap Dumpを確認してメモリリークが疑われるポイントを示してくれます。円形のグラフによってProcessのHeapに占める割合を表示し、どのデータがHeapを過剰に占有しているのかを確認できます。

image4.png

* 画像出典: AnyLogic Help, “Memory analyzer”, https://anylogic.help/advanced/debug/memory-analyzer.html

また、次のような画面も確認できます。

上の画面は、MATがHeap Dumpを分析した後に自動生成するLeak Suspectsレポートです。MATは単にオブジェクト一覧を並べるだけでなく、異常に多くのメモリを占有しているオブジェクトを「リーク疑い箇所(Leak Suspect)」として指摘して表示します。画面上部の円形グラフは、全体のHeapに対して各疑い対象のオブジェクトが占める割合を視覚的に示します。1つのオブジェクト、または特定クラスのインスタンス群が大部分を占有している場合、その領域が大きな割合で強調されるため、一目で把握できます。

グラフの下部には、各疑い箇所についての概要説明も表示されます。どのクラスがどれほどのRetained Heapを占有しているのか、またそのオブジェクトがどのClassLoaderや参照パスを通じて保持されているのかを、自然言語に近い形で整理してくれます。そのため、Heap Dump分析に慣れていない開発者でも、リーク原因の候補を素早く絞り込めます。本格的なDominator Tree分析に入る前に、最初に確認して分析の方向性を定める出発点として活用しやすい画面です。

Heap Dumpの取得方法

Heap Dumpは、OOMが発生した時点で自動的に生成されるよう設定することも、実行中のプロセスに直接コマンドを実行して取得することもできます。運用環境では、OOMが発生した瞬間の状態を捉えることが最も重要であるため、JVMオプションによる自動ダンプ設定が有効です。

-XX:+HeapDumpOnOutOfMemoryErrorオプションを追加すると、OutOfMemoryErrorが発生した時点で自動的に.hprofファイルが生成され、-XX:HeapDumpPathで保存先のパスを指定できます。実行中のプロセスからすぐに取得する必要がある場合は、jmap -dump:live,format=b,file=heap.hprof <pid>コマンドを使用します。liveオプションを指定すると、ダンプ前にFull GCが1回実行されるため、生存しているオブジェクトだけを対象に分析できます。

ただし、K8s環境では1つ考慮すべき点があります。PodがOOMによってKillされると、コンテナが終了すると同時に内部に生成されたダンプファイルも消えてしまう可能性があることです。そのため、HeapDumpPathをPersistentVolumeなどの永続ストレージのパスに指定するか、別のデバッグコンテナを通じて終了直前のファイルシステムにアクセスする方法も併せて検討する必要があります。

Dominator Treeを活用したリークオブジェクトの追跡

メモリ使用量が継続的に増加していた問題の状況で取得したHeap Dumpを、MATのDominator Treeで開いたところ、特定クラスのインスタンスが異常に多くのRetained Heapを占有していることを確認しました。Dominator Treeは、オブジェクト間の支配関係をツリー形式で表示し、どのオブジェクトが他のオブジェクトのメモリを保持しているのかを一目で把握できるようにします。

本来であれば一定の水準で回収されるはずのオブジェクトが、なぜ蓄積し続けるのかを確認するため、Path to GC Roots機能を利用しました。これは、対象オブジェクトがGC Rootからどのような参照パスでつながっているのかを逆方向に追跡する機能であり、誰がオブジェクトを保持して解放していないのかを正確に突き止められます。分析の結果、処理速度を超えるペースで流入していたメッセージオブジェクトがコレクションに蓄積され続け、消費されないまま強い参照として残っていたため、Garbage Collectorがメモリを回収できない状況になっていました。

結局、非同期処理の過程でDB I/Oのボトルネックによりメッセージの消費速度が生成速度に追いつかず、処理しきれなかったオブジェクトがヒープに無限に蓄積され、OOMにつながっていたのです。幸い、この事実をHeap Dumpの分析過程で発見できました。そして、ビジネスロジックにおけるDB I/Oを改善して処理速度を短縮することで、オブジェクトが無限に蓄積する状況を防ぐことができました。

おわりに

Heap Dumpは、「すでに発生したOOMの犯人は誰か」を最も確実に明らかにしてくれるツールです。ログには現れず、リアルタイムの指標だけでは原因を絞り込むのが難しいメモリリーク問題に直面したとき、その時点のヒープ全体を調べ、オブジェクト参照の絡まりを解きほぐしていく古典的な分析手法だと考えるとよいでしょう。何よりHeap Dump分析は、目の前の障害を解消するだけにとどまらず、オブジェクトがどのようなライフサイクルで生成・消滅するのか、そしてどのような参照関係がメモリの回収を妨げているのかを深く理解するきっかけになります。一度蓄積したこの分析経験は、その後、同様の兆候をより早く察知し、さらにはリークが発生しない構造を設計する洞察へとつながるという点で、大きな価値があると考えています。

jungboke

Site footer