Что такое systemd-oomd и как он предотвращает OOM
systemd-oomd, демон в составе systemd 247 и новее, следит за давлением на память через PSI и убивает процессы в проблемной cgroup раньше, чем ядро дойдёт до полного исчерпания RAM и swap. Решение принимается не по объёму свободной памяти, а по метрике memory pressure: сколько времени задачи простаивают в ожидании страниц.
Зачем это нужно на практике. Когда память заканчивается, ядро включает OOM Killer и выбирает жертву по эвристике oom_score. К этому моменту сервер уже десятки секунд молотит по свопу, все сервисы отвечают медленно, а убитым может оказаться процесс СУБД вместо разросшегося воркера. systemd-oomd реагирует на устойчивое давление и снимает всю cgroup целиком, а не один случайный процесс во всей системе.
Работает демон в пользовательском пространстве. Отсюда высокая настраиваемость: порог давления, длительность удержания условия, заполнение swap и список групп, для которых убийство разрешено.
Ключевые отличия от OOM Killer ядра
| Критерий | systemd-oomd | OOM Killer ядра |
|---|---|---|
| Момент срабатывания | Давление на память выше порога дольше заданного окна, например some 60% и 30 секунд | Ядро не может выделить страницу, reclaim и своп не помогают |
| Уровень | Пользовательское пространство, служба systemd-oomd.service | Ядро, внутри пути выделения памяти |
| Что убивает | Все процессы внутри выбранной cgroup | Один процесс с максимальным oom_score |
| Настройка | ManagedOOMMemoryPressure, ManagedOOMMemoryPressureLimit, oomd.conf | oom_score_adj, vm.overcommit_memory, vm.panic_on_oom |
| Связь со свопом | Отдельный параметр SwapUsedLimit | Своп учитывается косвенно, в эвристике badness |
| Отключение | systemctl disable --now systemd-oomd | Полностью отключить нельзя |
Практический пример разницы. Воркер приложения съел 12 ГБ при 16 ГБ RAM. OOM Killer сработает, когда свободной памяти останется ноль, и может прибить postgres. systemd-oomd увидит, что в cgroup приложения some держится на 65% дольше 30 секунд, и убьёт только эту cgroup, пока база продолжает работать.
Требования к системе: версия systemd, ядро, cgroup v2
Нужны три условия одновременно: systemd 247 или новее, ядро с поддержкой PSI (в ядре с 4.20, метрики на уровне cgroup с 5.2) и включённая унифицированная иерархия cgroup v2. Проверка занимает несколько секунд.
systemctl --version uname -r stat -fc %T /sys/fs/cgroup mount | grep cgroup2
Ответ cgroup2fs от stat означает, что v2 активна. Если вывод показывает tmpfs или mount перечисляет контроллеры cgroup v1, демон не запустится: переключите иерархию параметром ядра systemd.unified_cgroup_hierarchy=1 или cgroup_no_v1=all и перезагрузитесь.
Дистрибутивные нюансы. Fedora включает systemd-oomd с версии 33, Ubuntu 22.04 тоже активирует его по умолчанию, и именно поэтому там периодически «падали» браузеры и IDE: для пользовательских сессий порог срабатывал слишком агрессивно. Debian 12 и RHEL 9 ставят пакет, но службу нужно включать руками.
systemctl status systemd-oomd systemctl enable --now systemd-oomd
Как PSI (Pressure Stall Information) помогает принимать решения
PSI показывает, какую долю времени задачи ждут ресурс. Для памяти данные лежат в /proc/pressure/memory для хоста и в memory.pressure внутри каждой cgroup.
some avg10=0.42 avg60=1.15 avg300=0.88 total=18421302 full avg10=0.00 avg60=0.31 avg300=0.17 total=5210344
Значения avg10, avg60 и avg300 дают сглаженное давление за 10, 60 и 300 секунд, total содержит суммарное время ожидания в микросекундах с момента загрузки.
Метрики some и full: что они означают
some описывает ситуацию, когда минимум одна задача простаивает из-за нехватки памяти, остальные при этом работают. full означает, что память ждут все задачи в группе: это уже остановка полезной работы. some 10% читается так: десятую часть времени хотя бы один процесс не мог получить страницу.
systemd-oomd по умолчанию принимает решения по some. Метрика full интересна для диагностики: стабильно ненулевое full в cgroup сервиса означает, что он уже не справляется, даже если убийство ещё не сработало.
Как systemd-oomd использует PSI для выбора жертвы
Демон отслеживает только те cgroup, где явно задано ManagedOOMMemoryPressure=kill. Для каждой из них он читает memory.pressure, сравнивает some с порогом и держит условие в течение окна наблюдения. Как только давление превышает лимит дольше окна, система посылает SIGKILL всем процессам этой cgroup.
Пример с числами. Для cgroup с ManagedOOMMemoryPressureLimit=50% и стандартным окном 30 секунд убийство произойдёт при some около 55%, если такие значения держатся всё окно. Разовый всплеск на 80% в течение двух секунд срабатывания не даст. Такая логика защищает от ложных убийств при коротких пиках нагрузки.
Глобальные ориентиры лежат в /etc/systemd/oomd.conf: DefaultMemoryPressureLimit (по умолчанию 60%), DefaultMemoryPressureDurationSec (окно удержания, типовое значение 30s) и SwapUsedLimit (90% от объёма свопа). Отдельное свойство ManagedOOMSwap в systemd 252 объявлено устаревшим и удалено, за реакцию на своп теперь отвечает SwapUsedLimit.
Пошаговая настройка ManagedOOMMemoryPressure для сервисов и slices
Настройка сводится к трём действиям: проверить, что демон активен, включить политику kill для нужных unit и перечитать конфигурацию. Правки вносите по одной, с проверкой после каждого шага.
Настройка для systemd-сервисов
Разберём nginx. Создайте каталог drop-in и файл /etc/systemd/system/nginx.service.d/oomd.conf со следующим содержимым.
[Service] ManagedOOMMemoryPressure=kill ManagedOOMMemoryPressureLimit=50%
Затем перечитайте конфигурацию и перезапустите сервис.
systemctl daemon-reload systemctl restart nginx systemctl show nginx.service -p ManagedOOMMemoryPressure -p ManagedOOMMemoryPressureLimit
Свойство ManagedOOMMemoryPressureLimit задаёт индивидуальный порог в процентах, если его не указывать, применяется DefaultMemoryPressureLimit из oomd.conf. Кроме kill допустимо значение auto: действие определяется настройкой системы.
Тот же приём работает для любого сервиса с собственным cgroup. Если сервис уже ограничен через MemoryMax, комбинация получается предсказуемой: сервис сначала упирается в лимит памяти, создаёт давление в своей cgroup, после чего oomd убивает именно его, не задевая соседей. Как настраивать сами лимиты, разобрано в материале про усиление systemd-сервисов и drop-in конфиги.
Настройка для пользовательских slices
Пользовательские сессии удобно ограничивать через user@.service: файл /etc/systemd/system/user@.service.d/oomd.conf.
[Service] ManagedOOMMemoryPressure=kill ManagedOOMMemoryPressureLimit=60%
Для уже запущенной сессии примените свойство на лету, без перезапуска и без потери открытых процессов.
systemctl set-property user-1000.slice ManagedOOMMemoryPressure=kill systemctl set-property user-1000.slice ManagedOOMMemoryPressureLimit=60% systemctl show user-1000.slice -p ManagedOOMMemoryPressureLimit
Учтите: systemctl set-property пишет параметры в /etc/systemd/system.control и действует до ручного сброса через systemctl revert. Для постоянной конфигурации надёжнее drop-in файл.
Глобальные параметры в oomd.conf
Файл /etc/systemd/oomd.conf задаёт поведение демона для всего хоста.
[OOM] SwapUsedLimit=90% DefaultMemoryPressureLimit=60% DefaultMemoryPressureDurationSec=30s
После правки перезапустите демон и убедитесь, что он поднялся без ошибок.
systemctl restart systemd-oomd systemctl status systemd-oomd --no-pager
Снижать SwapUsedLimit ниже 90% стоит на серверах, где своп активно используется: если 40% свопа занято постоянно, порог в 90% сработает слишком поздно, а на диске к этому моменту будет очередь запросов.
Наблюдение за работой systemd-oomd: oomctl и journalctl
Диагностика строится на двух утилитах. oomctl показывает текущее состояние и список отслеживаемых групп, journalctl хранит историю решений демона.
Использование oomctl для диагностики
Команда без аргументов выводит общие лимиты и перечень контролируемых cgroup.
oomctl
Dry Run: no Swap Used Limit: 90.00% Default Memory Pressure Limit: 60.00% Default Memory Pressure Duration: 30s Memory Pressure Monitored CGroups: /system.slice/nginx.service /system.slice/postgresql.service /user.slice/user-1000.slice
Формат зависит от версии systemd, набор полей может отличаться. Детальный дамп с давлением по каждой группе даёт oomctl dump.
oomctl dump
Если в дампе у сервиса давление ползёт к порогу, например 45% при лимите 50%, у вас есть запас времени на разбор утечки памяти. Растущее давление при стабильном потреблении обычно указывает на нехватку CPU или медленный диск, а не на утечку.
Анализ логов через journalctl
Демон пишет в журнал решение и причину убийства.
journalctl -u systemd-oomd --since "1 hour ago" --no-pager journalctl -u systemd-oomd -f journalctl -u systemd-oomd | grep -i killed
Типовое сообщение о срабатывании содержит cgroup, фактическое давление и порог: Killed /user.slice/user-1000.slice due to memory pressure 62.10% > 60.00% for more than 30s. По этим строкам удобно считать частоту инцидентов: три убийства за сутки означают, что приложению не хватает памяти и пора пересматривать лимиты, а не порог oomd.
Для разбора инцидентов с состоянием памяти хоста пригодится материал про фильтры, хранение и ротацию логов journalctl: постоянное хранение журнала критично, потому что в tmpfs события исчезают после перезагрузки.
Детализацию логов поднимают drop-in файлом для самого демона, например /etc/systemd/system/systemd-oomd.service.d/debug.conf.
[Service] Environment=SYSTEMD_LOG_LEVEL=debug
systemctl daemon-reload systemctl restart systemd-oomd
Не держите debug включённым постоянно: записей становится заметно больше, а на дисках с ограниченным журналом они вытесняют остальные события.
Подбор порогов и безопасное тестирование
Значения по умолчанию подходят не всем. Порог ниже означает более раннее убийство и меньшее окно деградации, но выше риск потерять процесс, который мог бы пережить пик.
Как выбрать пороги для разных типов нагрузки
| Тип сервиса | ManagedOOMMemoryPressureLimit | Комментарий |
|---|---|---|
| nginx, обработчики очередей | 50-60% | Процессы быстро перезапускаются, потеря одного воркера дешева |
| СУБД (PostgreSQL, MySQL) | 70-80% | Высокий порог снижает риск убийства базы, но требует запаса RAM |
| Пользовательские сессии | 60-70% | На десктопах порог ниже 60% приводит к убийству браузеров |
| Сервисы в контейнерах с MemoryMax | 60% | Работает предсказуемо, сервис умирает внутри своего лимита |
Порядок действий: включите мониторинг, дайте системе поработать неделю, сравните давление в oomctl с реальными инцидентами и только потом опускайте порог. Если swap занят больше половины постоянно, снижайте и SwapUsedLimit, иначе реакция на своп придёт с опозданием.
Общие метрики памяти, диска и CPU под нагрузкой с пороговыми значениями собраны в руководстве по настройке производительности Linux-серверов под нагрузкой, оно помогает отделить нехватку памяти от нехватки CPU или дискового I/O.
Безопасное тестирование в изолированной среде
Проверять срабатывание oomd на рабочем сервере нельзя: убийство cgroup затронет живые процессы и может оборвать транзакции. Тестируйте на отдельной виртуальной машине или в контейнере с cgroup v2. Подойдёт недорогая VPS, например облачный сервер Timeweb Cloud, который можно создать на час и удалить после проверки.
Сценарий теста: запустите нагрузку во временном unit с включённой политикой kill и пониженным порогом.
systemd-run --unit=oomd-test --slice=oomd-test.slice -p MemoryHigh=256M \ -p ManagedOOMMemoryPressure=kill -p ManagedOOMMemoryPressureLimit=20% \ stress-ng --vm 1 --vm-bytes 1G --timeout 180s
MemoryHigh заставляет ядро тормозить группу и создавать давление, а не убивать процесс сразу. Дальше смотрите результат.
oomctl journalctl -u systemd-oomd --since "5 min ago" --no-pager
Ожидаемый итог: в журнале появляется запись об убийстве oomd-test.service, а на хосте не пострадал ни один рабочий сервис. После проверки уберите тестовый slice.
systemctl stop oomd-test.service systemctl reset-failed oomd-test.service
Альтернатива stress-ng, скрипт на Python, который выделяет и трогает страницы в цикле. Главное, чтобы нагрузка шла внутри тестовой cgroup, а не в system.slice.
Сравнение systemd-oomd и OOM Killer: что выбрать
Противопоставлять механизмы не нужно, они решают разные задачи: oomd снимает локальную перегрузку по давлению, ядро страхует от фатального исчерпания памяти.
| Ситуация | Что делать |
|---|---|
| Сервис с утечкой памяти, известный виновник просадок | Включить ManagedOOMMemoryPressure=kill и задать MemoryMax |
| Пользовательские сессии на терминальном сервере | Настроить user@.service с порогом 60-70% |
| Один разовый пик при сборке или бэкапе | Поднять DefaultMemoryPressureDurationSec, чтобы окно не совпало с пиком |
| Сервер с медленным swap на HDD | Снизить SwapUsedLimit и добавить мониторинг диска |
| Полный отказ памяти вопреки всем настройкам | OOM Killer ядра остаётся последним рубежом, отключить его нельзя |
Проверить, кто именно сработал, помогает сравнение журналов: строки systemd-oomd описывают убийство по давлению, а сообщения ядра вида Out of memory: Killed process говорят о работе классического OOM Killer. Если в логах регулярно появляется второе, пороги oomd выставлены слишком мягко или какая-то cgroup вообще не попала под контроль.
Заключение
systemd-oomd закрывает разрыв между «сервер тормозит из-за свопа» и «ядро прибило не тот процесс». Минимальный рабочий набор: убедиться в наличии cgroup v2 и ядра 5.4+, включить службу, прописать ManagedOOMMemoryPressure=kill для сервисов и пользовательских slices, выставить пороги 50-70% и проверить результат на тестовой ВМ через stress-ng, oomctl и journalctl.
Начинайте с одного сервиса, ведите наблюдение неделю и только затем расширяйте список контролируемых групп. Дополнительно полезно ограничить память самих сервисов: материал про метрики CPU, памяти, диска и сети помогает поймать рост потребления до того, как он превратится в инцидент.