MAATRIX / Блог / Как ядро Linux выбирает, какой процесс убить при нехватке памяти

Как ядро Linux выбирает, какой процесс убить при нехватке памяти

MAATRIX

Сервер падает не тихо — в dmesg остаётся строка «Killed process», а вы гадаете, почему ядро выбрало именно этот процесс, а не тот, что реально сожрал память. OOM killer работает по конкретному, вычислимому алгоритму: у каждого процесса есть число-«приговор», и ядро просто убивает того, у кого оно больше. Разберём, как это число считается, как на него повлиять и что делать, чтобы механизм не выносил вам продакшен-базу вместо тестового скрипта.

Что происходит в момент нехватки памяти

OOM killer — это не сторож, который следит за общим потреблением RAM и в какой-то момент решает «пора убивать». Он включается в одной конкретной ситуации: ядру нужно выделить физическую страницу памяти под запрос (allocation), и оно не может этого сделать даже после попытки освободить место.

Порядок действий примерно такой:

  1. Процесс или сам ядро запрашивает страницу через аллокатор памяти.
  2. Если свободной памяти мало, ядро запускает reclaim: выгружает страницы файлового кэша, сбрасывает на диск «грязные» страницы, при наличии swap — выгружает туда неактивные анонимные страницы (код процессов).
  3. Если после reclaim выделить память всё ещё нельзя — ядро вызывает out_of_memory(), и вот тут начинается непосредственно выбор жертвы.

Важный нюанс — оверкоммит памяти. По умолчанию (vm.overcommit_memory=0) ядро разрешает процессам резервировать больше виртуальной памяти, чем физически есть в системе: malloc() и fork() почти всегда возвращают успех, а физическая страница выделяется только в момент реального обращения к ней (page fault). Поэтому проблема часто вылезает не в момент запроса памяти, а позже, когда кто-то из процессов начинает эту память реально трогать. Отсюда ощущение «непредсказуемости»: ядро реагирует на физический дефицит, а не на то, что вы видите в ps как «резервируемую» память.

Отдельно стоит не путать OOM killer с обычным thrashing — состоянием, когда система свопит настолько активно, что процессор большую часть времени ждёт диск. Это выглядит как зависание сервера без единой строки в логах: ядро технически ещё способно выделять память, просто крайне медленно.

Как считается oom_score

У каждого процесса есть значение в /proc/<pid>/oom_score — число от 0 до 1000, примерно отражающее долю доступной памяти системы, которую займёт именно этот процесс (со всеми потоками одной группы). В основе расчёта — не «условная резервация», а реальный физический след: резидентная память (RSS), объём, вытесненный в swap, и память под таблицы страниц. Чем больше эта сумма относительно общего объёма памяти в системе, тем выше базовый балл.

Дальше к базовому баллу применяется поправка — oom_score_adj этого процесса, число от −1000 до +1000. Поправка сдвигает итоговый score пропорционально: отрицательное значение снижает шанс стать жертвой, положительное — повышает. Крайние значения особые:

  • oom_score_adj = -1000 — процесс полностью защищён, ядро никогда не выберет его как жертву (даже если по сырому потреблению памяти он лидер).
  • oom_score_adj = 1000 — процесс гарантированно будет выбран первым при прочих равных.

Ядро проходит по всем «убиваемым» процессам (кроме защищённых через −1000 и, разумеется, кроме init/PID 1, у которого убийство запрещено на уровне ядра отдельно), считает итоговый score для каждого и выбирает максимум. Никакой магии или эвристик «этот процесс важнее по названию» тут нет — только цифры.

Посмотреть текущий счёт процесса просто:

cat /proc/$(pidof mysqld)/oom_score
cat /proc/$(pidof mysqld)/oom_score_adj

Если у вас на сервере несколько тяжёлых процессов и вы хотите заранее понять, кого убьёт OOM killer в теории, — пройдитесь по /proc/*/oom_score циклом и отсортируйте. Правда стоит помнить, что это снимок на текущий момент: как только один процесс подрастёт по памяти, картина изменится.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать VPS

oom_score_adj: ручной рычаг приоритета

Это единственный официальный способ вручную повлиять на выбор жертвы, не трогая архитектуру приложения. Изменить значение можно прямо через /proc:

echo -500 > /proc/<pid>/oom_score_adj

Установка значения более отрицательного, чем разрешено без привилегий, требует root или CAP_SYS_RESOURCE: рядовой процесс не может защитить себя от убийства сильнее, чем позволяет политика системы.

Для сервисов под systemd поправку удобнее задавать декларативно в unit-файле, а не скриптом при каждом запуске:

[Service]
OOMScoreAdjust=-500
OOMPolicy=stop

OOMPolicy — отдельная настройка про то, что делать с самим systemd-юнитом, если ядро убило один из его процессов: continue (ничего не делать), stop (остановить юнит) или kill (убить весь юнит целиком). Это удобно, когда сервис состоит из нескольких процессов и вы не хотите, чтобы половина продолжала жить в неопределённом состоянии после того, как ядро убило только один из них.

В Docker есть флаг --oom-score-adj при запуске контейнера, а также поведение по умолчанию: контейнерам с ограничением --memory демон сам выставляет поправку так, чтобы при прочих равных сначала убивались процессы внутри контейнеров, которые ближе к своему собственному лимиту, а не произвольный процесс на хосте. Если у вас несколько сервисов в контейнерах и важно управлять приоритетом явно — не полагайтесь на дефолт, задавайте --oom-score-adj или лимиты памяти осознанно; смежная тема разобрана в статье про лимиты CPU и памяти в Docker.

Что происходит в момент убийства процесса

Когда жертва выбрана, ядро вызывает oom_kill_process(), который делает несколько вещей подряд:

  1. Пишет в dmesg подробный дамп: список процессов-кандидатов с их score, суммарное состояние памяти по зонам, и итоговую строку с именем и PID выбранной жертвы.
  2. Отправляет процессу SIGKILL — не SIGTERM. У приложения нет шанса перехватить сигнал, сбросить буферы или красиво завершиться: память отбирается принудительно и немедленно.
  3. Убивает не только сам процесс, а всю его группу потоков (thread group) — то есть все потоки одного процесса гибнут вместе, потому что делят одно адресное пространство.
  4. Если процесс работает в cgroup v2 с включённым memory.oom.group, убивает сразу всю cgroup целиком — это осознанная настройка для сервисов, которые не переживают частичную смерть (например, набор процессов одного приложения, где выживший «огрызок» бесполезен или опасен).

Отдельный механизм, который стоит знать — oom_reaper, поток ядра, появившийся в версии 4.6. Раньше освобождение памяти убитого процесса могло зависнуть, если сам процесс был заблокирован на медленном I/O и не успевал среагировать на сигнал. oom_reaper асинхронно освобождает адресное пространство жертвы независимо от её состояния — память возвращается в оборот быстро, даже если процесс не мог бы сам корректно завершиться вовремя.

Практический вывод: раз это SIGKILL без шанса на graceful shutdown, любое состояние, не зафиксированное на диске к моменту убийства, теряется. Для баз данных — откат незакоммиченных транзакций через WAL, для остального — недописанные файлы. Проектировать сервис так, будто его в любой момент могут «выключить из розетки», — прямое следствие того, как работает OOM killer, а не паранойя.

Как найти в логах, кого убил OOM killer

Самый быстрый способ посмотреть, был ли на сервере OOM-килл:

dmesg -T | grep -i -E "killed process|out of memory"
journalctl -k --since "-2 days" | grep -i oom

Если journalctl собран с поддержкой регулярных выражений, удобнее фильтровать через -g:

journalctl -k -g oom --since "-2 days"

В выводе ищите итоговую строку вида «Out of memory: Killed process <pid> (<имя>) total-vm:...kB, anon-rss:...kB» — точный формат немного отличается между версиями ядра, но имя процесса, PID и объём занятой памяти там присутствуют всегда. Перед этой строкой в dmesg обычно идёт полная таблица кандидатов на момент убийства — колонки вида pid uid tgid total_vm rss pgtables_bytes swapents oom_score_adj name. Эта таблица — золото для расследования: по ней видно не только жертву, но и кто ещё был близко к тому, чтобы быть убитым, и с каким oom_score_adj каждый процесс подошёл к моменту нехватки памяти.

Дальше стандартный ход расследования — сопоставить время убийства с внешними логами: ростом трафика, конкретным запросом, началом бэкапа. Общий подход к поиску первопричины сбоя по логам разобран в статье как читать логи и находить причину сбоя. Если убийства повторяются на одном сервисе без внешних причин — проверьте не разовый всплеск, а утечку памяти на сервере: растущий RSS без освобождения между запросами рано или поздно упирается в OOM killer.

Как защитить важный процесс и снизить риск OOM

Полностью исключить OOM killer нельзя — можно только сделать его срабатывания реже и предсказуемее.

Защита конкретного процесса. Для критичного сервиса задайте отрицательный oom_score_adj через systemd (OOMScoreAdjust=) — это не спасёт процесс при реальном исчерпании всей памяти сервера (тогда killer всё равно найдёт кого убить среди оставшихся), но избавит от ситуаций, когда база данных гибнет из-за утечки в соседнем скрипте.

Изоляция через cgroup. Ограничьте память сервисов через memory.max в cgroup v2 (или лимиты в systemd-юните — MemoryMax=), чтобы прожорливый процесс упирался в лимит своей собственной группы и убивался внутри неё, не затрагивая соседей на сервере. Это особенно важно, если на одном сервере крутится несколько независимых сервисов — без лимитов один упавший в утечку процесс может утянуть за собой всех.

Разумный swap. Чем больше у ядра запаса времени на reclaim перед тем, как ему придётся звать OOM killer, тем меньше шанс, что короткий всплеск потребления памяти закончится убийством нужного процесса. Умеренный swap — это буфер именно на такие всплески, а не замена нехватки RAM. Как выбрать разумный объём под конкретный сервер, разобрано в статье про правильный размер swap для VPS и в общей инструкции swap: когда нужен и как настроить.

Оверкоммит памяти. Настройка vm.overcommit_memory=2 отключает оптимистичное резервирование: ядро честно проверяет, хватит ли реальной памяти (плюс vm.overcommit_ratio от swap), уже на этапе выделения виртуальной памяти. Поведение становится предсказуемее, но не бесплатно: приложения с тяжёлым fork() (интерпретаторы, форкающиеся под каждый воркер) могут начать получать ошибки аллокации там, где раньше это просто проходило. Компромисс, а не универсальное улучшение — включайте осознанно.

Userspace-демоны раннего реагирования. earlyoom и systemd-oomd следят за давлением на память (в том числе через PSI — Pressure Stall Information) и убивают выбранную жертву раньше, чем до дела дойдёт ядро, часто начиная с мягкого SIGTERM. Логика выбора жертвы у них своя и не обязана совпадать с алгоритмом ядра — это дополнительный, а не заменяющий механизм.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать VPS

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Почему OOM killer убил не самый прожорливый по top процесс?

Score считается не только по текущему RSS, но и с учётом oom_score_adj. Процесс с меньшим потреблением памяти, но положительной поправкой (или процесс, чей владелец не защитил его отрицательной поправкой), может обойти по итоговому баллу более тяжёлый, но защищённый процесс.

Можно ли отключить OOM killer совсем?

Нет штатного «выключателя». vm.overcommit_memory=2 меняет момент и характер отказа (ошибка аллокации вместо убийства), а vm.panic_on_oom=1 вместо выбора жертвы уронит систему в панику целиком — это годится разве что для узких сценариев, где предсказуемая перезагрузка лучше непредсказуемого убийства процесса.

OOM killer постоянно убивает базу данных, что делать?

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

В контейнере OOM killer ведёт себя иначе, чем на голом сервере?

Да. Если у контейнера задан memory.max (через --memory в Docker или лимит cgroup), при достижении этого лимита cgroup-версия OOM killer сработает внутри контейнера, даже если на хосте в целом полно свободной памяти — с точки зрения ядра это отдельная зона нехватки ресурса, а не общесистемная.

Чем OOM killer отличается от «сервер завис от нехватки памяти», но без единой строки в dmesg?

Это, скорее всего, thrashing — система всё ещё технически может выделять память, но тратит на подкачку с диска столько времени, что выглядит замороженной. Здесь OOM killer не поможет, потому что формально аллокации всё ещё проходят успешно, просто очень медленно.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →