Освоение политики OOM: настройка реакции systemd на нехватку памяти

Научитесь управлять поведением OOM-киллера Linux с помощью systemd. Это руководство рассматривает директивы `OOMScoreAdjust` и `OOMPolicy` для защиты критических сервисов, влияя на то, какие процессы будут завершены при нехватке памяти. Освойте настройку OOM в systemd для повышения стабильности и отказоустойчивости системы.

Освоение политики OOM: настройка реакции systemd на нехватку памяти

Сбои из-за нехватки памяти редко происходят в удобное время. Пакетный импорт получает файл больше обычного, сервис "утекает" память за ночь, резервное копирование накладывается на пик трафика, или развертывание удваивает количество рабочих процессов. Когда Linux не может освободить достаточно памяти для выделения, ядро может вызвать OOM-киллера и завершить процесс, чтобы машина могла продолжать работу.

Неприятная часть заключается в том, что жертвой по умолчанию может оказаться не тот сервис, который вы бы выбрали. На общем хосте вы, возможно, предпочтете, чтобы перезапускаемый рабочий процесс очереди умер раньше, чем основной API. На сервере базы данных вы, вероятно, захотите, чтобы SSH и мониторинг остались живы, чтобы вы могли восстановить машину. Systemd предоставляет вам две ручки для такого рода решений: OOMScoreAdjust= и OOMPolicy=.

OOMScoreAdjust= влияет на то, какой процесс будет выбран. OOMPolicy= управляет тем, что systemd делает после того, как процесс в сервисе был убит. Они решают разные проблемы, и их путаница приводит к плохим runbook'ам.

Что оценивает ядро

Каждый процесс Linux имеет OOM-оценку, видимую в /proc/<pid>/oom_score. Более высокий балл означает, что процесс с большей вероятностью станет жертвой OOM. Ядро выводит эту оценку из использования памяти и другого контекста, а затем применяет значение корректировки из /proc/<pid>/oom_score_adj.

Параметр systemd OOMScoreAdjust= записывает эту корректировку для процессов, которые он запускает. Диапазон: от -1000 до 1000.

  • -1000 обеспечивает самую сильную защиту и фактически отключает OOM-убийство для этого процесса.
  • Отрицательные значения делают процесс менее вероятным для убийства.
  • Положительные значения делают процесс более вероятным для убийства.
  • 0 оставляет корректировку нейтральной.

Самый безопасный подход обычно заключается не в "защите всего важного". Если каждый сервис защищен, у ядра остается меньше полезных вариантов, когда на хосте уже не хватает памяти. Защитите небольшое количество сервисов и сделайте одноразовую работу более легкой для убийства.

Для основного сервиса API часто достаточно умеренной корректировки:

[Service]
OOMScoreAdjust=-300

Для рабочего процесса очереди, который может повторять задания:

[Service]
OOMScoreAdjust=500

Этот рабочий процесс может умереть первым при нехватке памяти, но в этом и суть. Неудачное задание может вернуться в очередь. Мертвая база данных или недоступный хост — это более крупный инцидент.

Что на самом деле делает OOMPolicy

OOMPolicy= не помечает юнит как "критический" и не выбирает первый процесс для убийства. Поддерживаемые значения: continue, stop и kill.

  • continue: systemd регистрирует событие OOM и оставляет юнит работающим, если остались какие-либо процессы.
  • stop: systemd регистрирует событие и чисто останавливает юнит.
  • kill: если один процесс в юните был убит OOM, оставшиеся процессы в этом юните убиваются как группа.

Используйте этот параметр, чтобы избежать полуживых сервисов. Если многопроцессный веб-сервис теряет рабочий процесс и продолжает принимать трафик в сломанном состоянии, continue может скрыть сбой. OOMPolicy=kill делает сбой очевидным и позволяет Restart=on-failure вернуть сервис в чистое состояние.

[Service]
OOMPolicy=kill
Restart=on-failure
RestartSec=5s

Для пакетного задания со вспомогательными процессами stop может быть менее резким для оставшихся процессов:

[Service]
OOMPolicy=stop

Процесс, выбранный ядром, уже исчез. stop влияет только на то, что systemd делает с остальной частью сервиса, поэтому не полагайтесь на это как на изящную точку сохранения. Долго работающие задания должны сами контролировать свои контрольные точки.

Практический шаблон настройки

Начните с сортировки сервисов на три группы.

Во-первых, определите сервисы, которые поддерживают возможность восстановления хоста: SSH, сеть, мониторинг и основная рабочая нагрузка. Дайте только самым важным из них скромные отрицательные корректировки.

Во-вторых, определите сервисы, которые можно повторить: рабочие процессы, импортеры, генераторы отчетов, обработчики изображений, прогреватели кэша и вспомогательные средства разработки. Дайте им положительные корректировки.

В-третьих, решите, может ли каждый сервис безопасно продолжать работу после того, как один процесс был убит. Если нет, используйте OOMPolicy=kill и политику перезапуска.

Реалистичное переопределение рабочего процесса может выглядеть так:

# /etc/systemd/system/image-worker.service.d/oom.conf
[Service]
OOMScoreAdjust=500
OOMPolicy=kill
Restart=on-failure
RestartSec=10s

Основной сервис приложения может выглядеть так:

# /etc/systemd/system/api.service.d/oom.conf
[Service]
OOMScoreAdjust=-300
OOMPolicy=kill
Restart=on-failure
RestartSec=5s

Я бы избегал OOMScoreAdjust=-1000, если вы не протестировали режим отказа. Если этот защищенный сервис является тем, который "утекает" память, машине все равно нужен способ восстановления.

Применение и проверка изменений

Используйте drop-in'ы вместо редактирования упакованных файлов юнитов:

sudo systemctl edit api.service

После сохранения переопределения перезагрузите systemd и перезапустите сервис:

sudo systemctl daemon-reload
sudo systemctl restart api.service

Проверьте объединенный юнит и значения, которые видит systemd:

systemctl cat api.service
systemctl show api.service -p OOMPolicy -p OOMScoreAdjust

Затем проверьте запущенный процесс:

PID=$(systemctl show api.service -p MainPID --value)
cat /proc/$PID/oom_score_adj
cat /proc/$PID/oom_score

oom_score_adj должен соответствовать вашей настроенной корректировке. oom_score может меняться по мере того, как процесс использует больше или меньше памяти.

После инцидента проверьте как журналы юнита, так и журнал ядра:

journalctl -u api.service --since "1 hour ago"
journalctl -k --since "1 hour ago" | grep -i oom

В системах, использующих systemd-oomd, также проверьте:

systemctl status systemd-oomd
oomctl

Политика OOM — это не планирование емкости

Настройка OOM — это последняя линия обороны. Вам все еще нужны лимиты памяти, оповещения и достаточный запас для нормальных пиков. Для сервисов с предсказуемыми границами рассмотрите контроль памяти cgroup:

[Service]
MemoryHigh=1500M
MemoryMax=2G

MemoryHigh= применяет давление до жесткого лимита. MemoryMax= — это потолок. Точное поведение зависит от версии systemd и настройки cgroup, но операционная идея проста: изолировать один сервис до того, как он потребит весь хост.

Swap заслуживает такого же внимания. Отсутствие swap может превратить короткие пики в резкие OOM-убийства. Слишком много медленного swap может поддерживать хост в живых, в то время как задержка становится бесполезной. Пересмотрите политику OOM вместе с swap, лимитами памяти, поведением при перезапуске и оповещениями.

Пример: один хост, три сервиса

Предположим, небольшой production-хост запускает API, кэш Redis и фоновый рабочий процесс отчетов. Рабочий процесс отчетов полезен, но он может повторить работу. Redis улучшает задержку, но приложение все еще может обслуживать некоторые запросы, обращаясь к базе данных. API — это сервис, ориентированный на клиентов.

Разумный первый подход может быть таким:

# api.service
[Service]
OOMScoreAdjust=-300
OOMPolicy=kill
Restart=on-failure
# redis.service drop-in, если этот экземпляр Redis является только кэшем
[Service]
OOMScoreAdjust=0
OOMPolicy=kill
# report-worker.service
[Service]
OOMScoreAdjust=600
OOMPolicy=kill
Restart=on-failure

Это не гарантирует, что рабочий процесс умрет первым в каждом возможном случае, но это делает ваше намерение ясным. Если рабочий процесс отчетов становится слишком большим, он является более легкой целью. Если API теряет один из своих процессов, systemd убивает остальные и чисто перезапускает его. Если Redis — это только кэш, вы можете решить не защищать его сильно; если Redis — ваше основное хранилище данных, вы примете другое решение.

Вот почему политика OOM должна быть привязана к роли сервиса, а не к имени продукта. "Redis" не является автоматически критическим или одноразовым. "Кэш, который мы можем перестроить" и "единственная копия состояния сессии" — это разные операционные объекты.

Тестирование без создания катастрофы

Вам не нужно крашить production-сервер, чтобы узнать, применены ли настройки. Начните с проверки:

systemctl show report-worker.service -p OOMScoreAdjust -p OOMPolicy
systemctl status report-worker.service

Затем проверьте запущенный процесс:

PID=$(systemctl show report-worker.service -p MainPID --value)
cat /proc/$PID/oom_score_adj

Для более глубокого тестирования используйте staging-хост или одноразовую виртуальную машину с той же версией systemd и режимом cgroup. Запускайте там контролируемый инструмент создания нехватки памяти, а не на общем production-сервере. Цель — подтвердить общее поведение: рабочий процесс легче убить, основной сервис не остается полуживым, и поведение перезапуска видно в журнале.

Если вы используете контейнеры, тестируйте в той же форме, в которой развертываете. Сервис, работающий непосредственно под systemd, ведет себя не так, как процесс внутри контейнера с собственным лимитом памяти. Ядро может применить лимит контейнера до того, как на хосте глобально закончится память. В этом случае ваша среда выполнения контейнера, Kubernetes или настройки cgroup могут быть первым уровнем, который решает, что умрет.

Чтение инцидента впоследствии

После события OOM избегайте прыгать сразу к "нам нужно больше ОЗУ". Иногда это так. Иногда кэш забыл TTL. Иногда развертывание изменило параллелизм рабочих процессов. Иногда активность персистентности или резервного копирования вызвала скачок памяти с копированием при записи.

Ищите три вещи:

journalctl -k --since "2026-05-24 01:00" | grep -i oom
journalctl -u api.service --since "2026-05-24 01:00"
systemctl show api.service -p Result -p NRestarts

Журнал ядра обычно сообщает вам, какой процесс был убит. Журнал юнита сообщает вам, как отреагировал systemd. Счетчики перезапусков сообщают вам, восстановился ли сервис чисто или "хлопал".

Затем сравните убитый процесс с вашим предполагаемым приоритетом. Если защищенный сервис умер раньше одноразового рабочего процесса, проверьте, действительно ли рабочий процесс работал под настроенным вами юнитом, было ли загружено переопределение и не сработал ли сначала другой лимит памяти. Если выбранная жертва соответствует политике, но инцидент все равно навредил пользователям, ваша классификация сервисов, возможно, нуждается в изменении.

Документируйте причину, а не только значение

Настройки OOM легко забыть, потому что они тихо сидят в drop-in'ах юнитов до плохого дня. Оставьте короткий комментарий в переопределении или в вашем репозитории инфраструктуры, объясняющий причину корректировки.

[Service]
# Перезапускаемый рабочий процесс очереди. Предпочтительнее убивать этот перед api.service при нехватке памяти на хосте.
OOMScoreAdjust=600
OOMPolicy=kill

Этот комментарий экономит время во время разбора инцидента. Без него кто-то может увидеть положительный OOM-балл и "исправить" его обратно на ноль, не осознавая, что это было преднамеренное решение о приоритете.

Также записывайте, когда вы в последний раз пересматривали настройку. Сервис может со временем менять роли. Рабочий процесс, который когда-то обрабатывал одноразовые миниатюры, позже может обрабатывать платежи, экспорт или видимые клиентом задания. Политика OOM должна следовать текущему риску, а не первоначальному назначению сервиса.

Распространенные плохие конфигурации

Одна плохая конфигурация — защита базы данных, API, рабочего процесса, кэша, отправителя журналов и агента мониторинга одновременно. Это кажется осторожным, но это дает ядру меньше вариантов. Выбирайте приоритеты.

Другая плохая конфигурация — установка OOMPolicy=continue на сервисе, который не может терпеть отсутствие дочерних процессов. Менеджер процессов, веб-сервер или пользовательский демон могут держать юнит активным, даже если часть рабочей нагрузки исчезла. Если ваш балансировщик нагрузки проверяет только, открыт ли порт, трафик может продолжать поступать к деградировавшему сервису.

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

Наконец, избегайте скрытия событий OOM одними только автоматическими перезапусками. Перезапуск "утекающего" сервиса может выиграть время, но он также может создать цикл, в котором память растет, сервис умирает, и пользователи видят периодические сбои. Добавьте оповещения о количестве перезапусков и росте памяти, а не только о состоянии процесса.

Краткий Runbook

Когда вы настраиваете реальный сервер, используйте повторяемый контрольный список:

  1. Перечислите сервисы, необходимые для восстановления и пользовательского трафика.
  2. Перечислите перезапускаемые сервисы, которые могут быть убиты первыми.
  3. Добавьте положительные значения OOMScoreAdjust для одноразовой работы.
  4. Добавьте умеренные отрицательные значения только для тех немногих сервисов, которые заслуживают защиты.
  5. Используйте OOMPolicy=kill для сервисов, которые не должны работать частично.
  6. Проверьте примененные значения через systemctl show и /proc.
  7. Настройте оповещения о нехватке памяти до того, как произойдут события OOM.

Цель состоит не в том, чтобы сделать события OOM безвредными. Цель состоит в том, чтобы сделать их понятными. OOMScoreAdjust= помогает выбрать жертву. OOMPolicy= помогает определить, что происходит с остальной частью юнита. Вместе они дают вам более предсказуемый порядок отказов, когда память уже исчерпана.