Новинка Виртуальный VDS сервер в Финляндии от 390р
Выберите продукт

CGroup‑изоляция сайтов: собственные slices для пулов PHP‑FPM и сервисов

Разберем, как на Linux с cgroup v2 и systemd выделять каждому сайту собственный slice: вынести пулы PHP‑FPM в отдельные сервисы, объединить воркеры и cron в общий бюджет, задать лимиты CPU, памяти и IO, безопасно проверить, мониторить и при необходимости откатить.
CGroup‑изоляция сайтов: собственные slices для пулов PHP‑FPM и сервисов

Задача: справедливо делить ресурсы между сайтами

Чем больше проектов на одном сервере, тем выше риск, что один «прожорливый» сайт заберет себе 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 через systemd-cgtop и системные метрики по slices

Проверяем 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
FastFox VDS
Облачный VDS-сервер
Виртуальные серверы с быстрым запуском и гибкой конфигурацией от 390₽ / мес
Доступные локации
Россия Нидерланды Финляндия

Добавляем сервисы сайта в тот же 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)

Маршрутизация Nginx к сокетам отдельных инстансов PHP‑FPM по сайтам

Типовые пресеты лимитов

  • Сайт средней нагрузки: 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. В большинстве случаев это избыточно: изоляции пула хватает.

Откат и безопасные эксперименты

Чтобы не «положить» прод, работайте поэтапно:

  1. Создайте slice без жестких потолков, только с CPUWeight и IOWeight.
  2. Вынесите пул PHP‑FPM сайта в отдельный инстанс, проверьте доступность.
  3. Добавьте фоновый воркер в тот же slice.
  4. Включите MemoryHigh, затем умеренный MemoryMax.
  5. При необходимости — 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 и сервисов сводят к минимуму взаимное влияние проектов и упрощают эксплуатацию: лимиты задаются декларативно, применяются мгновенно и легко мониторятся. Если нужна готовая площадка без ручной настройки — обратите внимание на наш виртуальный хостинг.

Поделиться статьей

Вам будет интересно

Как безопасно выполнить bootstrap MariaDB Galera после полного останова

Как безопасно выполнить bootstrap MariaDB Galera после полного останова

После одновременной остановки всех узлов Galera обычного запуска MariaDB недостаточно. Сначала найдите сервер с последней транзакц ...
CoreDNS в Kubernetes: исправляем CrashLoopBackOff, SERVFAIL и DNS-петлю

CoreDNS в Kubernetes: исправляем CrashLoopBackOff, SERVFAIL и DNS-петлю

Разберём, почему CoreDNS перезапускается или отвечает SERVFAIL, как обнаружить замкнутую пересылку DNS-запросов, проверить доступн ...
Apache Kafka KRaft на VDS: отдельные controller и broker

Apache Kafka KRaft на VDS: отдельные controller и broker

Соберём новый Kafka-кластер без ZooKeeper на шести Linux VDS: три узла controller образуют отказоустойчивый metadata quorum, а три ...