Зачем вообще ядру кого-то убивать
Когда программа просит память через malloc, Linux по умолчанию почти всегда говорит "да", даже если столько свободной памяти прямо сейчас нет. Это называется overcommit - переподписка. Логика простая: программы часто просят с запасом и реально используют не всё. Память выдаётся лениво, страница за страницей, в момент первого реального обращения (это называется demand paging). Поэтому malloc не падает сразу - проблема всплывает позже, когда процессы начинают по-настоящему трогать выданные страницы и физическая память вместе с подкачкой (swap) заканчиваются.
И вот тут наступает out of memory. Ядру некуда положить очередную страницу. Вернуть ошибку приложению оно уже не может - страницу-то обещали ранее, при malloc. Остаётся два варианта: повесить всю систему намертво (бесконечный reclaim, дикий своппинг, load average в небеса) или принести в жертву один процесс. Ядро выбирает второе и запускает OOM killer. Это не баг и не сбой - это аварийный клапан, который не даёт серверу превратиться в кирпич.
Поведением overcommit рулит параметр vm.overcommit_memory:
Код: Выделить всё
cat /proc/sys/vm/overcommit_memory- 0 - эвристика (значение по умолчанию). Ядро разрешает разумную переподписку, но явно безумные одиночные запросы отклоняет.
- 1 - всегда разрешать. malloc не откажет почти никогда. Удобно для процессов, которые резервируют гигантские области, но используют крохи (типичный пример - Redis при fork для bgsave, поэтому в его доке рекомендуют именно 1).
- 2 - строгий режим. Сумма выданного не превысит swap плюс часть RAM (доля задаётся vm.overcommit_ratio, по умолчанию 50, то есть RAM*0.5 + swap). Тут malloc уже честно вернёт NULL, и OOM killer срабатывает гораздо реже - но приложения обязаны корректно обрабатывать отказ в выделении памяти, иначе будут падать сами.
Код: Выделить всё
grep -i commit /proc/meminfo
# CommitLimit - потолок, Committed_AS - сколько уже обещано
Как читать OOM killer в dmesg и journalctl
Первое, что делаешь при подозрении на out of memory linux - идёшь в кольцевой буфер ядра. Логи приложения тут бесполезны: процесс умирает мгновенно по SIGKILL и обработать сигнал не успевает. Источник правды - сообщения самого ядра:
Код: Выделить всё
sudo dmesg -T | grep -iE "killed process|out of memory|oom-kill"
sudo journalctl -k --grep "oom-kill" --since "1 hour ago"Типичная запись выглядит так (привожу ключевые строки):
Код: Выделить всё
oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0,global_oom,task_memcg=/system.slice/myapp.service,task=python3,pid=4321,uid=1000
Out of memory: Killed process 4321 (python3) total-vm:8234560kB, anon-rss:6123456kB, file-rss:1024kB, shmem-rss:0kB, UID:1000 pgtables:12100kB oom_score_adj:0- constraint=CONSTRAINT_NONE - закончилась память во всей системе (рядом стоит метка global_oom). Если тут CONSTRAINT_MEMCG - значит уперлись в лимит cgroup, а не в физическую память сервера. Это важнейшее различие, к нему вернёмся. Бывают ещё CONSTRAINT_CPUSET и CONSTRAINT_MEMORY_POLICY - привязка к конкретным CPU/NUMA-узлам, редкость.
- task=python3, pid=4321 - имя и PID убитой жертвы.
- task_memcg=... - путь cgroup жертвы. По нему в контейнерной среде определишь конкретный под/контейнер.
- Killed process 4321 (python3) - собственно сам факт расправы.
- total-vm - сколько виртуальной памяти процесс запросил всего. Цифра часто огромная и пугающая, но сама по себе мало значит из-за overcommit: запросить можно много, занять реально - мало.
- anon-rss - вот это главное. Анонимная резидентная память: куча, стек, всё что реально занято в RAM и не сбрасывается обратно в файл. У жертвы обычно именно тут самые жирные цифры. 6 ГБ anon-rss у python3 (6123456 кБ) - вот он, виновник.
- file-rss, shmem-rss - память под файлы и разделяемые сегменты, обычно мелочь.
- pgtables - память под таблицы страниц; у процессов с гигантской и фрагментированной картой памяти может неожиданно раздуться.
- oom_score_adj - корректировка "оценки вредности", о ней ниже.
oom_score: как ядро выбирает, кого принести в жертву
Ядро не убивает случайный процесс и не обязательно того, кто попросил память последним. Оно считает каждому oom_score - грубо говоря, оценку "сколько памяти освободится, если убить именно этого". Чем больше процесс жрёт памяти относительно доступной, тем выше его балл и тем он более вероятная жертва. Логика честная: убить одного прожорливого и спасти десяток мелких. Базовый балл нормируется в диапазон 0..1000 (примерно процент от доступной памяти, которую занимает процесс).
Базовый балл считается ядром автоматически, но мы можем на него влиять через oom_score_adj в диапазоне от -1000 до +1000:
Код: Выделить всё
cat /proc/4321/oom_score # итоговая оценка процесса (0..1000)
cat /proc/4321/oom_score_adj # ручная поправка (-1000..1000)Особое значение - oom_score_adj = -1000. Это полная неприкосновенность: при таком значении итоговый oom_score обнуляется, и OOM killer такой процесс не тронет вообще, даже если он один сожрал почти всё. Защитить критичный процесс руками (значение применяется к процессу и наследуется его потомками):
Код: Выделить всё
echo -1000 | sudo tee /proc/4321/oom_score_adjКод: Выделить всё
# в [Service] юнита
OOMScoreAdjust=-1000OOM в cgroup и контейнерах: почему рестартится под в Kubernetes
Это место, где новички теряются чаще всего. Контейнеру можно задать лимит памяти, и в cgroup v2 (он по умолчанию на всех современных дистрибутивах в 2026) это файл memory.max. Когда процессы внутри контейнера упираются в лимит, ядро сначала пытается отжать что может (page cache, неактивные страницы - reclaim), а если это не помогает опуститься ниже лимита - запускает OOM killer ВНУТРИ cgroup, хотя на сервере физической памяти может быть полно. Жертва выбирается только среди процессов этого контейнера.
Именно поэтому в Kubernetes под падает с Reason: OOMKilled и Exit Code: 137 (это 128 + 9, то есть процесс получил сигнал 9, SIGKILL). Сервер при этом совершенно здоров, в системном dmesg может вообще не быть global_oom - зато будет строка с constraint=CONSTRAINT_MEMCG и путём task_memcg, указывающим на конкретный под. Это и есть "oom killed process" внутри лимита, а не из-за нехватки памяти на ноде.
Посмотреть статистику OOM по контейнеру в cgroup v2 (путь обычно вида /sys/fs/cgroup/kubepods.slice/.../...scope):
Код: Выделить всё
cat /sys/fs/cgroup/<путь>/memory.current # сколько занято сейчас
cat /sys/fs/cgroup/<путь>/memory.max # лимит (или max, если без лимита)
cat /sys/fs/cgroup/<путь>/memory.events # счётчики событийКод: Выделить всё
low 0
high 0
max 12
oom 3
oom_kill 1
oom_group_kill 1В kubectl факт OOM видно так:
Код: Выделить всё
kubectl get pod myapp -o jsonpath="{.status.containerStatuses[0].lastState.terminated.reason}"
# или человеческий вариант:
kubectl describe pod myapp # ищи Last State: Terminated, Reason: OOMKilleddmesg отвечает на вопрос "что уже случилось". Но если убийства редкие и плавающие, удобнее повесить трассировку и ждать события. В 2026 для этого штатный инструмент - eBPF, он давно вытеснил костыли с polling по логам. Готовая утилита oomkill из пакета bpftrace (а также её bcc-версия) цепляется kprobe на функцию ядра oom_kill_process() и печатает событие сразу, с временем, PID, именем и средним load average на момент убийства:
Код: Выделить всё
sudo oomkill.bt # из пакета bpftrace
# или вариант из bcc:
sudo oomkill-bpfccОтдельно стоит знать про systemd-oomd - это userspace-демон (часть systemd, по умолчанию активен в свежих Ubuntu/Fedora и ряде российских сборок). Он работает на упреждение: смотрит на PSI (Pressure Stall Information, /proc/pressure/memory) и убивает группу ДО того, как сработает медленный и грубый ядерный OOM killer. Его действия ищи отдельно, это не ядро:
Код: Выделить всё
journalctl -u systemd-oomd --since "today"
oomctl # показать текущие пороги и нагрузку по cgroupТипичные грабли и заблуждения
- "Раз OOM сработал - значит память сервера кончилась". Не обязательно. CONSTRAINT_MEMCG означает упирание в лимит cgroup, нода при этом может быть полупустой. Всегда смотри поле constraint в первую очередь.
- "Убили того, кто виноват". Нет. Ядро бьёт по самому жирному (высокий oom_score), а раздуться-то мог совсем другой процесс, который дёрнул аллокацию последним. Виновник утечки и жертва - часто разные процессы.
- "OOM killer чинит проблему". Он лишь симптом. Если процессы регулярно отстреливаются - у тебя либо утечка памяти, либо нет swap, либо неадекватный лимит контейнера, либо все вместе.
- "137 - это ошибка приложения". 137 = 128 + SIGKILL(9), чаще всего от OOM. Лезь в dmesg ноды или в kubectl describe pod, а не в логи приложения.
- "Если выключить overcommit (режим 2), OOM пропадёт". Не пропадёт совсем, но сместится: вместо внезапного убийства malloc начнёт возвращать NULL, и кривое приложение упадёт само, иногда раньше и неприятнее. Это не серебряная пуля.
- "swap не нужен, у меня памяти много". Без swap при пиках ядро лишается буфера и зовёт OOM killer резче. Немного swap (или zram/zswap, что сейчас популярно) даёт системе подышать вместо мгновенного отстрела.
Безопасно, на тестовой машине или в одноразовом контейнере, НЕ на проде:
- Посмотри текущую политику overcommit:
Код: Выделить всё
cat /proc/sys/vm/overcommit_memory - Выбери любой свой фоновый процесс, узнай его PID и глянь оценки:
Код: Выделить всё
cat /proc/$(pgrep -n bash)/oom_score cat /proc/$(pgrep -n bash)/oom_score_adj - Спровоцируй OOM в изолированном лимите cgroup, чтобы не уронить систему: (stress-ng ставится из штатных пакетов; если его нет, подойдёт любой жадный до памяти процесс под лимитом). Затем сразу глянь ядро:
Код: Выделить всё
sudo systemd-run --scope -p MemoryMax=100M stress-ng --vm 1 --vm-bytes 500M --timeout 20sи найди строку Out of memory: Killed process. Прочитай в ней anon-rss и constraint.Код: Выделить всё
sudo dmesg -T | tail -n 40 - В той же выдаче найди строку oom-kill:constraint=... и определи: это global_oom или CONSTRAINT_MEMCG? (Подсказка: ты задал MemoryMax, так что ждём MEMCG.)
- Бонус: до запуска stress-ng открой во втором терминале (если установлен bpftrace) и увидь событие убийства в реальном времени, с cgroup жертвы.
Код: Выделить всё
sudo oomkill.bt
Контрольные вопросы
- По каким полям в строке dmesg ты поймёшь, какой процесс убит и сколько РЕАЛЬНОЙ памяти он занимал, а какое поле почти бесполезно из-за overcommit?
- Чем отличается global_oom от constraint=CONSTRAINT_MEMCG и где искать причину во втором случае?
- Какое значение oom_score_adj делает процесс неприкосновенным и почему опасно ставить его всем подряд?
- Почему malloc не падает в момент вызова, а проблема всплывает позже - при чём тут overcommit и demand paging?
Процесс пропал без следов и без ошибки - первым делом sudo dmesg -T | grep -i oom либо journalctl -k --grep "oom-kill". Ищи строку Out of memory: Killed process: в ней anon-rss скажет, кто реально жрал память, а поле constraint - кончилась ли память на всём сервере (global_oom) или уперлись в лимит cgroup (CONSTRAINT_MEMCG, типичная причина рестартов и Exit Code 137 в Kubernetes). Кого убить, ядро решает по oom_score; защитить критичный процесс можно через oom_score_adj=-1000 (или OOMScoreAdjust в юните systemd), но не злоупотребляй. В 2026 для постоянного наблюдения держи под рукой oomkill.bt (eBPF, показывает и cgroup жертвы) и помни про systemd-oomd, который может убить раньше ядра. И главное: OOM killer - это не причина, а симптом. Лечить надо утечку, тесный лимит или отсутствие swap, а не сам killer.