Настройки кучи JVM и OOMKilled

Настройки кучи JVM и OOMKilled

1. Обзор

В этом проекте была необходима функция выявления аномалий в данных датчиков корабля и генерации сигналов тревоги. Для реализации этой функции была разработана новая пакетная логика, которая запрашивает данные датчиков по кораблям и определяет условия для генерации сигналов тревоги.

Эта функция выполнялась в сервисе, развернутом в кластере Kubernetes в форме Pod, и предполагалось, что она будет выполняться раз в день, накапливая статистику сигналов тревоги по каждому кораблю. Однако, неожиданно, во время выполнения логики повторялась проблема перезапуска Pod.

2. Проблемная ситуация

При проверке причины перезапуска Pod каждый раз, когда выполнялась логика, выяснилось, что возникала ошибка OOMKilled (Out Of Memory Killed), то есть контейнер был принудительно завершен OOM Killer из-за превышения лимита памяти. Мне показалось, что установка Memory Limit для обработки больших объемов данных корабля была слишком маленькой, и я попытался увеличить Memory Limit Pod, но проблема всё ещё не была решена.

В конечном итоге, ситуация с перезапуском Pod была явно вызвана OOMKilled, однако она не имела непосредственного отношения к настройке Limit. Почему Pod был принудительно завершен по причине OOMKilled, если недостатка в Memory Limit не было?

3. Память JVM: Heap и Non-Heap

Память JVM можно в основном разделить на области Heap и Non-Heap.

3.1 Область Heap

Это область памяти, в которой хранятся объекты Java. Большинство объектов создаются с использованием ключевого слова new в приложении Java и, как правило, хранятся в области Heap. Эта область является основной управляемой сборщиком мусора (GC), и объекты, которые более не ссылаются, очищаются 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) Старое поколение

Это область, в которой сохраняются объекты, пережившие долгое время. Spring Bean, кэшированные данные и объекты, прошедшие несколько Minor GC, могут остаться в Старом поколении.

Если эта область станет недостаточной, может произойти Full GC или Major GC. Full GC, как правило, более тяжелый, чем Minor GC, так как проверяет как Молодое, так и Старое поколение, и время остановки приложения Stop-The-World может значительно увеличиться.

3.2 Область Non-Heap

image2.png

Это область памяти JVM вне кучи, где хранятся Java-объекты. Хотя эта область не является основной целью сборки мусора (GC) для общих объектов, она необходима для выполнения Java-приложений. В Non-Heap входят Metaspace, Code Cache, Thread Stack, JVM Native Memory.

4. Связь между размером кучи и GC

Размер кучи JVM тесно связан с поведением GC. Большинство объектов, созданных приложением, хранятся в куче, и GC очищает эту область кучи, чтобы вернуть память для объектов, на которые больше нет ссылок. Таким образом, время появления, частота и продолжительность GC могут зависеть от того, как установлен размер кучи.

image3.png

4.1 Если размер кучи установлен слишком малым

Если размер кучи установлен слишком малым, пространство для хранения объектов быстро исчерпывается, и JVM будет выполнять GC чаще. Особенно, если Eden, область Молодого поколения, быстро заполняется, может часто возникать Minor GC, а если в Старом поколении недостаточно свободного пространства, может произойти Full GC. У маленькой кучи есть недостаток в том, что GC возникает часто, однако, поскольку объем памяти, который необходимо проверить и очистить за одну сборку, относительно мал, время выполнения GC может быть коротким.

4.2 Если размер кучи установлен слишком большим

Наоборот, если размер кучи установлен слишком большим, это позволяет хранить больше объектов, и частота возникновения GC может снизиться. Особенно, если область Старого поколения большая, объекты, живущие долго, могут накапливаться, и в итоге состояние нехватки пространства может достигаться позже, что снижает вероятность частых Full GC. Если при большом размере кучи происходит Full GC, необходимо обрабатывать более обширную область памяти, что может привести к большему времени выполнения одной сборки Full GC. Даже если используется Parallel GC, который позволяет обрабатывать несколько потоков GC параллельно, во время Full GC приложение может быть остановлено в режиме Stop-The-World.

Установка большой кучи может снизить частоту возникновения Full GC, однако, во время задержки возникновения GC несущественные объекты могут надолго оставаться в куче. И если происходит Full GC, то необходимо обрабатывать большую область памяти, что может сделать время GC длиннее. С другой стороны, если куча установлена слишком маленькой, GC может происходить слишком часто, что ведет к снижению производительности обработки приложения. Поэтому размер кучи не обязательно должен быть большим или маленьким, и необходимо корректно регулировать его в зависимости от ситуации приложения.

5. Опции выполнения JVM для Pod

Ограничение памяти Pod составило 2 ГБ, а опции выполнения JVM были установлены на -XX:MaxRAMPercentage=75, что устанавливало максимальный процент размера кучи на 75% от объема контейнера. По простым расчетам, размер кучи мог достигать около 1.5 ГБ, что составляет 75% от 2 ГБ, и оставшиеся около 0.5 ГБ могли быть использованы для Non-Heap и Native областей. В среде Kubernetes это относительно высокий уровень процента кучи, и, поскольку область кучи была установлена выше необходимого, также была относительно большая область Старого поколения, что в свою очередь привело к снижению частоты возникновения Full GC и вызывало сомнения в том, не является ли OOMKilled следствием этого.

Поэтому снизив до -XX:MaxRAMPercentage=60 и запустив логику пакетной обработки, а также мониторинг состояния Pod, было подтверждено, что логика выполняется нормально без перезапуска Pod. Также при проверке максимальной доступной памяти JVM, оставшейся памяти из выделенной, и фактически используемой памяти в логах приложения, была подтверждена нормальная работа Full GC, которая отличалась от предыдущих данных. Когда размер кучи составлял -XX:MaxRAMPercentage=75, возникала проблема, что память не очищалась во время выполнения логики, когда использованная память достигала максимума, и Pod перезапускался, но после снижения до -XX:MaxRAMPercentage=60 и проверки логов приложения было видно, что Full GC выполнялся в момент достижения максимальной памяти, и использованная память резко уменьшалась.

image4.png

6. Заключение

Из этого примера я понял, что просто установка ограничения памяти для Kubernetes Pod не решает проблему OOMKilled. Конечно, установка подходящего ограничения памяти более важна, но также необходимо учитывать настройку размера кучи JVM.

Следовательно, когда мы сталкиваемся с проблемой OOMKilled при работе с Java-приложениями в среде Kubernetes, нам следует не просто увеличивать ограничение памяти, а проверять, правильно ли настроены размер кучи и область Non-Heap, а также происходит ли сборка мусора. Это поможет сократить нерациональное использование ресурсов и создать более стабильную и эффективную среду для работы приложений.

deeenee

Site footer