Зачем администратору разбираться в cgroups v2, даже если «есть контейнеры»
Если на одном сервере или VDS живут несколько сервисов, воркеры, бэкенды и периодические джобы, то рано или поздно один «шумный сосед» начнёт портить жизнь всем остальным: съест CPU, выжмет память до OOM, упрётся в диск и начнёт растягивать задержки. В Linux это решается через control groups (cgroups). А в современных дистрибутивах роль «дирижёра» cgroups чаще всего играет systemd.
Де-факто стандарт сегодня — cgroups v2: единая иерархия, более предсказуемое распределение ресурсов и нормальная интеграция с ядром и systemd. При этом многие привычные вещи из v1 переехали, переименовались или стали работать иначе (особенно заметно на I/O и делегировании контроллеров).
Ниже — практический путь: как мыслить терминами systemd slices, как применять MemoryMax/MemoryHigh, CPUQuota/CPUWeight, где уместен io.max через systemd-параметры IOPS/bandwidth, как «читать боль» через PSI и что делает systemd-oomd.
Быстрая проверка: у вас точно cgroups v2?
На большинстве актуальных Debian/Ubuntu/Alma/Rocky — да, но лучше убедиться. Самый простой индикатор: смонтирован ли /sys/fs/cgroup как cgroup2.
stat -fc %T /sys/fs/cgroup
Если увидите cgroup2fs — это v2. Если будет что-то вроде tmpfs (и рядом лежат каталоги вида cpu/memory как в v1), значит у вас гибрид/legacy, и поведение лимитов может отличаться.
Полезно сразу посмотреть дерево, которое построил systemd:
systemd-cgls
Эта команда часто моментально объясняет, «кто кому сосед» и куда утекают ресурсы.
Модель systemd: unit → slice → cgroup
Чтобы уверенно управлять лимитами, важно принять простую схему:
- unit (service, scope, slice) — сущность systemd, у которой могут быть resource-лимиты;
- slice — «папка» для группировки юнитов (например, веб-стек отдельно, фоновые задачи отдельно);
- cgroup — реальная группа в ядре, куда systemd помещает процессы юнита.
С практической точки зрения: вы задаёте лимиты в unit-файле или drop-in, а systemd транслирует их в файлы контроллеров cgroups v2.
Типичная ошибка — пытаться вручную писать значения в
/sys/fs/cgroupи удивляться, что systemd всё перезаписал. Если ваш процесс запускается systemd, корректнее работать через настройки юнитов.
Если вы хотите разложить воркеры и веб по «ресурсным доменам», удобно делать это через slices. Отдельно полезный прикладной разбор для веб-стека — в статье про настройку slices и лимитов cgroups для PHP-FPM.

CPU: как работает CPUQuota и почему «100%» — не то, что вы думаете
Ключевой инструмент — CPUQuota. Он ограничивает суммарное CPU-время юнита. В systemd проценты считаются относительно одного CPU. Поэтому:
CPUQuota=100%— примерно одно ядро;CPUQuota=200%— примерно два ядра;CPUQuota=50%— половина ядра.
Задать лимит можно через drop-in (предпочтительно, чтобы не править пакетные unit-файлы):
systemctl edit myservice.service
Добавьте:
[Service]
CPUQuota=150%
Примените изменения:
systemctl daemon-reload
systemctl restart myservice.service
Если вам нужно не «отрезать», а справедливо делить CPU при конкуренции, используйте CPUWeight. Это относительный вес (аналог приоритета, но на уровне группы). Он удобен, когда сервисы могут разгоняться, но при перегрузе важный сервис должен получать больше.
Проверка эффекта и диагностика
Смотреть ограничения и связанные параметры:
systemctl show myservice.service -p CPUQuota -p CPUWeight -p Slice
А дерево процессов конкретного юнита:
systemd-cgls /system.slice/myservice.service
RAM: MemoryMax, MemoryHigh и реальный «потолок» памяти
В cgroups v2 лимиты памяти стали логичнее, но появилось две разные «границы», которые важно не путать:
MemoryMax— жёсткий верхний предел (если группе некуда деваться, будет OOM-убийство процессов внутри группы);MemoryHigh— мягкий порог: при превышении ядро начинает активно давить группу reclaim’ом, но это не запрет.
Для прикладных сервисов обычно хорошо работает паттерн: поставить MemoryHigh чуть ниже, а MemoryMax — как аварийный стоп-кран. Например:
[Service]
MemoryHigh=800M
MemoryMax=1G
Почему так? Резкий выход на MemoryMax может привести к «обрыву» (OOM kill) в самый неподходящий момент. А MemoryHigh заранее создаёт давление и даёт процессу шанс «сдуться» (GC, освобождение кэшей, уменьшение буферов).
Swap: учитывать или запрещать?
Если swap включён, то без дополнительных настроек сервис может «вылезать» по RSS, но продолжать жить за счёт swap — и вы получите латентность вместо падения. В systemd для cgroups v2 есть лимит swap:
[Service]
MemoryMax=1G
MemorySwapMax=0
MemorySwapMax=0 фактически запрещает swap для юнита (при наличии поддержки ядра). Полезно для latency-sensitive сервисов, но будьте аккуратны: запрет swap повышает риск OOM на пиках.
Почему «container limits» и systemd-лимиты — это одно и то же ядро
Частый вопрос: «У меня контейнеры, зачем мне systemd resource control?». Ответ: потому что и контейнерные лимиты, и systemd в итоге управляют теми же контроллерами cgroups v2. Разница — кто именно строит дерево, как устроено делегирование и где вы задаёте политику.
В смешанной среде (контейнеры плюс сервисы на хосте) имеет смысл выстроить единый подход: slices для ролей, лимиты для «шумных» задач, и одинаковые точки наблюдаемости, чтобы лимиты контейнеров не конфликтовали с лимитами на уровне host-юнитов/слайсов.
I/O: IOWeight, IOPS и io.max — где что применять
Дисковый I/O сложнее CPU и RAM: узкое место может быть в самом диске, контроллере, очереди, файловой системе, плюс многое зависит от типа нагрузки (мелкие random reads против больших sequential writes). В cgroups v2 есть два основных подхода:
- вес — относительное распределение при конкуренции (через
IOWeight); - жёсткие лимиты — ограничение скорости/IOPS через контроллер
io.max(в терминах systemd это семействоIOReadBandwidthMax,IOWriteBandwidthMax,IOReadIOPSMax,IOWriteIOPSMax).
IOWeight: «будь вежливым» по отношению к соседям
IOWeight — хороший выбор, когда вы не хотите «резать» сервис, а хотите сделать так, чтобы важные процессы выигрывали при конфликте за диск. Пример:
[Service]
IOWeight=800
Вес относительный: важны соотношения между юнитами, а не абсолютное число.
IOPS и bandwidth: когда нужен жёсткий предел
Если сервис способен «убить» диск очередью (бэкапы, индексация, массовые преобразования файлов, тяжёлые миграции), лучше поставить жёсткий лимит. В systemd можно ограничить IOPS и/или пропускную способность. Пример для ограничения записи:
[Service]
IOWriteBandwidthMax=/dev/vda 20M
IOWriteIOPSMax=/dev/vda 200
Два практических нюанса:
- лимиты задаются на конкретное блочное устройство (например,
/dev/vda,/dev/nvme0n1); - для реального эффекта нужен активный I/O-контроллер cgroups v2 и поддержка со стороны ядра/драйвера и планировщика.
Чтобы понять, что реально применилось на уровне cgroups v2, проверяйте «истину на земле» в io.max у соответствующей группы. Например:
cat /sys/fs/cgroup/system.slice/myservice.service/io.max
Там будут строки с major:minor устройства и лимитами.
systemd-run: быстро ограничить разовую задачу без unit-файлов
Для одноразовых админских задач (сжатие логов, экспорт дампа, перерасчёт кешей) удобно не создавать отдельные unit-файлы, а запускать через systemd-run в виде transient unit. Процесс сразу оказывается в отдельном cgroup, и вы задаёте лимиты на CPU/RAM/I/O на старте.
Пример: ограничим задачу одним ядром и 512 МБ памяти:
systemd-run --unit=job-archive --property=CPUQuota=100% --property=MemoryMax=512M /usr/bin/tar -czf /tmp/archive.tgz /var/www
Пример: ограничим запись на диск по bandwidth и IOPS (подставьте своё устройство):
systemd-run --unit=job-backup --property=IOWriteBandwidthMax=/dev/vda 10M --property=IOWriteIOPSMax=/dev/vda 150 /usr/bin/rsync -a /data/ /backup/
Посмотреть состояние transient unit:
systemctl status job-backup.service
systemd slices: как разделить сервер на «ресурсные домены»
Для хоста с несколькими проектами практично заранее разложить сервисы по слайсам и задавать лимиты на уровне слайса (а при необходимости переопределять на конкретных сервисах). Например:
web.slice— фронтенды и app-сервисы;db.slice— базы данных;batch.slice— фоновые тяжёлые задачи.
Плюс подхода: новые сервисы автоматически попадают в правильную «корзину», а не конкурируют со всем подряд в system.slice.
Создайте drop-in для слайса (или сам slice unit) и задайте общие рамки. Например, ограничим batch до 25% CPU (четверть ядра) и дадим ему низкий приоритет по I/O:
systemctl edit batch.slice
[Slice]
CPUQuota=25%
IOWeight=100
Дальше привязывайте сервисы:
systemctl edit mybatch.service
[Service]
Slice=batch.slice

PSI (pressure stall information): как увидеть проблему до падения
Обычные метрики CPU/RAM часто отвечают на вопрос «сколько занято», но хуже отвечают на вопрос «насколько больно». PSI — механизм ядра, который показывает pressure: долю времени, когда задачи не могли продвинуться из-за нехватки ресурса.
PSI бывает по CPU, памяти и I/O. Посмотреть можно прямо в файловой системе:
cat /proc/pressure/cpu
cat /proc/pressure/memory
cat /proc/pressure/io
Там будут значения some и full с усреднением по окнам 10/60/300 секунд. Практическая интерпретация:
- растёт memory PSI — система тратит время на reclaim/ожидание памяти, возможны лаги и будущий OOM;
- растёт io PSI — очередь диска/хранилища тормозит всех;
- растёт cpu PSI — runnable задач больше, чем CPU успевает переварить.
В cgroups v2 PSI полезен ещё и тем, что pressure можно анализировать на уровне групп (через файлы PSI внутри конкретного cgroup), а не только глобально. Доступность и удобство зависят от версии ядра/systemd и того, как у вас устроен мониторинг.
oomd: почему systemd-oomd дружит с cgroups v2
Linux OOM killer — это последний рубеж: когда памяти совсем нет, ядро выбирает «кого убить». Это может быть неожиданно: иногда вылетает важное, иногда — случайный воркер, иногда — часть стека.
systemd-oomd использует PSI и политику на уровне cgroups, чтобы раньше принять решение и «аккуратнее» завершить процессы в конкретной группе. Идея простая: если группа систематически создаёт memory pressure и не укладывается в рамки, лучше «погасить» её, чем положить весь хост.
oomd не заменяет грамотные лимиты. Он работает лучше всего, когда вы задали рамки через
MemoryHigh/MemoryMaxи разнесли нагрузки по slices.
Практический сценарий: ограничиваем «тяжёлый» воркер и защищаем веб
Допустим, у вас есть веб-сервис и фоновый воркер (очереди, генерация отчётов, конвертация файлов). Симптом: при пике воркер забивает CPU и диск, а веб начинает отвечать медленно.
Решение в терминах systemd:
- Веб кладём в
web.sliceи повышаем емуCPUWeightиIOWeight. - Воркера кладём в
batch.slice, ограничиваемCPUQuota, ставим низкийIOWeightи при необходимости жёсткие лимиты по IOPS/bandwidth. - На воркера задаём
MemoryHighиMemoryMax, чтобы memory pressure не сносил хост.
Пример настроек для воркера:
[Service]
Slice=batch.slice
CPUQuota=50%
MemoryHigh=700M
MemoryMax=900M
IOWeight=100
IOWriteIOPSMax=/dev/vda 150
После внедрения обычно видно две вещи: веб становится стабильнее на пиках, а деградация воркера становится контролируемой (он работает медленнее, но не «роняет» соседей).
Если воркеры запускаются не systemd-сервисом, а через supervisor-подобные схемы, часто полезно сначала привести их к systemd-юнитам и только потом навешивать лимиты. Практический путь — в статье про перенос воркеров очередей на systemd.
Частые ошибки и тонкости, на которых теряют время
1) Лимит поставили, а эффекта нет
Проверьте, что вы правите именно тот юнит, который реально запускает процесс. Иногда «сервис» — это обёртка, а реальная нагрузка живёт в другом юните (например, отдельные воркеры). Всегда сверяйтесь через systemd-cgls и systemctl status.
2) I/O-лимиты не применились
Проверьте:
- правильное устройство (
/dev/vdaи/dev/vda1— не одно и то же; чаще лимитят диск/том, но схема зависит от разметки и слоёв хранения); - что вы смотрите
io.maxименно вашей группы; - что нагрузка реально идёт через это устройство (а не через другой том или сетевую ФС).
3) Путают MemoryMax и «сколько процесс реально может занять»
MemoryMax ограничивает суммарное потребление в группе, но поведение при достижении порога зависит от типа памяти (anon/page cache) и способности процесса освобождать её. Поэтому практичнее начинать с MemoryHigh и мониторинга PSI/latency, а MemoryMax держать как аварийный ограничитель.
Мини-чеклист внедрения на проде
- Убедитесь, что у вас cgroups v2, и зафиксируйте дерево юнитов через
systemd-cgls. - Разнесите роли по systemd slices (web/db/batch).
- CPU: где нужно —
CPUQuota, где важнее справедливость —CPUWeight. - RAM: минимум
MemoryHighиMemoryMaxдля «шумных» сервисов; продумайтеMemorySwapMax. - I/O: начните с
IOWeight, а для «опасных» джоб добавьте IOPS/bandwidth лимиты и проверяйтеio.max. - Наблюдаемость: смотрите PSI и коррелируйте с задержками приложений.
- Если используете systemd-oomd — проверьте политику, чтобы критичные сервисы не стали «кандидатами» без необходимости.
Заключение
cgroups v2 — это не «фича для контейнеров», а базовый механизм управления ресурсами в Linux. А systemd resource control — самый удобный способ применять его на реальном сервере: декларативно, воспроизводимо и без ручного шаманства в /sys/fs/cgroup. Начните с простого: заведите slices, поставьте CPUQuota и MemoryHigh на самые прожорливые сервисы, затем добавьте I/O-веса и при необходимости жёсткие лимиты по IOPS/bandwidth. PSI и oomd помогут поймать проблему раньше, чем она станет ночным инцидентом.


