cgroups v2 — единая иерархия групп процессов, которая в свежих выпусках дистрибутивов заменила несколько независимых деревьев первой версии. Память, процессорное время, ввод-вывод и учёт запущенных задач описываются в одном дереве каталогов, а настройки наследуются сверху вниз. Разберём, как убедиться, что система работает именно в режиме v2, ограничить сервис по памяти и процессору, следить за реальным потреблением и находить причины внезапных завершений процессов.
Проверяем режим и возможности ядра
Сначала убеждаемся, что машина действительно использует вторую версию. В Debian 11 и новее, Ubuntu 21.10 и новее, Fedora 31 и новее унифицированная иерархия включена по умолчанию. Если система обновлялась с более старого выпуска, ядро может быть загружено с ключом совместимости, и часть подсистем останется на старой схеме.
stat -fc %T /sys/fs/cgroup/
mount | grep cgroup
grep cgroup_no_v1 /proc/cmdline
cat /proc/self/cgroup
ls /sys/fs/cgroup
Первая команда возвращает cgroup2fs, а строка вида 0::/ в выводе /proc/self/cgroup подтверждает работу в едином дереве. Если на верхнем уровне каталога видны cpu,cpuacct, memory и pids, режим гибридный: описанные ниже параметры применятся только к подсистемам, уже переведённым на вторую версию.
Ограничение памяти и процессорной квоты одной командой
Для разовых проверок удобен systemd-run: он создаёт временную группу, запускает в ней процесс и сразу накладывает ограничения. Группа живёт ровно столько, сколько живёт процесс, и не требует правки конфигурации сервисов.
systemd-run --scope --unit=memtest -p MemoryMax=512M -p MemorySwapMax=0 stress-ng --vm 2 --vm-bytes 256M --timeout 60s
systemd-run --scope --unit=cputest -p CPUQuota=40% dd if=/dev/zero of=/var/tmp/test.img bs=1M count=1024
systemctl show memtest.scope -p ControlGroup -p MemoryMax -p CPUQuota
cat /sys/fs/cgroup/memtest.scope/memory.current
cat /sys/fs/cgroup/memtest.scope/memory.stat
MemoryMax задаёт жёсткий предел, при превышении которого ядро завершает процесс силами oom-killer. Значение MemorySwapMax=0 запрещает вынос страниц в своп — это полезно для сервисов, где предсказуемая задержка важнее выживания при пиковом потреблении. В memory.stat видно, сколько памяти уходит на кэш страниц, а сколько занято анонимными страницами процесса.
Постоянные лимиты в unit-файле сервиса
Разовые запуски годятся для проверки гипотез, а боевые ограничения описывают в unit-файле. Правила удобно вынести в drop-in каталог, чтобы обновление пакета не затёрло ваши значения.
mkdir -p /etc/systemd/system/backup.service.d
cat > /etc/systemd/system/backup.service.d/limits.conf <<'EOF'
[Service]
MemoryMax=1G
MemoryHigh=800M
MemorySwapMax=0
CPUQuota=120%
CPUWeight=50
TasksMax=64
EOF
systemctl daemon-reload && systemctl restart backup.service
systemctl show backup.service -p MemoryMax -p CPUQuota -p TasksMax
MemoryHigh не убивает процесс, а начинает тормозить его аллокации, давая сервису шанс освободить память самому. CPUQuota=120% разрешает использовать полтора ядра: квота считается от периода, который по умолчанию равен 100 миллисекундам. TasksMax ограничивает число нитей и защищает систему от протекающих пулов соединений.
grep . /sys/fs/cgroup/system.slice/backup.service/memory.max
grep . /sys/fs/cgroup/system.slice/backup.service/cpu.max
grep . /sys/fs/cgroup/system.slice/backup.service/pids.max
systemctl status backup.service --no-pager | head -8
После перезапуска сверяйте фактические значения: в cpu.max записаны квота и период в микросекундах, а memory.max должен совпасть с тем, что вы задали в unit-файле. Расхождение обычно означает, что лимит продублирован в основном файле сервиса или переопределён другим drop-in.
Что происходит при превышении лимита памяти
События группы видны прямо в её файлах: memory.events хранит счётчик срабатываний oom-killer, а memory.current показывает текущее занятое. Это первое место, куда стоит смотреть, если сервис «просто перезапустился» посреди ночи.
grep . /sys/fs/cgroup/system.slice/backup.service/memory.events
journalctl -u backup.service -b --no-pager | tail -40
dmesg | grep -iE 'out of memory|killed process'
systemctl show backup.service -p Result -p ExecMainStatus
Колонка oom_kill в файле событий растёт только при реальном убийстве процессов. Если увеличивается счётчик max, а oom_kill остаётся нулем, значит, работал только механизм замедления, и память не была вытеснена принудительно. Значит, лимит занижен не критично, но сервис уже упирается в потолок и требует профилирования.
Квоты CPU и диагностика троттлинга
Ограничение процессора устроено иначе: группа получает периодические квоты и, исчерпав их, приостанавливается до следующего периода. Из-за этого сервис со спокойной средней загрузкой может получать рывки задержки в пике.
grep . /sys/fs/cgroup/system.slice/backup.service/cpu.stat
grep . /sys/fs/cgroup/system.slice/backup.service/cpu.pressure
systemd-cgtop -o cpu -n 3
systemd-cgtop -o mem -n 3
Поле nr_throttled показывает, сколько периодов группа провела в ожидании, throttled_usec — суммарную длительность этих ожиданий. Если счётчик растёт на глазах, квота занижена: поднимите CPUQuota или увеличьте CPUWeight, чтобы делить процессор честнее с соседями. Строка full в файле давления показывает долю времени, когда все ядра одновременно ждали ресурс.
Делегирование и пользовательские группы
По умолчанию пользователь не назначает себе лимиты: иерархия принадлежит системе. Параметр Delegate=yes передаёт сервису владение поддеревом, а в пользовательском пространстве systemd делает это автоматически для юнитов службы user@.service.
grep -R Delegate /usr/lib/systemd/system/user@.service
systemd-run --user --scope --unit=ubench -p MemoryMax=300M -p CPUQuota=80% stress-ng --vm 1 --vm-bytes 256M --timeout 30s
loginctl show-user $USER -p Lingering
systemctl --user status ubench.scope
Флаг Lingering отвечает за то, работают ли пользовательские юниты без активного входа: без него задачи ветки systemd --user завершаются вместе с последней сессией. Включается настройкой loginctl enable-linger и нужна для фоновых задач пользователя, таких как синхронизация или резервное копирование.
Лимиты контейнеров и чек-лист
Собственные контейнеры поднимают свои группы: движок Docker в режиме v2 пишет memory.max и cpu.max в scope демона. Проверять их удобно теми же файлами и штатными утилитами.
docker stats --no-stream
docker inspect -f '{{.HostConfig.Memory}} {{.HostConfig.MemorySwap}}' myapp
cat /sys/fs/cgroup/system.slice/docker.service/memory.current
Практический чек-лист настройки лимитов:
- проверить режим командой
stat -fc %T /sys/fs/cgroup/: должен вернутьсяcgroup2fs; - собрать замеры за спокойный день и прибавить к ним 30–50 процентов при расчёте
MemoryMax; - задавать
MemoryHighчуть нижеMemoryMax, чтобы процесс успевал реагировать на нехватку; - следить за счётчиками
oom_killиnr_throttledв течение недели после каждого изменения; - проверять квоту под нагрузкой, а не сразу после перезапуска сервиса;
- не забывать про
TasksMaxдля приложений с пулами потоков и воркерами.
Итог
cgroups v2 превращают ограничение ресурсов из магии в набор обычных файлов, которые можно прочитать и поправить на лету. Начинайте с systemd-run, переносите подтверждённые значения в drop-in каталоги сервисов и обязательно смотрите на счётчики памяти и процессора после каждого изменения. Такая привычка превращает случайные перезапуски и просадки в понятные числа с конкретной причиной и конкретным лимитом.