В любой инфраструктуре, особенно на VDS, неприятная истина проста: один неудачный релиз, бесконтрольный воркер или форк‑бомба могут превратить сервер в кирпич за минуты. Даже аккуратно настроенный веб‑стек не застрахован от runaway‑процессов — потери памяти при пиковой нагрузке, лавинообразного роста потоков или 100% CPU, «съеденного» одной задачей. Современный systemd даёт в руки удобные и предсказуемые лимиты поверх cgroups v2: MemoryMax
, CPUQuota
, TasksMax
и сопутствующие директивы. Ниже — практический разбор с примерами, где и как ставить пределы, чтобы сервисы не мешали друг другу и сервер оставался управляемым.
Как systemd лимитирует ресурсы
systemd управляет ресурсами через cgroups v2. Каждый unit (чаще всего .service
) получает собственную контрольную группу, в которой ядро считает память, CPU‑время и количество задач (процессов/потоков). Лимиты задаются в unit‑файле или через drop‑ins. В отличие от классических ulimit
, лимиты systemd работают на уровне cgroup, то есть охватывают всю цепочку дочерних процессов сервиса.
Важные понятия:
- Жёсткие лимиты (например,
MemoryMax
) — при превышении процесс выбивается OOM‑килером на уровне cgroup. - Мягкие лимиты (например,
MemoryHigh
) — ядро начнёт агрессивнее отбирать страницу кеша и давить память, но сервис ещё живёт. - Квоты CPU (
CPUQuota
) — ограничение доли CPU, чтобы один сервис не доминировал при контеншне. - Лимит задач (
TasksMax
) — верхний предел процессов и потоков в unit.
Если вы всё ещё опираетесь только на
ulimit
, переведите критичные сервисы на лимиты systemd. Они надёжнее и проще контролируются: процессы не выскальзывают за пределы unit.
MemoryMax: жёсткий предел памяти
MemoryMax
задаёт верхнюю границу использования памяти сервисом и всеми его дочерними процессами. В cgroup v2 это фактически memory.max
: при достижении лимита начинается отбрасывание страниц, а затем срабатывает OOM‑килер, убивая самый «тяжёлый» процесс внутри unit. Это именно то, что спасает сервер от глобального OOM и фризов.
Дополнительно доступны:
MemoryHigh
— мягкий предел (pressure), помогает сгладить пики без немедленного убийства процессов.MemorySwapMax
— ограничение свопа для unit, если своп включён. Удобно, когда не хотите «спасаться свопом», растягивая агонию.MemoryAccounting=yes
— включает учёт памяти; обычно в новых дистрибутивах включен глобально, но лучше указывать явно в критичных unit.
Пример drop‑in для сервиса приложения: лимит 1.5 ГБ RAM и 256 МБ свопа, мягкая граница на 1.2 ГБ:
# /etc/systemd/system/myapp.service.d/limits.conf
[Service]
MemoryAccounting=yes
MemoryHigh=1200M
MemoryMax=1500M
MemorySwapMax=256M
OOMPolicy=kill
Пояснения:
OOMPolicy=kill
указывает systemd, что делать при OOM внутри unit: убить весь сервис. Это лучше, чем оставлять часть воркеров «зомби».- Реальные значения выбирайте с запасом: сложите рабочую память воркеров, буферы, плюс 20–30% на всплески.
- Профессиональный подход — сначала включить мониторинг (
MemoryAccounting
) и посмотретьMemoryCurrent
под реальной нагрузкой, затем выставитьMemoryHigh
, потомMemoryMax
.
CPUQuota: не дать одному сервису «съесть» весь процессор
CPUQuota
позволяет ограничить долю CPU, доступную unit. Значение в процентах, где 100% — одно полное ядро. На 4 vCPU сервис с CPUQuota=200%
может использовать до двух ядер. Это удобно для фоновых задач и «шумных соседей» внутри одного сервера.
Сопутствующие опции:
CPUAccounting=yes
— включает учёт CPU, чтобы видеть реальное потребление (CPUUsageNSec
).CPUWeight=1..10000
— относительный приоритет при конкуренции (cfs.weight). Не ограничивает потолок, только влияет на справедливое распределение.CPUQuotaPeriodSec=
— период квоты; по умолчанию обычно 100 мс. Иногда имеет смысл увеличивать для задач с короткими пиками.
Минимальный пример для надежного фона:
# /etc/systemd/system/worker.service.d/cpu.conf
[Service]
CPUAccounting=yes
CPUQuota=50%
CPUWeight=100
Такой воркер никогда не заблокирует фронт приложения и Nginx: при конфликте ему достанется не более половины одного ядра.
TasksMax: защита от форк‑ и тред‑бомб
TasksMax
ограничивает общее количество потоков и процессов в unit. Это защита на случай утечки потоков, неправильной конфигурации пулов или намеренного форк‑спама. При достижении лимита новые fork/clone получают ошибку, что сохраняет управляемость системы.
Рекомендации по выбору значения:
- Для веб‑сервера (Nginx) обычно достаточно 512–1024 задач.
- Для PHP‑FPM или другого пула — кратно числу воркеров + запас на вспомогательные процессы, часто 1024–2048.
- Для шины фоновых задач — исходя из максимального числа конкурирующих джоб и потоков в каждой.
Пример:
# /etc/systemd/system/php-fpm.service.d/tasks.conf
[Service]
TasksAccounting=yes
TasksMax=1500
Важно не ставить слишком низко: приложениям на JVM, .NET или с активным нативным пулом потоков может понадобиться больше, чем кажется. Ориентируйтесь на замеры в пике.
Слои изоляции: service, slice и шаблоны
Кроме настройки каждого .service
, удобно создавать свои .slice
и размещать в них родственные сервисы. Лимиты в slice
задают общий потолок и наследуются сервисами, а уже в самих сервисах можно уточнять.
# /etc/systemd/system/tenant-a.slice
[Unit]
Description=Slice for tenant A
[Slice]
MemoryMax=4G
CPUQuota=250%
TasksMax=8000
Затем привяжите сервисы:
# /etc/systemd/system/myapp.service.d/slice.conf
[Service]
Slice=tenant-a.slice
Так вы ограничите суммарное потребление группы сервисов (например, фронт, воркеры и крон одного проекта), сохраняя индивидуальные лимиты внутри. Если вы управляете воркерами через systemd‑шаблоны, дополнительно загляните в материал о практиках для очередей и воркеров — он про шаблоны, рестарты и троттлинг управления воркерами через systemd.

Политика перезапуска и «самолечение»
Лимиты — это только половина решения. Вторая — корректная политика рестартов. Если сервис падает по OOM, не нужно оставлять его мёртвым без шанса на восстановление, но и перезапускать в бесконечном цикле тоже опасно.
# /etc/systemd/system/myapp.service.d/restart.conf
[Service]
Restart=on-failure
RestartSec=5s
StartLimitIntervalSec=60
StartLimitBurst=5
Так вы ограничите «тупой» перезапуск, но дадите сервису шанс вернуться после разовой аномалии. В логах удобно отслеживать NRestarts=
и причины падений.
Глобальные настройки по умолчанию
Чтобы не повторять директивы во всех unit, можно включить учёт ресурсов глобально и задать дефолтный потолок на задачи:
# /etc/systemd/system.conf
[Manager]
DefaultCPUAccounting=yes
DefaultMemoryAccounting=yes
DefaultTasksAccounting=yes
DefaultTasksMax=10000
После изменений выполните:
systemctl daemon-reexec
Используйте разумные дефолты: достаточно высокие, чтобы не мешать обычной работе, но предотвращающие неконтролируемый рост.
Диагностика и наблюдение
Несколько полезных приёмов, когда нужно понять, «упирается» ли сервис в лимит:
systemctl status myapp
— быстрый взгляд на состояние и последние строки журнала.systemctl show -p MemoryCurrent,MemoryHigh,MemoryMax myapp
— факты по памяти.systemctl show -p CPUUsageNSec,CPUQuota,CPUWeight myapp
— факты по CPU.systemctl show -p TasksCurrent,TasksMax myapp
— количество задач и потолок.systemd-cgtop
— «top» по cgroups: видно, кто ест CPU и RAM.journalctl -u myapp -g OOM
— поиск сообщений об OOM.
Для точечной проверки квот и лимитов удобно запускать нагрузку внутри временной cgroup:
systemd-run --unit=bench --scope -p CPUQuota=50% -p MemoryMax=512M stress-ng --vm 1 --vm-bytes 700M --timeout 30s
Команда покажет, как ядро ограничит процесс по памяти и CPU. Это безопасный способ проверить будущие значения на тестовом сервере. Если параллельно усиливаете изоляцию (ProtectSystem, RestrictNamespaces и т.п.), пригодится дополнительный разбор по усилению unit’ов — смотрите харднинг сервисов systemd.
Практичные пресеты для веб‑стека
Nginx
Обычно у Nginx небольшой RSS, но важна защита от лавины воркеров при проблемной конфигурации:
# /etc/systemd/system/nginx.service.d/limits.conf
[Service]
MemoryAccounting=yes
MemoryHigh=300M
MemoryMax=500M
TasksAccounting=yes
TasksMax=1024
CPUAccounting=yes
CPUQuota=100%
Квота в 100% одного ядра не помешает сервить статику на многоядерной машине (ядро распределит нагрузку между воркерами), но не позволит Nginx забрать все ядра при сбое.
PHP‑FPM
Здесь латентные риски — крупные чанки под запросы и рост числа воркеров. Кроме unit‑лимитов, обязательно проверьте pm.max_children
и pm.max_requests
в пуле.
# /etc/systemd/system/php-fpm.service.d/limits.conf
[Service]
MemoryAccounting=yes
MemoryHigh=1.5G
MemoryMax=2G
MemorySwapMax=512M
CPUAccounting=yes
CPUQuota=200%
TasksAccounting=yes
TasksMax=2048
Если пул и так ограничен по числу воркеров, TasksMax
остаётся страховкой на случай дополнительных потоков из расширений.
Очереди и воркеры
Воркеры для задач (например, обработка изображений, экспорт, рассылки) склонны «захватывать» CPU. Дайте им потолок и мягкую память:
# /etc/systemd/system/queue-worker@.service
[Unit]
Description=Queue worker %i
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/worker --queue=%i
Restart=on-failure
RestartSec=3s
CPUAccounting=yes
CPUQuota=150%
CPUWeight=200
MemoryAccounting=yes
MemoryHigh=800M
MemoryMax=1G
TasksAccounting=yes
TasksMax=1024
[Install]
WantedBy=multi-user.target
Шаблон с инстансами позволяет масштабировать горизонтально, сохраняя те же лимиты. Если нужно разное поведение для очередей, используйте drop‑ins на конкретные инстансы.
Тонкости и подводные камни
- cgroups v2. На современных дистрибутивах они включены по умолчанию. Если используете устаревшие ядра или cgroups v1, часть поведения будет отличаться, а некоторые директивы станут недоступны.
- Page cache тоже считается. Учёт памяти включает анонимные страницы и часть кеша страниц, закреплённого за cgroup. Это нормально, просто учитывайте это в расчётах.
- Высокие пики компиляции или GC. Для JVM/.NET/Node пиковые всплески вполне нормальны. Сначала посмотрите
MemoryCurrent
под профилированной нагрузкой, и только потом ставьтеMemoryMax
. - CPUQuota и латентность. Слишком агрессивная квота может увеличить задержки в пиковых сценариях. Играйте
CPUQuotaPeriodSec
, если видите «дёрганую» производительность. - TasksMax и баги библиотек. Когда библиотека внезапно порождает десятки тысяч потоков, лимит спасёт сервер, но приложение начнёт падать на
EAGAIN
. Это ожидаемо и правильно — ищите причину и чините.
Защита от runaway‑процессов: полный чек‑лист
Соберём вместе практику, которую стоит применить к ключевым сервисам на VDS:
- Включить учёт ресурсов глобально:
DefaultCPUAccounting
,DefaultMemoryAccounting
,DefaultTasksAccounting
. - Для каждой роли сервиса задать разумные
MemoryHigh
,MemoryMax
,CPUQuota
,TasksMax
. - Выделить логические
.slice
для проектов/тенантов. Лимитировать с запасом на будущий рост. - Настроить рестарты:
Restart
,RestartSec
,StartLimit*
,OOMPolicy
. - Мониторить: собирать
MemoryCurrent
,CPUUsageNSec
,TasksCurrent
,NRestarts
в метрики и алерты. - Тестировать под нагрузкой:
systemd-run
и инструментами нагрузки проверять, как сервис ведёт себя у потолка.
Главная цель — не «урезать всем всё», а сделать так, чтобы сбой одного компонента не сносил весь сервер и не мешал остальным сервисам выполнять свою работу.
Отладка инцидентов: быстрый сценарий
Если сервер начал тормозить или один из сервисов постоянно перезапускается, выполните:
systemd-cgtop
— кто лидер по CPU и RSS прямо сейчас.systemctl show -p MemoryCurrent,MemoryMax,TasksCurrent,TasksMax culprit.service
— уткнулся ли сервис в лимит.journalctl -u culprit.service --since "-1h"
— содержит ли журнал OOM/ошибки аллокаций.- Временно поднимите
MemoryHigh
drop‑in’ом, если видите давление памяти без OOM, и наблюдайте эффект.
После стабилизации пересмотрите лимиты: возможно, реальные требования сервиса выросли, или наоборот — лимит слишком широк и позволяет утечкам копиться до критических значений.
Роль systemd‑oomd
На современных системах дополнительную защиту даёт systemd-oomd
— пользовательский демон, который реагирует на давление памяти и может предварительно завершать «прожорливые» cgroups до глобального OOM. Для него критически важно, чтобы был включён учёт ресурсов и разумно выставлены MemoryHigh
/MemoryMax
. Это не замена лимитам, а ещё один инструмент сделать отказ более контролируемым.
Итого
Лимиты systemd — это инструмент «страхования» производительности. MemoryMax
обрывает утечки и не даёт вытащить сервер в глобальный OOM, CPUQuota
сохраняет отзывчивость системы под пиками, TasksMax
спасает от форк‑ и тред‑бомб. Добавьте к этому здравую политику рестартов, слои через .slice
и наблюдаемость — и ваши сервисы на VDS будут предсказуемы даже в стрессовых сценариях. Начните с включения учёта, замерьте реальную нагрузку, задайте мягкие лимиты, а затем постепенно приближайте жёсткие — это лучший путь к стабильности без случайных даунтаймов.