OOMKilled и exit code 137: почему ядро убило контейнер и как подобрать лимит памяти
Команда KAI Lab · · 8 мин чтения
OOMKilled значит, что процесс в контейнере превысил лимит памяти своей cgroup и ядро Linux убило его сигналом SIGKILL; код выхода 137 — это 128 + 9, где 9 — номер SIGKILL. Главная причина — лимит памяти меньше, чем приложению нужно на пике, и исправляют её замером реального потребления и правильным лимитом, а не удалением лимита.
Ниже — как устроено убийство по памяти, как отличить OOMKilled от других случаев с кодом 137, где найти подтверждение в журнале ядра и как выбрать requests и limits, чтобы история не повторилась.
Что происходит при OOMKilled
Лимиты контейнера задаёт kubelet вместе с container runtime, а исполняет ядро через cgroups. Лимит памяти становится лимитом cgroup контейнера. Когда процесс пытается выделить больше, включается подсистема out-of-memory ядра и завершает процесс в контейнере. Если это основной процесс, а политика перезапуска позволяет, Kubernetes поднимает контейнер заново.
Три детали, которые объясняют странности OOMKilled:
- Лимит работает реактивно. В документации Kubernetes прямо сказано: ядро убивает процесс, когда замечает нехватку памяти, поэтому контейнер может ненадолго выйти за лимит и не умереть сразу.
- SIGKILL нельзя перехватить. По
man 7 signal, SIGKILL и SIGSTOP нельзя поймать, заблокировать или проигнорировать. Приложение не успевает ничего записать, и лог просто обрывается на середине. - На cgroup v2 убивают весь контейнер. По умолчанию kubelet включает для cgroup контейнера
memory.oom.group, и процессы контейнера завершаются вместе. Старое поведение cgroup v1, когда убивали один процесс, возвращает параметрsingleProcessOOMKill: trueв конфигурации kubelet.
OOMKilled не то же самое, что java.lang.OutOfMemoryError. Это исключение бросает сама JVM, когда кончается её куча: приложение пишет его в лог и само решает, что делать. OOMKilled приходит снаружи, от ядра.
Как выглядит OOMKilled
В Kubernetes статус пода чередуется: сразу после убийства — OOMKilled, во время паузы перед рестартом — CrashLoopBackOff.
$ kubectl get pods -n shop
NAME READY STATUS RESTARTS AGE
reports-7f9c6d8b5-kx4wd 0/1 OOMKilled 3 (41s ago) 12mПричина надёжнее всего видна в kubectl describe pod, в блоке Last State:
$ kubectl describe pod reports-7f9c6d8b5-kx4wd -n shop
...
State: Waiting
Reason: CrashLoopBackOff
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Started: Fri, 25 Sep 2026 14:02:11 +0500
Finished: Fri, 25 Sep 2026 14:05:47 +0500
Ready: False
Restart Count: 3
Limits:
memory: 512Mi
Requests:
cpu: 250m
memory: 512MiВ Docker без Kubernetes то же самое показывает docker inspect:
$ docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' reports
true 137Чем 137 отличается от 143
Когда процесс завершается по сигналу N, код выхода равен 128 + N. SIGTERM — сигнал 15, SIGKILL — 9.
- 143 = 128 + 15, SIGTERM. Контейнер вежливо попросили остановиться:
docker stop, удаление пода, выкатка новой версии, перезапуск по liveness probe. Приложение получило шанс закрыть соединения и выйти. - 137 = 128 + 9, SIGKILL. Процесс убили без предупреждения. Это OOM killer, но не только он:
docker killпо умолчанию шлёт SIGKILL, аdocker stopи Kubernetes добивают SIGKILL процесс, который не завершился после SIGTERM за отведённое время. В Docker это 10 секунд для Linux-контейнеров, в Kubernetes —terminationGracePeriodSeconds, по умолчанию 30 секунд.
Поэтому код 137 сам по себе не доказывает нехватку памяти. Смотрите на Reason: OOMKilled в Kubernetes или флаг OOMKilled в docker inspect. В событиях Docker разница видна сразу:
$ docker events --since 30m --filter container=reports --filter event=oom --filter event=die --filter event=kill
2026-09-25T14:05:47.118532114+05:00 container oom 5e1a...c3d9 (image=registry.example.kz/shop/reports:3.1.0, name=reports)
2026-09-25T14:05:47.204761983+05:00 container die 5e1a...c3d9 (exitCode=137, image=registry.example.kz/shop/reports:3.1.0, name=reports)Если бы контейнер не уложился в таймаут остановки, вместо события oom были бы два kill: сначала с signal=15, через 10 секунд с signal=9, и потом die с exitCode=137.
Где найти подтверждение: dmesg на ноде
Ядро записывает каждое убийство в свой журнал. На ноде Kubernetes это можно посмотреть по SSH или через отладочный под с правами администратора:
$ kubectl debug node/worker-2 -it --image=ubuntu --profile=sysadmin
root@worker-2:/# dmesg -T | grep -i -E "out of memory|oom-kill"
[Fri Sep 25 14:05:47 2026] python3 invoked oom-killer: gfp_mask=0xcc0(GFP_KERNEL), order=0, oom_score_adj=969
[Fri Sep 25 14:05:47 2026] Memory cgroup out of memory: Killed process 30817 (python3) total-vm:1893312kB, anon-rss:520544kB, file-rss:10240kB, shmem-rss:0kB, UID:1000 pgtables:1432kB oom_score_adj:969Важна первая половина строки. Memory cgroup out of memory значит, что процесс упёрся в лимит своего контейнера. Просто Out of memory: Killed process — что память кончилась на всей ноде и ядро выбирало жертву среди всех процессов. anon-rss показывает, сколько памяти процесс занимал в момент убийства.
При нехватке памяти на всей ноде жертву выбирают по oom_score с поправкой oom_score_adj, а её kubelet выставляет по QoS-классу пода: Guaranteed получают −997, BestEffort — 1000, Burstable — значение между ними, которое тем меньше, чем больше запрошено памяти. Поэтому поды без requests умирают первыми.
Requests и limits: что на что влияет
- Request нужен планировщику: под попадёт только на ноду, где сумма requests помещается в доступную память. Контейнер может потреблять больше request, если на ноде есть свободная память.
- Limit — потолок cgroup. Выше него контейнер не пустят: его процесс убьёт ядро.
Если контейнер ест больше своего request, а память на ноде заканчивается, дело может закончиться не OOMKilled, а выселением всего пода kubelet-ом. Это другой механизм, мы разобрали его в статье под Evicted.
Без лимита контейнер может занять всю память ноды. Документация Kubernetes предупреждает, что это ведёт к OOM на уровне ноды, и тогда под нож идут соседние сервисы. Администратор может задать лимит по умолчанию для пространства имён через LimitRange.
Пошаговая диагностика
Шаг 1. Причина завершения. kubectl describe pod, блок Last State: Reason: OOMKilled, Exit Code: 137.
Шаг 2. Текущее потребление против лимита. При установленном metrics-server:
$ kubectl top pod reports-7f9c6d8b5-kx4wd -n shop --containers
POD NAME CPU(cores) MEMORY(bytes)
reports-7f9c6d8b5-kx4wd reports 212m 489MiВ Docker — docker stats --no-stream, колонка MEM USAGE / LIMIT:
$ docker stats --no-stream reports
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
5e1a0b7c3d9f reports 38.12% 498.3MiB / 512MiB 97.32% 1.2MB / 845kB 4.1MB / 0B 9Шаг 3. Журнал ядра. dmesg -T на ноде: убийство по лимиту cgroup или по памяти всей ноды.
Шаг 4. Характер роста. На графике памяти контейнера OOM на старте, под пиковой нагрузкой и при медленной утечке выглядят по-разному: вертикальный скачок, всплеск вместе с трафиком или ровная наклонная линия до потолка.
Шаг 5. Что изменилось. Новая версия, новый тип отчёта, выросший объём данных, изменённые параметры рантайма.
Причины OOMKilled: таблица
| Причина | Как узнать | Как исправить |
|---|---|---|
| Лимит ниже рабочего пика | OOMKilled под нагрузкой, потребление у самого лимита | замерить пик и поднять limits.memory с запасом |
| Утечка памяти | потребление растёт ровной линией, перезапуски через похожие интервалы | профилировать и чинить код; больший лимит только отодвигает падение |
| Куча рантайма больше лимита | OOMKilled без OutOfMemoryError в логах | согласовать кучу с лимитом: -XX:MaxRAMPercentage для JVM, --max-old-space-size для Node.js |
| Данные в памяти через tmpfs | том emptyDir с medium: Memory растёт | sizeLimit для тома или обычный диск |
| Память кончилась на всей ноде | в dmesg Out of memory без слов Memory cgroup | requests по реальному потреблению, резерв system-reserved и kube-reserved, больше нод |
| Код 137 без OOM | OOMKilled: false, в событиях kill с signal=15, затем с signal=9 | обрабатывать SIGTERM, проверить terminationGracePeriodSeconds |
Как подобрать лимит памяти
Лимит выбирают по замерам, а не по ощущениям. Снимите потребление под реальной нагрузкой за период, где были пики: утренний трафик, ночные отчёты, конец месяца. Request ставьте около обычного потребления, limit — на наблюдаемый пик с запасом.
Для критичных сервисов удобно ставить request равным limit по памяти и CPU. Такой под получает класс Guaranteed: его выселяют в последнюю очередь, а при нехватке памяти на ноде ядро выбирает его последним.
Убрать лимит — плохое решение. Сервис перестанет падать сам, но однажды займёт память соседей по ноде, и упадут уже они.
В Docker лимит задают флагом -m (--memory) или mem_limit в Compose. Есть нюанс со swap: если --memory-swap не задан, а на хосте есть swap, контейнер может использовать столько же swap, сколько памяти. Флаг --oom-kill-disable документация Docker советует включать только вместе с -m, иначе при нехватке памяти ядру придётся убивать системные процессы хоста.
Если контейнер с restart: always падает по памяти, снаружи это выглядит как сервис, который работает через раз. Как ловить такие перезапуски, мы разобрали в статье контейнер Docker постоянно перезапускается. В Kubernetes повторяющиеся OOMKilled приводят к CrashLoopBackOff.
Частые вопросы
Exit code 137 всегда означает нехватку памяти? Нет. 137 — любое завершение по SIGKILL: OOM killer, docker kill, принудительная остановка после таймаута. Про память говорит только Reason: OOMKilled в Kubernetes или "OOMKilled": true в docker inspect.
Почему в логах приложения нет ни одной ошибки? SIGKILL нельзя перехватить: процесс не получает шанса записать сообщение или сбросить буферы. Лог обрывается на середине, а объяснение есть только в журнале ядра и в статусе контейнера.
Чем OOMKilled отличается от Evicted? OOMKilled — ядро убило контейнер за превышение его собственного лимита, и kubelet перезапускает его в том же поде. Evicted — kubelet сам остановил весь под, потому что на ноде кончается память или диск. Такой под на этой ноде больше не запускается, замену создаёт контроллер.
Можно ли просто убрать лимит памяти? Технически да, но тогда контейнер может занять всю память ноды. При нехватке памяти на ноде пострадают соседние поды, а без requests ваш под станет первым кандидатом и на выселение, и на OOM killer.
Как понять, какой лимит нужен? Замерить потребление под реальной нагрузкой за период с пиками, поставить limit на пик с запасом и настроить алерт, когда потребление подходит к лимиту. Это частый вопрос на собеседовании по Kubernetes, и хороший ответ всегда начинается с замера.
Потренироваться на практике
В симуляторе DevOps KAI Lab есть модули по Docker и Kubernetes: консоль в браузере отдаёт реальные выводы docker events, dmesg и kubectl describe, а разбираться с перезапусками и лимитами приходится по живым тикетам. Проверить себя можно в бесплатном тесте навыков DevOps.
Источники
- Kubernetes. Resource Management for Pods and Containers — лимиты через cgroups, реактивное применение лимита памяти, разбор OOMKilled.
- Kubernetes. Assign Memory Resources to Containers and Pods — пример OOMKilled с кодом 137, контейнер без лимита, LimitRange.
- Kubernetes. Node-pressure Eviction — OOM на уровне ноды,
oom_score_adjпо QoS-классам. - Kubernetes. Pod Quality of Service Classes — Guaranteed, Burstable, BestEffort.
- Kubernetes. Kubelet Configuration (v1beta1) —
singleProcessOOMKillиmemory.oom.group. - Kubernetes. Debug Running Pods —
kubectl debug node,--profile=sysadmin. - Docker. Resource constraints —
--memory,--memory-swap,--oom-kill-disable. - Docker. docker events и docker container stop — события
oomиdie, таймаут остановки 10 секунд. - signal(7) и bash(1) — номера сигналов, код 128 + N.
- Linux. mm/oom_kill.c — формат сообщений
invoked oom-killerиMemory cgroup out of memory.