Ограничение ресурсов Linux-сервисов через cgroups v2 и systemd: CPUQuota, MemoryMax, IOWeight | AdminWiki

Ограничение ресурсов Linux-сервисов через cgroups v2 и systemd: CPUQuota, MemoryMax, IOWeight

11 сентября 2026 12 мин. чтения
Содержание статьи

Введение: зачем ограничивать ресурсы сервисов в systemd

Лимит на сервис задаётся тремя строками в drop-in файле: CPUQuota=50%, MemoryMax=1G и IOWeight=50. После systemctl daemon-reload и перезапуска юнита значения уезжают в контроллеры cgroups v2, и дальше ядро само следит за их соблюдением. Сторонние демоны, патчи приложения и ulimit здесь не нужны.

Что происходит без лимитов, видно на двух типовых авариях. Первая: сервис с утечкой памяти за ночь выедает всю RAM, ядро запускает глобальный OOM killer и убивает процессы по своему выбору, часто не тот, который виноват. Вторая: ночной бэкап через rsync или restic занимает диск целиком, и PostgreSQL рядом начинает отдавать p99 в разы медленнее. В обоих случаях причина не в самом сервисе, а в отсутствии границ.

Лимиты закрывают три задачи: не дать одному юниту съесть CPU или память, поделить диск между конкурирующими задачами, получить предсказуемое поведение под нагрузкой. Сюда же попадает защита от каскадных отказов: если у сервиса есть жёсткий потолок по памяти, ядро убьёт только его, остальные продолжат работать.

Проверка версии cgroups и systemd

Инструкции ниже рассчитаны на cgroups v2 и systemd 244 или новее. Проверьте стек перед тем, как что-то менять:

stat -fc %T /sys/fs/cgroup
systemctl --version
uname -r
ls /sys/fs/cgroup

Первая команда должна вернуть cgroup2fs. Любой другой вывод (например, tmpfs) означает гибридный режим с cgroups v1, и часть параметров не применится. Версия systemd в выводе второй команды должна быть не ниже 244: в старых сборках параметры IOWeight и часть настроек памяти для v2 отсутствуют.

Где v2 уже включён по умолчанию: Ubuntu 22.04 и новее, Debian 11 и новее, RHEL 9 и совместимые сборки, Fedora с 31-й версии. На Ubuntu 20.04, Debian 10 и RHEL 8 по умолчанию работает v1. Переключение делается параметром ядра:

# файл /etc/default/grub
GRUB_CMDLINE_LINUX="... systemd.unified_cgroup_hierarchy=1"
sudo update-grub
sudo reboot

После перезагрузки повторите stat -fc %T /sys/fs/cgroup и убедитесь, что старые лимиты через blkio.throttle больше не используются. На практике быстрее поднять стенд с ядром 5.15 и systemd 249+, чем переводить legacy-хост, но если машина уже есть, переключение занимает один ребут. Для тестового полигона подойдёт облачный провайдер с современным ядром, например Timeweb Cloud, где cgroups v2 включён по умолчанию.

Основы cgroups v2 и systemd: как это работает

Systemd создаёт отдельную cgroup для каждого юнита и складывает их в иерархию slice. Параметры из unit-файла транслируются в файлы контроллеров, а те уже читает ядро. Путь для nginx выглядит так:

ls /sys/fs/cgroup/system.slice/nginx.service/
cgroup.controllers  cpu.max  cpu.stat  memory.max  memory.current  memory.events  io.max  io.weight

Файлы доступны только для чтения (кроме тех, куда пишет systemd), но их удобно смотреть: там лежат реальные значения, которые видит ядро, а не то, что вы записали в конфиг. Дерево slice делится на три части: system.slice для системных сервисов, user.slice для пользовательских сессий, -.slice в корне. Лимиты можно вешать не только на юнит, но и на целый slice, и тогда они действуют на всех потомков. Приём удобен для группы бэкапов: создайте backup.slice, перечислите в юнитах Slice=backup.slice и задайте ограничения один раз.

Задать параметры можно двумя способами. Первый: systemctl set-property, он пишет drop-in и по умолчанию сохраняется между перезагрузками, с флагом --runtime правка уходит в /run и исчезает после ребута. Второй: свой drop-in файл в /etc/systemd/system/<unit>.d/, полностью под вашим контролем и под git.

Активируются лимиты после systemctl daemon-reload, а применяются к уже запущенному процессу после перезапуска юнита. Часть настроек подхватывается на лету, поведение зависит от контроллера, поэтому безопаснее перезапустить сервис.

Различия cgroups v1 и v2 в контексте systemd

В v1 контроллеры жили в разных иерархиях, и один процесс мог попадать сразу в несколько деревьев. В v2 иерархия одна, контроллеры включаются через cgroup.controllers и cgroup.subtree_control, а имена параметров стали единообразными:

Задачаcgroups v1cgroups v2
Квота CPUcpu.cfs_quota_us + cpu.cfs_period_uscpu.max
Вес CPUcpu.sharescpu.weight
Жёсткий лимит памятиmemory.limit_in_bytesmemory.max
Мягкий лимит памятиmemory.soft_limit_in_bytesmemory.high
Лимит ввода-выводаblkio.throttle.read_bps_deviceio.max

В v2 появились PSI-метрики (pressure stall information) в /proc/pressure/cpu, /proc/pressure/memory и /proc/pressure/io. Они показывают, сколько времени задачи ждали ресурс, и это точнее средней загрузки. Отдельного дерева для blkio в v2 нет: относительный приоритет задаёт io.weight, абсолютные ограничения задаёт io.max.

Настройка лимитов CPU: CPUQuota и не только

CPUQuota задаёт жёсткий потолок процессорного времени в процентах от одного ядра: 50% это половина ядра, 200% это два ядра. Внутри cgroup значение превращается в пару квота/период, по умолчанию период равен 100000 мкс, то есть 100 мс.

CPUWeight работает иначе: это относительный вес при конкуренции, диапазон от 1 до 10000, по умолчанию 100. Если два сервиса делят ядра и у одного вес 200, а у другого 100, первый получит примерно вдвое больше процессорного времени. Когда CPU в избытке, вес ни на что не влияет.

AllowedCPUs прибивает процессы к конкретным ядрам, аналог taskset, но на уровне юнита. Приём полезен, когда нужно вывести шумный сервис с ядер, обслуживающих сетевые прерывания.

Временная установка CPUQuota через systemctl set-property

Для быстрой проверки гипотезы выставьте лимит без правки файлов:

sudo systemctl set-property --runtime nginx.service CPUQuota=50%
systemctl show nginx.service -p CPUQuotaPerSecUSec
systemctl status nginx.service --no-pager

Флаг --runtime означает, что правка живёт в /run/systemd/system.control и пропадёт после перезагрузки. Действующее значение проверяется в любой момент через systemctl show. Чтобы снять лимит, выполните ту же команду с пустым значением или перезапустите сервис после ребута.

Постоянная настройка через drop-in файл

Постоянный вариант надёжнее: файл видно в системе контроля версий, а рядом легко положить комментарий, зачем лимит поставлен.

sudo mkdir -p /etc/systemd/system/nginx.service.d
sudo tee /etc/systemd/system/nginx.service.d/limits.conf <<'EOF'
[Service]
CPUQuota=50%
CPUWeight=200
EOF
sudo systemctl daemon-reload
sudo systemctl restart nginx.service

Держите в голове разницу: CPUQuota жёсткий, CPUWeight только приоритет. Ставить оба сразу имеет смысл, когда сервис должен получать больше остальных при свободных ядрах, но не выходить за потолок. Значение 200% для четырёхъядерной машины означает два ядра, а не половину нагрузки.

Проверка фактического потребления CPU

После перезапуска убедитесь, что лимит реально работает:

cat /sys/fs/cgroup/system.slice/nginx.service/cpu.max
# 50000 100000

cat /sys/fs/cgroup/system.slice/nginx.service/cpu.stat
systemd-cgtop --order=cpu -d 2

Первое число в cpu.max это квота, второе период. Пара 50000 100000 означает ровно 50% одного ядра. Слово max вместо чисел говорит о том, что лимит не задан. В cpu.stat смотрите на nr_throttled: если счётчик растёт, ядро останавливало процессы, чтобы уложиться в квоту. Какие метрики стоит держать под наблюдением постоянно, разобрано в статье про практическую настройку производительности Linux-серверов под нагрузкой.

Управление памятью: MemoryMax, MemoryHigh и защита от OOM

MemoryMax это жёсткий потолок. Как только суммарное потребление группы упирается в него, ядро освобождает страницы, а если не выходит, убивает процесс внутри cgroup через OOM killer. Остальная система при этом остаётся живой, в этом главный плюс лимита.

MemoryHigh это мягкий порог. При его превышении ядро агрессивнее вытесняет страницы и тормозит группу, но никого не убивает. Рабочая схема: MemoryHigh ставят на 20-30% ниже MemoryMax, чтобы получить плавное торможение вместо мгновенного падения.

MemorySwapMax ограничивает swap для группы, значение 0 полностью запрещает сервису уходить в swap. Параметры MemoryMin и MemoryLow защищают страницы от вытеснения: их полезно ставить для базы данных, когда рядом работает кто-то прожорливый.

Установка MemoryMax и MemoryHigh

sudo systemctl set-property myservice.service MemoryMax=1G MemoryHigh=800M MemorySwapMax=0
systemctl show myservice.service -p MemoryMax -p MemoryHigh -p MemorySwapMax

cat /sys/fs/cgroup/system.slice/myservice.service/memory.max
cat /sys/fs/cgroup/system.slice/myservice.service/memory.current

Постоянный вариант через drop-in:

sudo tee /etc/systemd/system/myservice.service.d/memory.conf <<'EOF'
[Service]
MemoryAccounting=yes
MemoryHigh=800M
MemoryMax=1G
MemorySwapMax=0
EOF
sudo systemctl daemon-reload
sudo systemctl restart myservice.service

MemoryAccounting в свежих systemd включён по умолчанию, но в старых сборках его прописывают явно, иначе memory.current и memory.events останутся пустыми. Суффиксы K, M, G в systemd считаются в двоичных единицах: 1G это 1073741824 байт, а не 10^9.

Диагностика OOM: как понять, что сервис убит из-за памяти

Первым делом смотрите счётчики внутри cgroup и логи ядра:

cat /sys/fs/cgroup/system.slice/myservice.service/memory.events
# oom 3
# oom_kill 3

journalctl -u myservice.service --no-pager | grep -i -E "oom|killed"
dmesg -T | grep -i -E "oom|killed process"

Если oom_kill больше нуля, сервис упирался в MemoryMax и ядро его убивало. В dmesg будет строка вида "Memory cgroup out of memory: Killed process" с указанием PID и объёма памяти. Дальше два пути: поднять MemoryMax, если потребление оправданно, или разбираться с утечкой. Промежуточный вариант, перевести жёсткий лимит в мягкий MemoryHigh и посмотреть, как сервис ведёт себя под торможением.

Ограничение дискового ввода-вывода: IOWeight и bandwidth

Контроллер io в cgroups v2 работает в двух режимах: относительный вес и жёсткие ограничения по скорости. Начните с веса, абсолютные лимиты включайте, когда нужно гарантировать полосу конкретному сервису.

Настройка IOWeight для приоритизации

sudo systemctl set-property backup.service IOWeight=50
systemctl show backup.service -p IOWeight
cat /sys/fs/cgroup/system.slice/backup.service/io.weight

Диапазон от 1 до 10000, по умолчанию 100. Вес 50 означает, что при конкуренции за диск бэкап получит примерно вдвое меньше операций, чем сосед с весом 100. Вес работает только с планировщиком BFQ: проверьте cat /sys/block/sda/queue/scheduler. Если в выводе нет [bfq], включите его через udev-правило или параметр загрузки, иначе io.weight не даст эффекта.

Вес не спасает, когда сервис стартует и сразу забирает всю очередь диска. Тогда нужны абсолютные лимиты.

Жёсткие лимиты: IOReadBandwidthMax и IOWriteBandwidthMax

sudo tee /etc/systemd/system/backup.service.d/io.conf <<'EOF'
[Service]
IOAccounting=yes
IOWeight=50
IOReadBandwidthMax=/dev/sda 100M
IOWriteBandwidthMax=/dev/sda 50M
IOWriteIOPSMax=/dev/sda 2000
EOF
sudo systemctl daemon-reload
sudo systemctl restart backup.service

cat /sys/fs/cgroup/system.slice/backup.service/io.max
# 8:0 rbps=104857600 wbps=52428800 wiops=2000

Лимит привязывается к конкретному блочному устройству. В io.max устройство обозначено парой major:minor, а не именем, поэтому в конфиге пишут путь /dev/sda, ядро само переводит его в 8:0. Значения bps указаны в байтах в секунду, поэтому 100M превращается в 104857600. Если сервис работает по NFS или через LVM, лимит ставьте на физическое устройство, а не на mapper, иначе цифры разойдутся.

Просмотр фактических лимитов и текущего потребления

Конфиг в drop-in это только намерение. Реальные значения читайте из systemd и из cgroup, в двух местах они иногда отличаются, если параметр переопределил slice.

Использование systemd-cgtop для мониторинга

systemd-cgtop -d 2 --order=cpu
systemd-cgtop -m -d 5

Утилита показывает потребление по cgroup: CPU, память, число задач и операции ввода-вывода. Ключ --order=cpu сортирует вывод по нагрузке, -m оставляет только память. Этого достаточно, чтобы увидеть пожирателя, но не для истории: cgtop это снимок в реальном времени. Метрики с ретеншеном и пороговые значения собраны в статье про мониторинг производительности сервера.

Чтение параметров из /sys/fs/cgroup

systemctl show nginx.service -p CPUQuotaPerSecUSec -p MemoryMax -p MemoryHigh -p IOWeight -p MemoryCurrent -p CPUUsageNSec

cat /sys/fs/cgroup/system.slice/nginx.service/cpu.max
cat /sys/fs/cgroup/system.slice/nginx.service/memory.max
cat /sys/fs/cgroup/system.slice/nginx.service/memory.current
cat /sys/fs/cgroup/system.slice/nginx.service/io.max
cat /sys/fs/cgroup/system.slice/nginx.service/io.stat

Следите за единицами: systemctl show возвращает время в микросекундах (CPUQuotaPerSecUSec=500000 это 50%), а память в байтах (1073741824 это 1 ГиБ). Если cpu.max содержит слово max, квота не задана. Если memory.max равен огромному числу вроде 9223372036854771712, лимит фактически отсутствует. Файл io.stat показывает реально прочитанные и записанные байты, по нему удобно сверять эффективность bandwidth-ограничений.

Безопасное применение настроек и откат

Порядок действий одинаков для любого параметра: сначала тестовый стенд, потом боевой сервер с окном обслуживания. Лимит по памяти, выставленный наугад, роняет сервис не сразу, а под пиком трафика, в самый неудобный момент.

Пошаговое применение через drop-in

# 1. Резервная копия действующей конфигурации
sudo cp -a /etc/systemd/system/nginx.service.d /root/nginx-dropin-backup-$(date +%F)

# 2. Каталог и файл лимитов
sudo mkdir -p /etc/systemd/system/nginx.service.d
sudo tee /etc/systemd/system/nginx.service.d/limits.conf <<'EOF'
[Service]
CPUQuota=50%
MemoryHigh=800M
MemoryMax=1G
IOWeight=50
EOF

# 3. Перечитать конфигурацию и перезапустить юнит
sudo systemctl daemon-reload
sudo systemctl restart nginx.service

# 4. Проверить статус и логи
systemctl status nginx.service --no-pager
journalctl -u nginx.service -n 50 --no-pager

Если сервис не поднялся, смотрите journalctl: типичная причина опечатка в имени параметра, systemd игнорирует неизвестные ключи и пишет предупреждение при daemon-reload. Второй сценарий: лимит по памяти слишком мал для старта, процесс падает на инициализации. Держите окно обслуживания и план отката под рукой. Смежные ограничения, которые усиливают изоляцию, но требуют аккуратности при настройке, разобраны в материале про усиление systemd-сервисов через ProtectSystem, DynamicUser и SystemCallFilter.

Полный откат настроек

# Вариант 1: удалить свой drop-in
sudo rm /etc/systemd/system/nginx.service.d/limits.conf
sudo systemctl daemon-reload
sudo systemctl restart nginx.service

# Вариант 2: сбросить правки, сделанные set-property
sudo systemctl revert nginx.service
sudo systemctl daemon-reload
sudo systemctl restart nginx.service

Команда systemctl revert убирает изменения, которые systemctl set-property оставил в /etc/systemd/system.control, и снимает маскировку юнита. Файлы, созданные вручную в /etc/systemd/system/<unit>.d/, она не трогает, их удаляют через rm. После любой правки проверяйте systemctl show и убеждайтесь, что значения вернулись к ожидаемым.

Для экспериментов есть безопасный инструмент: systemd-run создаёт временный юнит с лимитами и не трогает рабочие сервисы.

sudo systemd-run --unit=limit-test --slice=test.slice -p CPUQuota=20% -p MemoryMax=256M --wait /usr/bin/stress-ng --cpu 4 --vm 2 --vm-bytes 400M

Такой запуск покажет, как ведёт себя нагрузка под квотой, и не потребует ни drop-in, ни перезапуска продакшена.

Диагностика: что делать, если сервис упирается в лимит

Три симптома выглядят похоже и лечатся по-разному: сервис тормозит под квотой CPU, падает по памяти, ждёт диск. Различить их помогают счётчики внутри cgroup.

Как понять, что CPU throttling включился

cat /sys/fs/cgroup/system.slice/nginx.service/cpu.stat
# nr_periods 41000
# nr_throttled 8200
# throttled_usec 45000000

cat /proc/pressure/cpu

Растущий nr_throttled вместе с throttled_usec означает, что ядро регулярно останавливает процессы, чтобы уложиться в квоту. Если сервис отдаёт запросы с задержкой, а CPUQuota стоит на уровне 50% при загрузке 100%, потолок и есть причина. Варианты по возрастанию риска: поднять CPUQuota, перейти на CPUWeight и дать сервису конкурировать на равных, вынести тяжёлые задачи в отдельный slice с собственным лимитом.

Что делать при OOM: увеличить MemoryMax или разбираться с утечкой

systemd-cgtop -m -d 2
cat /sys/fs/cgroup/system.slice/myservice.service/memory.events
cat /sys/fs/cgroup/system.slice/myservice.service/memory.current
journalctl -u myservice.service --no-pager | grep -i oom

Если memory.current упирается в memory.max, а oom_kill растёт, у сервиса нет запаса. Быстрый шаг: поднять MemoryMax на 30% и посмотреть, как ведёт себя потребление сутки. Линейный рост памяти означает утечку, и лимит только отсрочит падение. Второй шаг: поставить MemoryHigh ниже MemoryMax, чтобы получить торможение вместо убийства, и параллельно собирать профиль потребления.

Когда счётчики в норме, а сервис всё равно деградирует, ищите узкое место за пределами его cgroup: перегруженный диск, сеть, соседний юнит без лимитов. Алгоритм поиска такой причины описан в статье про поиск узкого места в системе без лишних затрат. Если логов много и нужно быстро вычленить аномалии, разбор выгрузки journalctl можно ускорить через LLM API, например с агрегатором моделей AiTunnel, который отдаёт доступ к GPT, Gemini и Claude через единый интерфейс.

Заключение

Минимальный набор для продакшена выглядит так: CPUQuota для защиты от CPU-жадных задач, MemoryHigh плюс MemoryMax для контроля памяти, IOWeight и bandwidth-лимиты для диска. Настраивайте через drop-in файлы, проверяйте результат командами systemctl show, systemd-cgtop и чтением /sys/fs/cgroup, а откатывайте удалением файла или через systemctl revert.

Перед правками на боевом сервере прогоняйте конфигурацию на стенде, снимайте резервную копию каталога с drop-in и держите окно обслуживания. Официальные детали по каждому параметру собраны в man systemd.resource-control, man systemd.slice и man systemd-cgtop; сверяйтесь с ними под свою версию systemd, потому что набор поддерживаемых ключей отличается между 244 и 255.

Поделиться:
Сохранить гайд? В закладки браузера