Анализ причин OOMKilled в среде Kubernetes

Анализ причин OOMKilled в среде Kubernetes

Обзор

При работе над проектом вы можете столкнуться с OOM или OOMKilled. Особенно в окружении Kubernetes часто возникают ситуации, когда "Pod был перезапущен из-за OOMKilled."

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

Этот документ подводит итоги расследования и обзора того, что такое OOMKilled, почему он возникает и как его анализировать и решать на основе встреченных проблем в ходе проекта.

Разница между OOM и OOMKilled

OOM и OOMKilled — это проблемы, связанные с распределением памяти. Однако они различаются по сущности, которая их вызывает, и их операционным механизмам.

OOM (Ошибка исчерпания памяти) происходит в JVM. Когда оперативная память заполнена и больше не может быть выделено объектов, JVM бросает java.lang.OutOfMemoryError и завершает процесс. Другими словами, JVM распознает свой собственный предел и вызывает ошибку.

OOMKilled происходит вне JVM, в контейнерных средах, таких как Kubernetes. В момент, когда превышен лимит памяти, установленный для контейнера, Kubernetes принудительно завершает этот процесс. Поскольку он убивается извне до того, как JVM успевает распознать ошибку, труднее определить причину, так как не остаётся отдельных логов ошибок, в отличие от OOM.

Классификация

OOM

OOMKilled

Сущность, вызывающая возникновение

Внутри JVM

Kubernetes (cgroup)

Причина

Исчерпание памяти кучи

Превышен лимит контейнера

Журнал ошибок

Журнал OutOfMemoryError сохранён

Принудительное завершение без журнала

Код выхода

1

137

Целевая память

Область кучи

Всего кучи + Не кучи + Вне кучи

Когда происходит OOMKilled, Pod автоматически перезапускается в соответствии с политикой перезапуска Kubernetes. Если это происходит в периоды низкой загрузки, это может не быть большой проблемой. Однако, если OOMKilled происходит в условиях высокой нагрузки, ситуация может стать серьёзной. В зависимости от состояния Pod, служба может быть прервана во время перезагрузки, и если трафик резко увеличится сразу после перезагрузки, память может быстро заполониться, что приведёт к повторным инцидентам OOMKilled. В конечном итоге, по мере увеличения нагрузки на процессор и память, это может привести к критическим сбоям для всей службы.

Различие памяти

Когда речь идёт о памяти, связанной с OOMKilled, важно понимать не только распределение памяти кучи, но и детали памяти контейнера, JVM и другие более специфические параметры.

Память контейнера

Одним из важных факторов, которые необходимо учитывать в среде Kubernetes, является диапазон настроек памяти. Ограничение памяти, установленное для контейнера, применяется не только к JVM, но и ко всему контейнеру.
Другими словами, важно признать взаимосвязь, при которой память контейнера больше, чем память JVM.

Container 메모리 
│
├── JVM 프로세스 메모리
│   ├── Heap
│   ├── Non-Heap
│   └── Off-Heap (Direct Buffer)
│
├── OS 및 시스템 라이브러리
└── 사이드카 Container (Istio sidecar 등) 등 기타…

Память JVM

Память JVM в основном делится на три области.

Область кучи

Это пространство, где хранятся объекты, создаваемые Java-приложениями. Это также область, на которую нацелена сборка мусора (GC).

Метрики

Описание

Используемая куча

Память, которая в настоящее время используется

Выделенная куча

Память, которую JVM предварительно выделила из ОС. Она не высвобождается сразу после сборки мусора.

Максимальная куча

Максимальный размер кучи, доступный для JVM.

Связь между тремя метриками всегда следующая: Используемая ≤ Выделенная ≤ Максимальная, и Выделенная очищается во время полной сборки мусора.

Не кучная область

Область для собственных операций JVM, не подлежащая сборке мусора.

Область

Описание

Метапробел

Хранит метаданные классов и статические переменные.

Метапробел теоретически может расти бесконечно, если не установлено -XX:MaxMetaspaceSize.

Область вне кучи (Прямой буфер)

Область, которая не является ни кучей, ни не кучей, память выделяется напрямую JVM из ОС. В основном используется для обработки файлов на основе NIO или сетевого ввода-вывода.

Поскольку она не подлежит сборке мусора, она может накапливаться, если не освобождена явно, и может расти бесконечно, если не установлено ограничение с помощью -XX:MaxDirectMemorySize.

Соотношение между памятью контейнера и памятью JVM

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

В системе, которая использует 200 МБ для не кучи, 100 МБ для вне кучи и 50 МБ для другой памяти, связанной с JVM, если предел контейнера установлен на 1Gi и -XX:MaxRAMPercentage=80, возникает следующая ситуация.

Heap Max (80%)  :  819MB  ← JVM이 사용 가능하다고 인식하는 Heap 한도
Non-Heap        :  200MB
Off-Heap        :  100MB
JVM 외 기타      :   50MB
─────────────────────────
Heap 외 합계     :  350MB
실질적으로 Heap에 허용되는 메모리 : 1,024MB - 350MB = 674MB

JVM распознает, что может использовать до 819 МБ кучи и продолжает выделять память без полной сборки мусора. Однако, поскольку Kubernetes суммирует кучу + не кучу + вне кучи + другие и сравнивает это с пределом контейнера, в момент, когда использование кучи превышает 674 МБ, оно превышает предел контейнера в 1Gi, и Kubernetes принудительно завершает процесс до того, как JVM осознает это. Это то, что называется OOMKilled.

Настройки JVM

Ниже представлено краткое определение параметров JVM, связанных с OOMKilled.

MaxRAMPercentage

  • Задает максимальный размер кучи в процентах от лимита контейнера. Установка слишком низкого значения может привести к неэффективному использованию памяти, а установка слишком высокого — к превышению лимита контейнера в сочетании с потреблением вне кучи и вне платформы.

MaxMetaspaceSize

  • Ограничивает максимальный размер Metaspace в зоне вне кучи. Если не установить, Metaspace теоретически может увеличиваться без ограничений. Рекомендуется установить предел с небольшим запасом на основе текущего использования.

MaxDirectMemorySize

  • Ограничивает максимальный размер вне кучи (Direct Buffer). В сервисах, которые активно используют обработку файлов на основе NIO или сетевой ввод-вывод, если не установить, Direct Buffer может расти без предела и превышать лимит контейнера. Установка этой опции также предоставляет дополнительное преимущество в виде логирования OutOfMemoryError: память прямого буфера, когда лимит превышен, что облегчает определение причины.

Анализ случая OOMKilled на сервере

Ниже приведен обзор состояния памяти сервиса, в котором произошло OOMKilled, проанализированный ИИ на основе входных данных. Проблема OOMKilled была решена после внесения окончательных изменений в параметры JVM.

Ссылка на оригинальное представление:
https://app.notion.com/p/vizend/Pod-OOMKilled-38b35bc54c13806699e8e3ae329bf38f

Анализ состояния памяти

1. Настройки ресурсов контейнера

Элемент

Значение

Ограничение памяти

1Gi (1,073 МБ)

Запрос памяти

256Mi

Класс QoS

Временно увеличенный

2. Статус памяти JVM (Текущее время)

Область

Значение

Преобразование MB

Используемая куча

376,595,248 байт

~359 МБ

Зафиксированная куча

512,229,376 байт

~488 МБ

Макс кучи

859,308,032 байт

~819 МБ

Использовано вне кучи

210,018,744 байт

~200 МБ

└ Метапамять

127,576,696 байт

~122 МБ

3. Анализ основной проблемы

Container Limit          :  1,073 MB  (1Gi)
────────────────────────────────────────
Heap Max (JVM 설정)       :    819 MB
Non-Heap Used (현재)      :    200 MB
────────────────────────────────────────
합산 최대 사용 가능       :  1,019 MB  ← 한계에 매우 근접
나머지 여유               :    ~54 MB  ← 위험 수준

Максимальная куча + некучная память JVM составляет примерно 95% от лимита контейнера.

При добавлении внутренней накладной JVM (стек потоков, метаданные GC и т.д.) это превышает 1Gi, что приводит к OOMKilled.

4. Проверьте метод GC

Ответ Актюатора показывает PS Eden Space, PS Old Gen, PS Survivor Space.

Мы рекомендуем использовать G1GC.

5. Сводка причин

Причина

Описание

Чрезмерный максимум кучи

Превышение лимита чуть перед 1,019MB с 819MB кучи + 200MB некучевых данных

Неподходящий метод сборки мусора

Параллельная сборка мусора → Недостаточное распознавание памяти контейнера

Опция JVM не установлена

Предполагаем недостаток опций для распознавания контейнера, таких как -XX:MaxRAMPercentage

Burstable QoS

Запрос ≠ Лимит → Нет гарантии памяти, приоритет OOM

Результат симуляции MaxRAMPercentage

Сравнили и проанализировали MaxRAMPercentage с текущих 80% до 60%.

Использование кучи

Элемент

80%

70%

65%

60%

Лимит контейнера

1,073МБ

1,073МБ

1,073МБ

1,073МБ

Макс. куча

858МБ

~751МБ

~698МБ

~644МБ

Замер вне кучи

~200МБ

~200МБ

~200МБ

~200МБ

Накладные расходы, включая стек потока

~50~80МБ

~50~80МБ

~50~80МБ

~50~80МБ

Общая оценка

~1,108~1,131МБ

~1,001~1,031МБ

~948~978МБ

~894МБ

Свободно

~42~72МБ

~95~125МБ

~179МБ

Сравнение по опции

Настройки

Максимум кучи

Свободно

Оценка

Сравнение

Текущий 80%

~858МБ

~15МБ

Риск

Проблемы при использовании дополнительных ресурсов вне кучи, вне кучи

70%

~751МБ

~42~72МБ

Нестабильно

Увеличить лимит контейнера с G1GC (1.5Gi)

60%

~644МБ

~149~179МБ

Рекомендуется

Применить G1GC

50%

~536МБ

~257МБ

Безопасно, но объема кучи может быть недостаточно

Исключить из обзора

  • Самый безопасный подход - установить его на 60% и контролировать, увеличивая при необходимости.

  • Не-кучи или вне-кучи 200MB - это не фиксированное значение и может вызвать OOMKilled в ситуациях нагрузки, таких как дополнительные загрузки файлов.

Рекомендуемые параметры JVM (окончательные)

# 현재
-XX:InitialRAMPercentage=25
-XX:MaxRAMPercentage=80
-XX:+UseParallelGC
# 권장
-XX:InitialRAMPercentage=25
-XX:MaxRAMPercentage=60          # Heap ~644MB로 제한
-XX:+UseG1GC                     # Container 친화적 GC
-XX:MaxMetaspaceSize=192m        # Metaspace 상한 설정 (현재 122MB 사용 중)
-XX:MaxDirectMemorySize=140m     # Direct Buffer 상한 설정 (현재 108MB 사용 중)
  • Максимум кучи: ~644MB

  • Не-куча: ~200MB

  • Всего: ~844MB → Обеспечить резерв внутри предела контейнера в 1Gi

sby

Site footer