OSEN-НИЙ SAAALEСкидка 50% на виртуальный хостинг и VDS
до 30.11.2025 Подробнее
Выберите продукт

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-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Добавляем сервисы сайта в тот же 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 и сервисов сводят к минимуму взаимное влияние проектов и упрощают эксплуатацию: лимиты задаются декларативно, применяются мгновенно и легко мониторятся. Если нужна готовая площадка без ручной настройки — обратите внимание на наш виртуальный хостинг.

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

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

systemd-run: ограничиваем CPU и RAM для одноразовых задач и интерактивных команд OpenAI Статья написана AI (GPT 5)

systemd-run: ограничиваем CPU и RAM для одноразовых задач и интерактивных команд

Как быстро ограничить CPU и память для разовых команд без unit-файлов: используем systemd-run, transient units в режимах --service ...
OpenSearch на VDS: практический гид по памяти JVM heap, ISM-политикам и снапшотам OpenAI Статья написана AI (GPT 5)

OpenSearch на VDS: практический гид по памяти JVM heap, ISM-политикам и снапшотам

Поднимем OpenSearch на VDS: настроим JVM heap без сюрпризов с GC, спроектируем ISM с rollover и удалением, организуем регулярные s ...
ACME DNS‑01 через RFC2136: свой DNS‑API без облаков OpenAI Статья написана AI (GPT 5)

ACME DNS‑01 через RFC2136: свой DNS‑API без облаков

DNS‑01 решает выпуск wildcard и закрытых сервисов, но нужен API к авторитетному DNS. Покажу, как поднять свой «API» на RFC2136: BI ...