С чего начать Первые шаги трек · 15 ступеней
Linux Labs

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

10 мин Ubuntu 24.04 · Linux 6.8 · systemd 255

Механизм ядра: когда память кончилась на машине или в cgroup с лимитом, завершает процесс сигналом SIGKILL.

Шпаргалка

7 команд Версия для печати
  • sudo journalctl -k -o cat | grep 'Killed process' кого убили
  • systemctl show app -p Result -p NRestarts Result=oom-kill
  • systemctl show app -p MemoryMax -p MemoryPeak лимит и пик
  • cat /sys/fs/cgroup/system.slice/app.service/memory.events
  • choom -p 1234 оценка процесса и поправка
  • sudo choom -n -500 -p 1234 беречь процесс от OOM
  • free -h available — запас памяти машины

Обзор

OOM killer (out of memory) — часть ядра, которая освобождает память, когда её больше неоткуда взять. Сначала ядро вытесняет кэш страниц и, если есть swap, редко нужные страницы программ. Когда освобождать нечего, OOM killer выбирает один процесс и завершает его сигналом SIGKILL. Этот сигнал нельзя перехватить: программа не успевает ничего записать в свой журнал.

Машина или cgroup

Память кончается в двух местах:

Где Когда Строка в журнале ядра Среди кого выбирают
вся машина кончились память и swap системы Out of memory: Killed process все процессы машины
cgroup группа упёрлась в свой лимит Memory cgroup out of memory: Killed process процессы этой группы

Лимит группы — MemoryMax= в юните systemd или --memory у контейнера (см. cgroups-namespaces). Он защищает остальную машину: служба с MemoryMax=64M упирается в свои 64 МиБ, соседи работают дальше. Поэтому OOM в cgroup случается при свободной памяти в free -h.

На всей машине отчёт другой: в строке oom-kill: стоят constraint=CONSTRAINT_NONE и global_oom, а итог — Out of memory: Killed process 1500 (python3) … oom_score_adj:1000. Так закончился python3, который занял 400 МиБ на машине без swap.

Как выбирается жертва

Каждому процессу ядро считает оценку oom_score: чем больше памяти он держит (RSS, swap, таблицы страниц), тем она выше. Поправка oom_score_adj от -1000 до 1000 сдвигает оценку: -1000 — не трогать никогда, 1000 — убивать первым. Завершают процесс с наибольшей оценкой.

text
student@ubuntu:~$ choom -p $$
pid 954's current OOM score: 673
pid 954's current OOM score adjust value: 0
student@ubuntu:~$ sudo choom -n 500 -p $$
pid 954's OOM score adjust value changed from 0 to 500
student@ubuntu:~$ choom -p $$
pid 954's current OOM score: 1006
pid 954's current OOM score adjust value: 500

Поправку службе ставит systemd из OOMScoreAdjust= юнита: у systemd-journald она -250. stress-ng ставит своим рабочим процессам 1000, чтобы под OOM попадали они.

Что пишет ядро

Отчёт OOM в журнале ядра — несколько десятков строк: стек вызовов, память группы, список задач. Главные строки находит grep:

bash
sudo journalctl -k -o cat | grep -E \
  'invoked oom|memory: usage|oom-kill:|Killed process'

Служба hog с MemoryMax=64M, в которой tail копит ввод в памяти, дала такие строки:

Строка Что из неё взять
tail invoked oom-killer: gfp_mask=0x100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0 кто просил память в момент нехватки; жертвой может стать другой процесс
memory: usage 65536kB, limit 65536kB, failcnt 597 группа упёрлась в свой лимит 64 МиБ
oom-kill:constraint=CONSTRAINT_MEMCG,…,oom_memcg=/system.slice/hog.service,…,task=tail,pid=1216,uid=0 CONSTRAINT_MEMCG — нехватка в cgroup; oom_memcg — какая группа: юнит или контейнер
Memory cgroup out of memory: Killed process 1216 (tail) total-vm:70808kB, anon-rss:64768kB, … жертва — PID и имя; anon-rss — память программы, которую она держала

Перед последней строкой ядро печатает задачи группы с их памятью в страницах по 4 КиБ (средние столбцы здесь пропущены):

text
Tasks state (memory values in pages):
[  pid  ]   uid  tgid total_vm      rss … oom_score_adj name
[   1214]     0  1214      700      448 …             0 sh
[   1215]     0  1215     1531      448 …             0 head
[   1216]     0  1216    17702    16640 …             0 tail

Журнал ядра читают root и группы adm и systemd-journal. Без sudo journalctl -k печатает -- No entries --, а dmesg — Operation not permitted.

Что видит systemd

systemd узнаёт об OOM в группе службы и останавливает службу целиком (OOMPolicy=stop по умолчанию): остальные её процессы получают SIGTERM. Итог службы — oom-kill:

text
student@ubuntu:~$ sudo journalctl -u hog -o cat
Started hog.service - /usr/bin/sh -c "head -c 300M /dev/zero | tail".
hog.service: A process of this unit has been killed by the OOM killer.
hog.service: Failed with result 'oom-kill'.
student@ubuntu:~$ systemctl show hog -p Result -p NRestarts
Result=oom-kill
NRestarts=2

С Restart=on-failure служба поднимается снова, упирается в тот же лимит, и NRestarts растёт. Счётчики группы — в файле memory.events. Служба vmhog — stress-ng под MemoryMax=96M с OOMPolicy=continue: она переживает OOM, а stress-ng поднимает рабочий процесс заново.

bash
cat /sys/fs/cgroup/system.slice/vmhog.service/memory.events
text
low 0
high 0
max 15070
oom 102
oom_kill 102
oom_group_kill 0

max — сколько раз группа упиралась в MemoryMax, oom_kill — сколько процессов в ней завершил OOM killer. Файл живёт, пока служба запущена.

Код 137 и Killed

Процесс, убитый SIGKILL, завершается с кодом 137 = 128 + 9 (см. exit-codes). Оболочка, чей потомок погиб, пишет Killed. Контейнер Docker после OOM — Exited (137) и OOMKilled: true (см. docker).

Рецепты

4

Найти, кого и когда убил OOM

bash
sudo journalctl -k -o short-iso | grep 'Killed process'
sudo journalctl -k -b -1 | grep -i oom    # прошлая загрузка
sudo dmesg -T | grep -i 'killed process'

Время из журнала сверь с журналом службы (journalctl -u <служба>) — станет видно, что служба делала перед гибелью.

Лимит памяти службы

Лимит ставят в drop-in — файле дополнения рядом с юнитом (см. drop-in):

bash
sudo mkdir -p /etc/systemd/system/demo.service.d
printf '[Service]\nMemoryMax=256M\n' \
  | sudo tee /etc/systemd/system/demo.service.d/memory.conf
sudo systemctl daemon-reload
sudo systemctl restart demo
systemctl show demo -p MemoryMax -p DropInPaths
text
MemoryMax=268435456
DropInPaths=/etc/systemd/system/demo.service.d/memory.conf

MemoryHigh= — мягкий порог: выше него ядро отбирает у группы память и тормозит её, но не убивает. stress-ng с 100 МиБ под MemoryHigh=64M отработал без OOM, но вместо 8 секунд шёл 13. Сколько служба занимает сейчас и сколько занимала больше всего — MemoryCurrent и MemoryPeak в systemctl show.

Беречь важный процесс

bash
sudo choom -n -500 -p 1234          # работающему процессу
# в юните: [Service] OOMScoreAdjust=-500

Отрицательную поправку ставит только root. Поправка сдвигает очередь, но память не добавляет: если важный процесс сам съел всё, OOM заберёт соседей, а потом и его.

Воспроизвести OOM безопасно

bash
sudo systemd-run --unit=hog -p MemoryMax=64M \
  sh -c 'head -c 300M /dev/zero | tail'

tail ждёт перевода строки и копит весь ввод в памяти, пока не упрётся в 64 МиБ своей группы. Машина при этом не страдает. Нагрузку памятью с таймером даёт и stress-ng: --vm 1 --vm-bytes 160M --timeout 10s.

Неполадки

5

Служба перезапускается по кругу

systemctl status — activating (auto-restart) (Result: oom-kill), NRestarts растёт. Служба упирается в MemoryMax при каждом запуске: лимит меньше, чем ей нужно, или память течёт. Сравни MemoryPeak с MemoryMax, подними лимит drop-in'ом или найди утечку в приложении.

Процесс пропал без единой строки в своём журнале

SIGKILL не даёт программе ничего записать. Ищи в журнале ядра: sudo journalctl -k | grep -i 'killed process'. В журнале службы останется Killed от оболочки или код 137.

Result=exit-code, status=137

Главный процесс-оболочка успел выйти сам — с кодом 137, когда OOM убил его потомка, — раньше, чем systemd получил событие OOM:

text
Killed
lab-hog.service: Main process exited, code=exited, status=137/n/a
lab-hog.service: Failed with result 'exit-code'.

Это тот же OOM. Подтверждение — строка Memory cgroup out of memory с этим временем в journalctl -k.

OOM при свободной памяти

free -h показывает память всей машины, а OOM случился в cgroup. Лимит группы — systemctl show <служба> -p MemoryMax или файл memory.max в /sys/fs/cgroup/…; у контейнера — docker inspect.

OOM убил не того

Процесс из строки invoked oom-killer только попросил память в момент нехватки. Жертву выбирают по oom_score — обычно самый большой процесс группы или машины. Сравни anon-rss убитого с остальными в таблице Tasks state.

Опасно

1

Поправка -1000 для прожорливого процесса

С oom_score_adj=-1000 процесс не трогают никогда. Если течёт именно он, OOM будет убивать всё вокруг — вплоть до sshd и журнала. Береги -1000 для процессов, чья память не растёт.

Термины

1
оценка OOM (oom_score) OOM score (oom_score)
Число, по которому OOM killer выбирает жертву: растёт с памятью процесса, поправка oom_score_adj от -1000 до 1000 сдвигает его. Видно в /proc/<pid>/oom_score и choom -p. Ещё встречается в: /proc: процессы и ядро в виде файлов, stress-ng

См. также

  • memory-swap — free, available, кэш страниц и swap.
  • cgroups-namespaces — группы и их лимиты.
  • systemd — юниты и drop-in.
  • journalctl — журнал ядра -k и журнал службы -u.
  • exit-codes — код 137 и другие коды 128+N.
  • docker — лимит памяти контейнера и OOMKilled.
  • kill — сигналы SIGTERM и SIGKILL.
Загрузка…