Задача: справедливо делить ресурсы между сайтами
Чем больше проектов на одном сервере, тем выше риск, что один «прожорливый» сайт заберет себе CPU, память и диск, оставив остальных в голодном режиме. CGroup v2 плюс systemd позволяют задать справедливые лимиты и гарантии: отдельные .slice на сайт, внутри которых живут пулы PHP‑FPM, фоновые воркеры и задания. Мы получаем предсказуемую производительность, управляемые бюджеты и быстрый контроль. Для самостоятельного администрирования это особенно удобно на собственном VDS.
В статье — практическая схема: как построить иерархию slices, вынести пулы PHP‑FPM в отдельные сервисы, связать с ними другие процессы конкретного сайта, выставить лимиты CPU/Memory/IO и быстро проверить результат. Подход подходит для cgroup v2 (актуальные дистрибутивы).
Коротко о терминах: slice, service, scope
Systemd управляет cgroup через юниты:
.slice— группа с лимитами и приоритетами (может содержать другие slices, services, scopes)..service— управляемый сервис (демон), процессы которого помещаются в нужный slice..scope— произвольная группа процессов, запущенных извне или черезsystemd-run.
Иерархия закодирована в имени: дочерний slice для sites.slice называется sites-example-com.slice. Именно на slice вешаются лимиты: CPUWeight, CPUQuota, MemoryHigh, MemoryMax, IOWeight, IOReadBandwidthMax и др.
Выбор стратегии для PHP‑FPM
Если запустить все пулы в одном демоне php-fpm, они окажутся в одном и том же unit и одной cgroup — разделить ресурсы между пулами будет проблематично. Поэтому для изоляции по сайтам лучше запускать отдельные инстансы PHP‑FPM в виде шаблонного юнита php-fpm@site.service. Каждый инстанс будет привязан к своему sites-... .slice.
Это прозрачно для Nginx: каждый сайт слушает свой unix‑socket или порт, остальная конфигурация не меняется. Плюс мы можем добавить в тот же slice воркеры очередей, индексаторы и systemd-run задачи cron — все они будут делить один бюджет.

Проверяем cgroup v2 и включаем учет ресурсов
Убедитесь, что используется cgroup v2 и включен учет ресурсов в systemd. Проверки:
mount | grep cgroup2
systemd-detect-virt
systemctl show -p DefaultMemoryAccounting -p DefaultCPUAccounting -p DefaultIOAccounting systemd
Если учет по умолчанию выключен, его можно включить глобально (или задавать адресно в нужных slice):
mkdir -p /etc/systemd/system.conf.d
printf "[Manager]\nDefaultCPUAccounting=yes\nDefaultMemoryAccounting=yes\nDefaultIOAccounting=yes\n" > /etc/systemd/system.conf.d/accounting.conf
systemctl daemon-reload
systemctl restart systemd-journald
Глобально включать необязательно: мы все равно включим учет в наших .slice.
Создаем иерархию slices
Начнем с корневого slice для всех сайтов и дочернего — для конкретного домена.
printf "[Unit]\nDescription=All sites top slice\n\n[Slice]\nCPUAccounting=yes\nMemoryAccounting=yes\nIOAccounting=yes\n" > /etc/systemd/system/sites.slice
printf "[Unit]\nDescription=example.com site slice\n\n[Slice]\nCPUAccounting=yes\nMemoryAccounting=yes\nIOAccounting=yes\nCPUWeight=200\n# Пример жесткого потолка CPU: 150%% от 1 CPU (на многопроцессорной системе делится по ядрам)
CPUQuota=150%%\n# Мягкий порог памяти: троттлинг без убийства
MemoryHigh=1.5G\n# Жесткий потолок памяти: при превышении — OOM в этом slice
MemoryMax=2G\n# Доля ввода-вывода относительно других slices
IOWeight=200\n# Лимиты на количество задач (форков)
TasksMax=2048\n" > /etc/systemd/system/sites-example-com.slice
systemctl daemon-reload
systemctl start sites.slice
systemctl start sites-example-com.slice
Примечание по именованию: sites-example-com.slice автоматически становится дочерним для sites.slice, так как имя начинается с sites-.
Шаблонный юнит для PHP‑FPM на сайт
Создадим php-fpm@.service, где каждый инстанс — отдельный мастер php-fpm со своим конфигом. Все процессы инстанса попадут в sites-%i.slice (например, sites-example-com.slice).
cat > /etc/systemd/system/php-fpm@.service << 'EOF'
[Unit]
Description=PHP-FPM pool %i
After=network.target
PartOf=sites-%i.slice
[Service]
Type=notify
ExecStart=/usr/sbin/php-fpm8.2 --nodaemonize --fpm-config /etc/php/8.2/fpm/pools/%i.conf
ExecReload=/bin/kill -USR2 $MAINPID
PIDFile=/run/php/php-fpm-%i.pid
RuntimeDirectory=php-fpm-%i
RuntimeDirectoryMode=0755
Restart=on-failure
RestartSec=2s
# Привязка к сайту
Slice=sites-%i.slice
# Дополнительные страховки на уровне сервиса (наследуются от slice, но можно ужесточить)
MemoryMax=2G
TasksMax=2048
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
Пути и бинарник (php-fpm8.2) подберите под свою версию PHP.
Конфиг пула для отдельного инстанса
Для каждого сайта — отдельный конфигурационный файл. Важно: у каждого инстанса свои pid, listen, error_log и каталоги.
mkdir -p /etc/php/8.2/fpm/pools
cat > /etc/php/8.2/fpm/pools/example-com.conf << 'EOF'
[global]
pid = /run/php/php-fpm-example-com.pid
error_log = /var/log/php-fpm/example-com.error.log
include = /etc/php/8.2/fpm/pool.d/example-com.conf
[www]
; Вариант: можно держать пул в этом же файле вместо include
EOF
mkdir -p /etc/php/8.2/fpm/pool.d /var/log/php-fpm
cat > /etc/php/8.2/fpm/pool.d/example-com.conf << 'EOF'
[example-com]
user = www-data
group = www-data
listen = /run/php/php-fpm-example-com.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 30
pm.start_servers = 6
pm.min_spare_servers = 6
pm.max_spare_servers = 12
pm.max_requests = 500
request_terminate_timeout = 120s
; Пригодится для диагностики
pm.status_path = /status
slowlog = /var/log/php-fpm/example-com.slow.log
request_slowlog_timeout = 5s
php_admin_value[error_log] = /var/log/php-fpm/example-com.php.error.log
php_admin_flag[log_errors] = on
EOF
Запускаем инстанс:
systemctl enable --now php-fpm@example-com.service
systemctl status php-fpm@example-com.service
Добавляем сервисы сайта в тот же slice
Чтобы все процессы сайта делили общий бюджет, поместите их в sites-example-com.slice:
- Очереди/воркеры (например, консольные команды фреймворка) — отдельный
.serviceсSlice=sites-example-com.slice. - Одиновременные разовые задачи — через
systemd-runс--sliceили таймеры. - Если у вас отдельный веб-сервер для сайта (редко), также привяжите его к slice.
cat > /etc/systemd/system/site-worker@example-com.service << 'EOF'
[Unit]
Description=Site worker for %i
After=network.target
[Service]
Type=simple
WorkingDirectory=/var/www/%i/current
ExecStart=/usr/bin/php artisan queue:work --sleep=1 --max-jobs=1000
Restart=always
RestartSec=2s
Slice=sites-%i.slice
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now site-worker@example-com.service
Разовое выполнение в бюджете сайта:
systemd-run --slice=sites-example-com.slice --unit=site-once-example-com --property=WorkingDirectory=/var/www/example-com/current /usr/bin/php artisan schedule:run
Подробнее про организацию воркеров и restart‑политику — см. разбор в systemd‑воркеры и Supervisor для очередей.
Настройка лимитов: CPU, память, IO
CPU: вес против квоты
CPUWeight задает относительный приоритет (1–10000) в моменты конкуренции: больше вес — больше CPU‑долей. CPUQuota — жесткий потолок, например CPUQuota=150% разрешает до полутора ядер суммарно. Рекомендация: начинайте с CPUWeight для fairness и добавляйте CPUQuota, если нужен предсказуемый потолок для «шумных соседей».
Память: мягкий и жесткий пороги
MemoryHigh включает троттлинг при достижении порога (ядро замедляет аллокации и страницует), что сглаживает пики. MemoryMax — жесткий лимит (OOM внутри slice). Начинайте с разумного MemoryHigh и удерживайте MemoryMax немного выше. Учитывайте размер pm.max_children и типичные per‑child RSS. Для тонкой настройки PHP‑лимитов на уровне конфигурации посмотрите материал лимиты PHP через php.ini и .user.ini.
IO: веса и полосы
IOWeight задает относительный приоритет на блокировке. Для cgroup v2 доступны точечные лимиты на устройство: IOReadBandwidthMax, IOWriteBandwidthMax, IOReadIOPSMax, IOWriteIOPSMax. Указывайте устройство явно.
# Узнаем устройство каталога проекта
findmnt -T /var/www/example-com/current
# Допустим, это /dev/vda
# Ограничим запись до 30MB/s
systemctl set-property sites-example-com.slice IOWriteBandwidthMax=/dev/vda 30M
# Уберем лимит
systemctl set-property sites-example-com.slice IOWriteBandwidthMax=/dev/vda max
Эти свойства живут до перезагрузки. Для постоянства внесите их в файл slice.
Проверка и мониторинг
Быстро посмотреть распределение ресурсов:
systemd-cgls
systemd-cgtop
Текущие метрики по slice:
systemctl show sites-example-com.slice -p CPUUsageNSec -p MemoryCurrent -p IOReadBytes -p IOWriteBytes -p TasksCurrent
Сырые счетчики cgroup v2:
cat /sys/fs/cgroup/sites.slice/sites-example-com.slice/memory.current
cat /sys/fs/cgroup/sites.slice/sites-example-com.slice/cpu.stat
Статус инстанса PHP‑FPM и количество чилдов:
systemctl status php-fpm@example-com.service
ps -o pid,cmd,stime,rss --ppid $(cat /run/php/php-fpm-example-com.pid)

Типовые пресеты лимитов
- Сайт средней нагрузки:
CPUWeight=200, безCPUQuota,MemoryHigh=1.5G,MemoryMax=2G,IOWeight=200,pm.max_children=30. - Нагруженный API:
CPUWeight=500,CPUQuota=300%на пике,MemoryHigh=2G,MemoryMax=3G, короткиеrequest_terminate_timeout, агрессивныйpm.max_requests. - Фоновый индексатор:
CPUWeight=50,CPUQuota=50%,IOWeight=50, жесткиеIOWriteBandwidthMax.
Подход: сперва измерения, затем аккуратная фиксация потолков. Следите за 95‑м перцентилем времени ответа и относительной загрузкой CPU под конкуренцией.
Связка с Nginx и маршрутизация к пулам
Для Nginx оставьте общий сервис, но направляйте сайт к сокету своего пула:
fastcgi_pass unix:/run/php/php-fpm-example-com.sock;
fastcgi_read_timeout 120s;
Если нужен per‑site nginx (редко), создайте шаблонный nginx@.service и привяжите Slice=sites-%i.slice. В большинстве случаев это избыточно: изоляции пула хватает.
Откат и безопасные эксперименты
Чтобы не «положить» прод, работайте поэтапно:
- Создайте slice без жестких потолков, только с
CPUWeightиIOWeight. - Вынесите пул
PHP‑FPMсайта в отдельный инстанс, проверьте доступность. - Добавьте фоновый воркер в тот же slice.
- Включите
MemoryHigh, затем умеренныйMemoryMax. - При необходимости —
CPUQuotaи точечные IO‑лимиты.
Откат прост: остановите инстанс, верните сайт к старому сокету, удалите или ослабьте лимиты на slice. Не забывайте daemon-reload после правок unit‑файлов.
Подводные камни
- Слишком низкий
CPUQuota: одиночные запросы станут медленнее под нагрузкой. Лучше регулироватьCPUWeight, а квоту держать с запасом. - Жесткий
MemoryMax: неожиданные OOM при спайках. Сначала настройтеpm.max_childrenиMemoryHigh. - IO‑лимиты по устройству: указывайте правильный блок‑девайс (посмотрите
findmnt,lsblk), иначе лимит не применится. - Общий
php.ini: у инстансов могут быть разныеphp_admin_value, учитывайте это при отладке. - Контейнеры: если PHP живет в контейнере, лимиты cgroup снаружи будут на весь контейнер, внутри — свои. Следите, чтобы бюджеты не конфликтовали.
Быстрые команды на каждый день
# Посмотреть активные лимиты slice
systemctl show sites-example-com.slice | egrep 'CPU|Memory|IO|Tasks'
# Временно поднять квоту CPU до 300%%
systemctl set-property sites-example-com.slice CPUQuota=300%%
# Текущая память и задачи
systemctl status sites-example-com.slice
# Топ по потреблению
systemd-cgtop
Чеклист по внедрению
- Проверен cgroup v2 и включен учет ресурсов.
- Создан
sites.sliceи базовые политики. - Для каждого сайта — свой
sites-... .sliceс лимитами. - Пулы
PHP‑FPMзапущены отдельными инстансамиphp-fpm@site. - Сокеты Nginx указывают на «свой» пул.
- Фоновые процессы и разовые задачи запускаются внутри slice сайта.
- Наблюдение:
systemd-cgtop,systemctl show, метрики cgroup. - Есть план отката и тестовая среда для проверки лимитов.
Итог
Перенос изоляции на уровень cgroup и systemd делает работу веб‑стека предсказуемой: каждый сайт получает свой гарантированный кусок CPU, памяти и диска, а администратор — удобные рычаги управления. Собственные slices для пулов PHP‑FPM и сервисов сводят к минимуму взаимное влияние проектов и упрощают эксплуатацию: лимиты задаются декларативно, применяются мгновенно и легко мониторятся. Если нужна готовая площадка без ручной настройки — обратите внимание на наш виртуальный хостинг.


