OOM killer: кто и за что убил процесс

Рейтинг: 55.2% · 12 голосов
Подробный курс по диагностике и производительности Linux для админов и DevOps: с чего начать, методология USE, логи (journalctl, dmesg), процессы, CPU, память и OOM, диск и IO (iostat), сеть (ss, tcpdump), системные вызовы и strace, ltrace, perf, флеймграфы, ftrace, eBPF и bpftrace, lsof, мониторинг. Разбор каждого инструмента с чтением вывода.
Ответить
Аватара пользователя
Dmitry_SRE
Сообщения: 47
Зарегистрирован: 11 май 2026, 05:31

OOM killer: кто и за что убил процесс

Сообщение Dmitry_SRE »

Оглавление курса (47)
  1. С чего начать диагностику Linux: методология вместо паники
  2. Первые 60 секунд: экспресс-диагностика нагруженного сервера
  3. Методологии диагностики: USE, RED и здравый смысл
  4. Логи systemd через journalctl: где искать причину
  5. Где лежат логи Linux: /var/log, dmesg и rsyslog
  6. Источники правды: load average, /proc и /sys
  7. Процессы Linux: ps, pstree и состояния процессов
  8. Интерактивный мониторинг: top и htop
  9. Метрики процессов во времени: pidstat
  10. Приоритеты и ограничения: nice, ionice, cgroups
  11. Сигналы и зависшие процессы: kill, и что делать с D-state
  12. Загрузка CPU: user, system, iowait, steal и контекст-свитчи
  13. Диагностика CPU по ядрам: vmstat и mpstat
  14. Профилирование CPU: perf top и поиск пожирателя
  15. Частоты, троттлинг и NUMA: turbostat и numastat
  16. Память Linux: RSS, VSZ, page cache и миф о нехватке памяти
  17. Сколько памяти занято: free, /proc/meminfo и vmstat
  18. Поиск утечек и пожирателей памяти: smem, pmap, smaps
  19. Swap и подкачка: swapon, swappiness, когда своп - это боль
  20. OOM killer: кто и за что убил процесс (вы здесь)
  21. Память ядра и slab: slabtop и куда уходит RAM
  22. Подсистема ввода-вывода: путь запроса от приложения до диска
  23. Диагностика диска: iostat и чтение await, %util
  24. Кто грузит диск: iotop и атрибуция io процессам
  25. Файловые системы: df, du, иноды и куда делось место
  26. Глубокая диагностика блочного слоя: blktrace и biolatency
  27. Кеш страниц и грязные данные: dirty pages, fsync, drop_caches
  28. Сетевой стек Linux: путь пакета и где возникают задержки
  29. Сокеты и соединения: ss и netstat
  30. Захват трафика: tcpdump для диагностики сети
  31. Задержки и потери в сети: ping, mtr, диагностика латентности
  32. Сеть как источник проблем: nftables, conntrack, дропы
  33. Что такое системный вызов и зачем его трассировать
  34. strace: трассировка системных вызовов на практике
  35. strace в бою: почему программа висит, падает или тормозит
  36. ltrace: трассировка вызовов библиотек
  37. Накладные расходы трассировки и безопасные альтернативы
  38. perf: универсальный профайлер Linux
  39. Флеймграфы: визуализация профиля производительности
  40. Трассировка ядра: ftrace и trace-cmd
  41. eBPF: революция в наблюдаемости Linux
  42. Инструменты eBPF на практике: BCC и bpftrace
  43. lsof: открытые файлы, дескрипторы, кто держит файл и порт
  44. Разбор кейса: приложение тормозит - пошаговая диагностика
  45. Непрерывный мониторинг: sar, node_exporter, Grafana, Zabbix
  46. Диагностика в контейнерах: cgroups, лимиты, Docker и Kubernetes
  47. Карта инструментов и сквозной разбор инцидента производительности
Знакомая ситуация: сервис просто пропал. В мониторинге дыра, в логах приложения - тишина, последняя строчка обрывается на полуслове, как будто кто-то выдернул вилку. Приложение не падало с ошибкой, не писало стектрейс, оно просто исчезло. Если ты гуглишь "убил процесс linux" или "почему процесс умер без следов" - велик шанс, что это работа OOM killer. Out of memory killer - это механизм ядра, который при нехватке памяти выбирает жертву и убивает её, чтобы спасти систему целиком. В этом уроке разберём, как понять, что виноват именно он, как ядро выбирает кого убить, как читать его сообщения по полям, и почему в Kubernetes контейнеры рестартятся "на ровном месте". Всё актуально на 2026 год: современный Linux с systemd, cgroup v2 по умолчанию, плюс новый игрок - systemd-oomd.

Зачем вообще ядру кого-то убивать

Когда программа просит память через 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 срабатывает гораздо реже - но приложения обязаны корректно обрабатывать отказ в выделении памяти, иначе будут падать сами.
Посмотреть, сколько ядро в режиме 2 ещё готово отдать, можно так:

Код: Выделить всё

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"
Флаг -T у dmesg переводит сырые секунды аптайма в человекочитаемое время (с оговоркой: при переводе часов или ресинхронизации NTP метки могут немного "плыть"). journalctl -k показывает только сообщения ядра (это фильтр _TRANSPORT=kernel) и удобен, когда нужно посмотреть за вчера, а не только то, что ещё лежит в кольцевом буфере, который перетирается. Опция --grep у journalctl фильтрует по тексту самого сообщения - чище, чем гнать всё через внешний grep.

Типичная запись выглядит так (привожу ключевые строки):

Код: Выделить всё

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 - корректировка "оценки вредности", о ней ниже.
Чуть выше этих строк ядро печатает целую таблицу всех задач с колонками pid, rss, swapents, oom_score_adj. Это золото: видно не только убитого, но и кто ещё раздулся и стоял в очереди. Отсортируй мысленно по rss - и поймёшь, утечка это в одном процессе или общая деградация.

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)
Поправка фактически смещает итоговый балл (по сути это процентные пункты от объёма памяти). Поставишь +1000 - сделаешь процесс первоочередным кандидатом на отстрел. Поставишь отрицательное число - наоборот, защитишь.

Особое значение - oom_score_adj = -1000. Это полная неприкосновенность: при таком значении итоговый oom_score обнуляется, и OOM killer такой процесс не тронет вообще, даже если он один сожрал почти всё. Защитить критичный процесс руками (значение применяется к процессу и наследуется его потомками):

Код: Выделить всё

echo -1000 | sudo tee /proc/4321/oom_score_adj
Для сервиса под systemd то же самое делается постоянно и правильно - через unit-файл, а не руками после каждого рестарта:

Код: Выделить всё

# в [Service] юнита
OOMScoreAdjust=-1000
Но осторожно: если назначить неприкосновенными слишком многих, ядру может стать некого убить, и тогда оно либо паникует (kernel panic, если включён vm.panic_on_oom), либо уходит в бесконечный reclaim и система фактически зависает вместо того чтобы пожертвовать одним процессом. Защищай точечно - СУБД, sshd для аварийного доступа, ключевой сервис. Всё подряд защищать нельзя. И помни обратное: для жирных, но необязательных батчей (cron-задачи, отчёты) полезно ставить положительный adj, чтобы под нож пошли именно они.

OOM в 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    # счётчики событий
В memory.events интересны строки:

Код: Выделить всё

low 0
high 0
max 12
oom 3
oom_kill 1
oom_group_kill 1
oom_kill больше нуля - значит внутри лимита кого-то уже убивали (это самый надёжный маркер cgroup-OOM, его удобно скрейпить в мониторинг). Поле max - сколько раз приложение упёрлось в потолок и было задавлено reclaim, даже без убийств; растёт max - значит лимит тесный, под на грани. На Kubernetes 1.28+ с cgroup v2 по умолчанию включён memory.oom.group=1: тогда убивается не один процесс, а сразу вся группа процессов контейнера (отдельный счётчик oom_group_kill), чтобы не остался "полуживой" под, где главный процесс жив, а воркеры выкошены. Кстати, в cgroup v1 как раз была классическая ловушка: убивали дочерний процесс, главный (PID 1 контейнера) жил, и Kubernetes даже не помечал под как OOMKilled - в v2 это вылечено.

В kubectl факт OOM видно так:

Код: Выделить всё

kubectl get pod myapp -o jsonpath="{.status.containerStatuses[0].lastState.terminated.reason}"
# или человеческий вариант:
kubectl describe pod myapp   # ищи Last State: Terminated, Reason: OOMKilled
eBPF: ловим OOM в реальном времени (актуально на 2026)

dmesg отвечает на вопрос "что уже случилось". Но если убийства редкие и плавающие, удобнее повесить трассировку и ждать события. В 2026 для этого штатный инструмент - eBPF, он давно вытеснил костыли с polling по логам. Готовая утилита oomkill из пакета bpftrace (а также её bcc-версия) цепляется kprobe на функцию ядра oom_kill_process() и печатает событие сразу, с временем, PID, именем и средним load average на момент убийства:

Код: Выделить всё

sudo oomkill.bt        # из пакета bpftrace
# или вариант из bcc:
sudo oomkill-bpfcc
Важное обновление: с сентября 2025 oomkill.bt умеет показывать ещё и memory cgroup жертвы - то есть прямо в выводе видно, в каком контейнере произошло убийство, без раскопок task_memcg в dmesg. Требуется root и относительно свежее ядро (5.x+, на 2026 это норма). Это удобно держать запущенным на ноде, которая загадочно рестартит поды.

Отдельно стоит знать про systemd-oomd - это userspace-демон (часть systemd, по умолчанию активен в свежих Ubuntu/Fedora и ряде российских сборок). Он работает на упреждение: смотрит на PSI (Pressure Stall Information, /proc/pressure/memory) и убивает группу ДО того, как сработает медленный и грубый ядерный OOM killer. Его действия ищи отдельно, это не ядро:

Код: Выделить всё

journalctl -u systemd-oomd --since "today"
oomctl                 # показать текущие пороги и нагрузку по cgroup
Если процесс пропал, а в dmesg чисто - проверь, не systemd-oomd ли постарался.

Типичные грабли и заблуждения
  • "Раз 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, чтобы не уронить систему:

    Код: Выделить всё

    sudo systemd-run --scope -p MemoryMax=100M stress-ng --vm 1 --vm-bytes 500M --timeout 20s
    (stress-ng ставится из штатных пакетов; если его нет, подойдёт любой жадный до памяти процесс под лимитом). Затем сразу глянь ядро:

    Код: Выделить всё

    sudo dmesg -T | tail -n 40
    и найди строку Out of memory: Killed process. Прочитай в ней anon-rss и constraint.
  • В той же выдаче найди строку oom-kill:constraint=... и определи: это global_oom или CONSTRAINT_MEMCG? (Подсказка: ты задал MemoryMax, так что ждём MEMCG.)
  • Бонус: до запуска stress-ng открой во втором терминале

    Код: Выделить всё

    sudo oomkill.bt
    (если установлен bpftrace) и увидь событие убийства в реальном времени, с cgroup жертвы.
На Astra Linux и RED OS всё работает идентично - это стандартный механизм ядра Linux, дистрибутив роли не играет. Различаться может разве что путь к cgroup, набор предустановленных утилит (bpftrace/bcc иногда надо доставить) и наличие systemd-oomd.

Контрольные вопросы
  • По каким полям в строке 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.
👍2 ❤️4 🔥1 😄 🤔1
✔ Лучший ответ сформирован автоматически — Kracker9
Блин, вот оно. У меня под в кубе раз в день рестартился с Exit 137, а я в логи приложения смотрел и ничего не понимал. Полез в describe pod - там OOMKilled, а на ноде памяти вагон. Теперь ясно, это лимит memory.max и constraint=MEMCG, а не нода. Запустил ещё oomkill.bt на ноде - ловит сразу с именем cgroup, кайф.
Перейти к ответу →
Аватара пользователя
Kracker9
Сообщения: 1
Зарегистрирован: 24 май 2026, 18:03

Re: OOM killer: кто и за что убил процесс

Сообщение Kracker9 »

✔ Лучший ответ — сформирован автоматически
Блин, вот оно. У меня под в кубе раз в день рестартился с Exit 137, а я в логи приложения смотрел и ничего не понимал. Полез в describe pod - там OOMKilled, а на ноде памяти вагон. Теперь ясно, это лимит memory.max и constraint=MEMCG, а не нода. Запустил ещё oomkill.bt на ноде - ловит сразу с именем cgroup, кайф.
👍2 ❤️1 🔥 😄 🤔1
Аватара пользователя
pgfan
Сообщения: 1
Зарегистрирован: 16 май 2026, 12:26

Re: OOM killer: кто и за что убил процесс

Сообщение pgfan »

Вопрос новичка: правильно ли понял, что anon-rss это реально занятая память, а total-vm можно почти игнорировать? А то у меня в dmesg total-vm 12 гигов, а на сервере всего 8, и я сначала запаниковал, пока не дочитал про overcommit.
👍2 ❤️2 🔥 😄 🤔
Ответить
← Предыдущая глава
Swap и подкачка: swapon, swappiness, когда своп - это боль
Следующая глава →
Память ядра и slab: slabtop и куда уходит RAM

Все главы курса «Диагностика и производительность Linux: логи, strace, perf и eBPF»

Поделиться темой: ✈ Telegram VK
Похожие запросы: как посмотреть сколько памяти занято в linuxperf top как найти что грузит процессорчто такое системный вызов и зачем трассироватькак посмотреть процессы в linux и убить зависшийчто такое load average в linux и какое значение нормальноеoom killer кто убил процесс linux как узнать

Вернуться в «Диагностика и производительность Linux: логи, strace, perf и eBPF»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость