Анализ HeapDump с использованием MAT

Анализ HeapDump с использованием MAT

Классика анализа производительности, Heap Dump

Во время работы над проектом в среде K8s мы обнаружили случай, когда определённый под перезапускался многократно. Чтобы выяснить, почему под перезапускается, мы рассмотрели свойство Last State, и причиной, конечно, оказался OOM. Обычно случаи бесконечного перезапуска пода в основном вызваны OOM Killed.

Чтобы найти причину OOM в этой ситуации, сначала проверяем логи. Однако при проверке логов причины OOM не так легко обнаружить. Это связано с тем, что основной причиной OOM является не ошибка, а узкое место в потоках, то есть слишком долгое время выполнения DB I/O, что приводит к узкому месту в процессе асинхронной обработки. В ситуации, когда память постепенно заполняется без единой строки в журнале ошибок и под умирает, трудно определить причину, опираясь только на логи.

Когда логи не могут помочь в анализе причин OOM, очень полезным может оказаться Heap Dump. Это одна из самых традиционных методик анализа процессов и одна из наиболее часто используемых технологий для анализа производительности бэкенда по сей день. Heap Dump содержит снимок всех объектов и их ссылочных отношений, существующих в куче JVM в определённый момент времени, что позволяет увидеть, какие объекты занимают память и почему они не очищаются, в статическом виде.

Однако при анализе чистого Heap Dump файл сам по себе не является дружелюбным для пользователя, и возникает проблема с его трудночитаемостью. На самом деле практически невозможно интерпретировать исходный файл, где сотни тысяч объектов и ссылок запутаны в двоичном коде, без помощи. В таких случаях полезным является инструмент анализа Memory Analyzer Tool (MAT), который является открытым исходным кодом.

Так что же такое OOM

Перед тем как подробно рассмотреть MAT, нам нужно понять, что именно представляет собой OOM (Out Of Memory), с которым мы столкнулись. OOM — это ситуация, когда память, доступная приложению, исчерпана, и больше нет места для выделения новых объектов. Хотя нехватка памяти может происходить и в других уровнях, по сути она делится на две большие категории, и различение этих категорий является первым шагом в устранении неполадок.

image1.png

* Источник изображения: https://www.cnblogs.com/java1024/p/12381457.html

Первая категория — это OutOfMemoryError, возникающий внутри JVM. JVM задаёт максимальный размер кучи (Heap) при запуске и выдает объекты в пределах этого предела, но когда неочищенные объекты накапливаются и куча достигает своего предела, JVM бросает java.lang.OutOfMemoryError и прекращает нормальную работу.

image2.jpeg

* Источник изображения: https://suneeta-mall.github.io/blog/2021/03/14/wth-who-killed-my-pod---whodunit/

Вторая категория — это OOM Killed, возникающий на уровне контейнера. В среде K8s поды получают ограничения по памяти, и если фактически используемая контейнером память превышает этот лимит, OOM Killer хоста принудительно завершает этот процесс. В этом случае под оставляет Last State с OOMKilled и снова перезапускается в соответствии с политикой перезапуска.

Проблема заключается в том, что OOM — это не инцидент, который происходит в определённый момент, а скорее результат накопления. Непросмотренные объекты постепенно накапливаются в куче, сборщик мусора пытается их очистить, но они по-прежнему сильно ссылаются откуда-то, не позволяя очистке завершиться. Как только количество неочищенных объектов преодолевает критическую точку, происходит OOM, и в записях журнала в момент возникновения ошибки часто фиксируется не настоящий виновник, а невинный код, который последним запрашивал память. В итоге, чтобы выяснить истинную причину OOM, нужно взглянуть не на момент, когда произошла ошибка, а на то, что находилось в куче непосредственно перед этим. Именно здесь нужны Heap Dump и MAT для его анализа.

Memory Analyzer Tool

Memory Analyzer Tool (далее MAT) — это инструмент анализа Heap Dump с открытым исходным кодом, предоставляемый фондом Eclipse. Он парсит сырые Heap Dump (.hprof), которые трудно читать человеком, и визуально показывает, сколько памяти занимает каждый объект и почему этот объект не может быть предметом GC.

Ключевыми аспектами MAT являются два показателя: Shallow Heap и Retained Heap. Shallow Heap обозначает размер памяти, занимаемой объектом, а Retained Heap — общий объём памяти, который будет очищен, если данный объект будет очищён. При поиске причины OOM ключевым аспектом является отслеживание объектов с аномально большим Retained Heap. Поскольку сумма памяти, которую этот объект удерживает, а не размер самого объекта, указывает на реальный объём утечки.

image3.png

* источник изображения: AnyLogic Help, “Memory analyzer”, https://anylogic.help/advanced/debug/memory-analyzer.html

Данный вид можно проверить, импортировав файл Heap Dump в MAT (Memory Analyzer Tool). Этот вид отвечает за ключевые функции Memory Analyzer Tool и показывает точки, где предположительно происходит утечка памяти, на основе Heap Dump, которые анализирует MAT. Круговой график показывает процент по сравнению с Heap процесса и помогает выяснить, какие данные чрезмерно занимают Heap.

image4.png

* источник изображения: AnyLogic Help, “Memory analyzer”, https://anylogic.help/advanced/debug/memory-analyzer.html

Также можно увидеть следующий экран.

На этом экране представлен отчет Leak Suspects, который автоматически создается MAT после анализа Heap Dump. MAT не просто перечисляет список объектов, но и указывает на объекты, занимающие аномально много памяти, как “подозрительные точки утечек (Leak Suspect)”. Круговой график в верхней части экрана визуально отображает долю каждого подозрительного объекта от общего Heap, и если один объект или группа экземпляров определенного класса занимает большую часть, этот участок будет выделен, чтобы его было легче заметить.

В нижней части графика предлагается обобщенное описание каждой подозрительной точки. На каком классе и сколько Retained Heap занимает, и через какой ClassLoader или путь ссылок поддерживается этот объект, организовано в форме, близкой к естественному языку, что позволяет даже разработчикам, не знакомым с анализом Heap Dump, быстро сузить круг кандидатов на причину утечки. Это хороший экран, который можно использовать как отправную точку для анализа и определения направления перед тем, как начать детальный анализ Dominator Tree.

Способ извлечения Heap Dump

Heap Dump можно настроить так, чтобы он автоматически создавался в момент возникновения OOM или можно извлечь его, отдав команду работающему процессу. В рабочей среде наиболее важно захватить состояние в момент возникновения OOM, поэтому настройка автоматического дампа через параметры JVM является эффективным решением.

Добавив опцию -XX:+HeapDumpOnOutOfMemoryError, .hprof файл автоматически создается в момент возникновения OutOfMemoryError, а путь для сохранения можно указать с помощью -XX:HeapDumpPath. Если необходимо немедленно извлечь в работающем процессе, используйте команду jmap -dump:live,format=b,file=heap.hprof <pid>. Опция live позволяет выполнить Full GC перед дампом, чтобы анализировать только живые объекты.

Однако в окружении K8s есть один момент, который стоит учитывать. В момент, когда под убивается из-за OOM, контейнер завершает работу, и файл дампа, созданный внутри, также может исчезнуть. Поэтому следует рассмотреть возможность указания HeapDumpPath как путь к постоянному хранилищу, такому как PersistentVolume, или использовать отдельный отладочный контейнер для доступа к файловой системе непосредственно перед завершением.

Отслеживание объектов утечки с помощью Dominator Tree

В результате анализа извлеченного Heap Dump в MAT с использованием Dominator Tree, было установлено, что экземпляры определенного класса занимают аномально много Retained Heap. Dominator Tree показывает доминирующие отношения между объектами в форме дерева, что делает возможным быстрое понимание того, какие объекты удерживают память других объектов.

Чтобы понять, почему объекты, которые должны были быть собраны на определенном уровне, продолжают накапливаться, использовалась функция Path to GC Roots. Это функция обратного отслеживания, которая определяет, каким образом данный объект связан с корнями GC, и точно указывает, кто удерживает объект и не освобождает его. В результате анализа было обнаружено, что объекты сообщений, поступавшие сверх скорости обработки, продолжали накапливаться в коллекции и оставались в сильной ссылке, что не позволяло сборщику мусора освободить память.

В конечном итоге, из-за узкого места в I/O БД в процессе асинхронной обработки скорость потребления сообщений не могла угнаться за производительностью, и объекты, которые не были обработаны, бесконечно накапливались в Heap, что привело к OOM. К счастью, этот факт удалось обнаружить в ходе анализа Heap Dump, и с улучшением I/O логики БД удалось сократить скорость обработки и предотвратить бесконечное накопление объектов.

В конце

Heap Dump — это инструмент, который наиболее точно выясняет, кто стал причиной OOM, уже произошедшего. Когда вы сталкиваетесь с проблемой утечки памяти, которая не видна в журналах и трудно определить по показателям в реальном времени, лучше всего подумать о традиционном методе анализа, который позволяет посмотреть на кучу в этот момент и распутать клубок ссылок объектов. Важнее всего, что анализ Heap Dump не просто решает текущую проблему, но и предоставляет возможность глубже понять, какой цикл жизни имеет объект, когда он создается и исчезает, а также какие ссылки мешают сбору памяти. Я считаю, что накопленный опыт анализа позволяет быстрее распознавать подобные признаки в будущем, а также развивает способность проектировать структуры, в которых утечки не происходят.

jungboke

Site footer