TL;DR:
В статье показано, как ограничивать потребление CPU и памяти для процесса с помощью cgroups v2. Есть три способа:
1) напрямую через cgroupfs (каталоги и файлы в /sys/fs/cgroup),
2) через утилиты libcgroup (cgcreate/cgset/cgexec)
3) и через systemd (systemd-run, а также “постоянные” slice-юниты).
Техники применимы и к контейнерам: systemd-слайс можно использовать как ��одительскую cgroup для Docker, а принципы совпадают с тем, как управляются ресурсы в Kubernetes.
Это не исчерпывающее руководство, а практический пример того, как ограничивать потребление процессом CPU и оперативной памяти с помощью cgroups в Linux. Этот подход можно использовать, чтобы:
Защитить систему от особенно прожорливого процесса
Обеспечить справедливое распределение ресурсов между несколькими приложениями
Проверять производительность приложения при ограниченных ресурсах
Гарантировать доступность ресурсов в мультиарендных средах
Мы начнём с создания, настройки и добавления процесса в cgroup самым базовым (и трудоёмким) способом, напрямую работая с виртуальной файловой системой cgroupfs. Затем посмотрим, как сделать ровно то же самое с помощью более высокоуровневых инструментов вроде cgcreate и cgexec из libcgroup или systemd-run и slice-юнитов в systemd.
Хотя основной фокус будет на «родных» для Linux способах управления ресурсами процессов, описанные техники напрямую применимы и для управления ресурсами контейнеров и подов, так что этот материал будет полезен и тем, кто работает с Docker и Kubernetes.
Примеры в статье опираются на cgroup v2, но общая идея должна работать и с cgroup v1.

Для тех, кто хочет изучить и закрепить базу по Linux, есть (почти бесплатный) мини-курс: 26 часов видеолекций с практикой. Подробности
Что такое cgroup, если совсем коротко?
Определение cgroups с man7.org:
Контрольные группы, обычно называемые cgroups, — это возможность ядра Linux, которая позволяет объединять процессы в иерархические группы, а затем ограничивать и отслеживать потребление ими разных типов ресурсов.
Единственный интерфейс ядра для работы с cgroups — это псевдофайловая система под названием cgroupfs. Отдельных системных вызовов для создания, изменения или удаления cgroup’ов нет: все операции с ними выполняются через создание каталогов и запись в файлы со специальными именами внутри псевдофайловой системы cgroup.
На современных дистрибутивах Linux эта псевдофайловая система обычно примонтирована в /sys/fs/cgroup.
mount -l | grep cgroupcgroup2 on /sys/fs/cgroup type cgroup2 ...Создание подкаталога в cgroupfs приводит к созданию новой cgroup:
mkdir /sys/fs/cgroup/my_cgroupКаталог автоматически заполняется набором файлов, которые можно использовать для настройки новой cgroup:
ls /sys/fs/cgroup/my_cgroupcgroup.controllers io.max
cgroup.events io.stat
cgroup.freeze memory.current
cgroup.max.depth memory.events
cgroup.max.descendants memory.events.local
cgroup.procs memory.high
cgroup.stat memory.low
cgroup.subtree_control memory.max
cgroup.threads memory.min
cgroup.type memory.numa_stat
cpu.max memory.oom.group
cpu.stat memory.stat
cpu.weight memory.swap.current
cpu.weight.nice memory.swap.events
cpuset.cpus memory.swap.high
cpuset.cpus.effective memory.swap.max
cpuset.cpus.partition pids.current
cpuset.mems pids.events
cpuset.mems.effective pids.maxТочный набор файлов зависит от включённых контроллеров, которые можно увидеть в файле cgroup.controllers. Чаще всего используются контроллеры
cpuset,cpu,io,memory,hugetlbиpids.
Поскольку cgroups управляются через каталоги и файлы псевдофайловой системы, достаточно выдать права на запись в конкретный каталог (или его файлы), чтобы разрешить пользователям без root прав настраивать cgroups, включая создание дочерних cgroup. Если набор доступных контроллеров для дочерней cgroup нужно дополнительно ограничить, это можно сделать, записав нужные значения в файл cgroup.subtree_control родительской cgroup:
echo "+cpu +memory -io" > /sys/fs/cgroup/<parent>/cgroup.subtree_controlЭтот подход называется делегированием cgroup.
Высокоуровневые инструменты вроде cgcreate, cgexec и даже systemd-run — это всего лишь обёртки, которые выполняют mkdir, запись в файлы и другие подобные операции в поддереве cgroupfs.
Теперь давайте запачкаем руки и проведём пару экспериментов на самодельной «прожорливой» программе.
Исходный код программы «пожиратель ресурсов»
Программа ниже пытается загрузить все доступные CPU, запуская бесконечный цикл для каждого ядра. ��араллельно она выделяет память порциями по 10 МБ каждую секунду, пока процесс не будет завершён.
package main
import (
"context"
"fmt"
"os/signal"
"runtime"
"sync"
"syscall"
"time"
)
func main() {
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()
var wg sync.WaitGroup
// Задействовать все доступные CPU
numCPU := runtime.NumCPU()
runtime.GOMAXPROCS(numCPU)
// Запустить «занятую» горутину на каждое ядро CPU
for i := 0; i < numCPU; i++ {
go func() {
wg.Add(1)
defer wg.Done()
fmt.Println("Started a CPU hog")
for ctx.Err() == nil { /* Пустой цикл, чтобы держать CPU занятым */ }
}()
}
//
Настройка cgroup с помощью cgroupfs
Сначала создадим новую cgroup, создав каталог в файловой системе cgroup. Это будет группа, в которой мы сможем задать ограничения на использование CPU и памяти для нашего ресурсоёмкого процесса.
mkdir /sys/fs/cgroup/hog_penДальше зададим лимиты на использование CPU и памяти. Допустим, мы хотим ограничить CPU до 50%, а память до 100 МБ.
Чтобы ограничить CPU, запишем значения <cpu_quota> и <cpu_period> в файл cpu.max:
echo "50000 100000" > /sys/fs/cgroup/hog_pen/cpu.maxЗдесь 50000 — это максимально допустимое время CPU за период (в микросекундах), а 100000 — длительность периода. В итоге использование CPU будет ограничено до 50%.
В многоядерных системах максимальное значение CPU может быть больше 100%. Например, на машине с 2 ядрами значение 200000 100000 полностью корректно.
Чтобы ограничить использование памяти, запишем значение в файл memory.max:
echo "100M" > /sys/fs/cgroup/hog_pen/memory.maxТеперь запустим ресурсоёмкий процесс:
~/hogПока что этот процесс не ограничен теми лимитами cgroup, которые мы задали. Чтобы переместить его в cgroup hog_pen, нужно записать его PID в файл cgroup.procs.
Вот как это сделать (из отдельной вкладки терминала):
HOG_PID=$(pgrep -xo hog)
echo ${HOG_PID} >> /sys/fs/cgroup/hog_pen/cgroup.procsКогда процесс, запущенный в cgroup с лимитом по памяти, приближается к этому лимиту, срабатывает событие out-of-memory (OOM), и обычно это приводит к завершению процесса.
Процесс может состоять только в одной cgroup одновременно, но его можно перемещать между cgroup.
Добавлять уже запущенный процесс в cgroup, записывая его PID в cgroup.procs, удобно, но этот способ не подходит, если нужно гарантировать, что потребление ресурсов будет ограничено постоянно, без даже короткого промежутка между стартом процесса и его добавлением в cgroup.
К счастью, при запуске процесс наследует cgroup родительского процесса, поэтому можно сначала переместить родительский процесс в нужную cgroup, а затем запустить уже «ограниченный» дочерний процесс.
Чтобы это продемонстрировать, переместим текущий процесс оболочки в cgroup hog_pen:
echo $$ >> /sys/fs/cgroup/hog_pen/cgroup.procs…и запустим из неё процесс hog:
~/hogНаконец, когда cgroup больше не нужна, её можно удалить командой rmdir. Обратите внимание: удалить cgroup не получится, если внутри неё всё ещё есть процессы, поэтому придётся дождаться, пока процесс hog будет завершён из-за OOM, а также выйти из сессии оболочки, которую вы переместили в cgroup hog_pen.
Из новой вкладки терминала выполните:
mdir /sys/fs/cgroup/hog_penЕсли команда rmdir завершается ошибкой Device or resource busy, перепроверьте список процессов в cgroup командой cat /sys/fs/cgroup/hog_pen/cgroup.procs. Список должен быть пустым.
Любопытный факт:
rm -rf /sys/fs/cgroup/<name>не подходит для удаления cgroup. Отдельные файлы в файловой системе cgroupfs удалить нельзя, поэтому и используетсяrmdir.
Вот и всё! Вы успешно создали cgroup и ограничили использование CPU и памяти для процесса с помощью cgroupfs. Теперь посмотрим, как сделать то же самое с помощью более высокоуровневых инструментов.
Настройка cgroup с помощью libcgroup
Инструменты libcgroup упрощают управление cgroups. Ниже показано, как воспроизвести ту же конфигурацию cgroup, что и выше, используя команды cgcreate, cgset и cgexec.
В некоторых дистрибутивах Linux (например, в RHEL и SLES) пакет libcgroup объявлен устаревшим, и вместо него рекомендуют использовать systemd для работы с cgroups, чтобы у файловой системы cgroupfs был только один «менеджер».
Сначала убедитесь, что нужные инструменты установлены. В Debian-подобных системах их можно поставить так:
apt-get install cgroup-tools
# yum install libcgroup
# yum install libcgroup-toolsЧтобы создать новую cgroup с помощью libcgroup, используйте команду cgcreate -g <controllers>:<path>:
cgcreate -g cpu,memory:/hog_pen2
cgcreate -g cpu,memory:/hog_pen2Чтобы ограничить CPU и память, используйте команду cgset:
cgset -r cpu.max="50000 100000" hog_pen2cgset -r memory.max="100M" hog_pen2Чтобы проверить, что новая cgroup работает как задумано, запустите процесс hog:
~/hog…а затем переместите его в cgroup hog_pen2 с помощью команды cgclassify:
HOG_PID=$(pgrep -xo hog)
cgclassify -g cpu,memory:hog_pen2 ${HOG_PID}Либо можно использовать команду cgexec и запустить процесс hog сразу в cgroup hog_pen2:
cgexec -g cpu,memory:hog_pen2 ~/hogАналогично приёму с rmdir, можно использовать команду cgdelete, чтобы удалить cgroup hog_pen2, когда она больше не нужна:
cgdelete -g cpu,memory:/hog_pen2Удобно, правда? Не нужно писать в какие-то неочевидные файлы или вручную перемещать процесс оболочки в cgroup перед запуском целевого процесса. Теперь посмотрим, что может предложить systemd.
Запуск ограниченного процесса с помощью systemd-run
Как это обычно бывает с systemd, он стремится контролировать каждый аспект жизни системы, и управление ресурсами не исключение.
Команда systemd-run позволяет удобно запускать процессы как фоновые сервисы, при необходимости с заданными ограничениями по ресурсам. Например, чтобы ограничить использование CPU и памяти для процесса, можно выполнить:
systemd-run -u hog -p CPUQuota=50% -p MemoryMax=100M ~/hogЭта команда создаст временный service unit hog.service с указанными лимитами по ресурсам.
Когда фоновый процесс hog достигнет заданного лимита по памяти, его завершит OOM-killer.
Совет:
systemd-runавтоматически удаляет временные юниты для успешно завершившихся запусков. Однако временные юниты, созданные неудачными одноразовыми запусками, автоматически не очищаются. Чтобы «прибрать мусор» после таких юнитов, используйтеsystemd-runс флагом-G|--collect.
Либо можно вручную выгрузить все юниты со статусом failed, выполнив systemctl reset-failed. Теперь вы знаете, что делать, если увидите ошибку вроде этой:
Failed to start transient service unit:
Unit <name>.service was already loaded or has a fragment file.Посмотреть только что созданную cgroup (и все остальные cgroup в системе) можно с помощью systemd-cgls:
systemd-cgls --allКроме того, можно использовать команду systemd-cgtop, чтобы увидеть «топ» cgroup по потреблению ресурсов:
systemd-cgtopЛюбопытный факт: на дистрибутивах Linux, где используется systemd, обычно разумно полагаться только на systemd при работе с cgroups. Поскольку systemd запускает свои сервисы в выделенных cgroup-«слайсах», он фактически становится менеджером файловой системы cgroupfs. Как следствие, такие системы, как containerd, Docker и Kubernetes, как правило, стараются не работать с cgroupfs напрямую и вместо этого используют systemd в роли «драйвера» cgroups. Подробнее см. здесь и здесь.
Использование systemd-слайса для создания «постоянной» cgroup
В примере выше systemd-run создал временную cgroup, которая автоматически очищается после завершения целевого процесса. Если вам нужна cgroup, которая будет жить дольше процесса и даже переживёт перезагрузку системы, можно использовать слайс-юнит systemd.
Для начала создайте новый файл слайса в /etc/systemd/system/:
cat <<EOF > /etc/systemd/system/hog_pen.slice
[Slice]
CPUQuota=50%
MemoryMax=100M
EOFМожно выполнить команду
systemctl daemon-reload, чтобы принудительно заставить systemd перечитать файлы юнитов.
И теперь в этот слайс можно поместить произвольное количество процессов:
systemd-run -u hog1 --slice=hog_pen.slice ~/hog
systemd-run -u hog2 --slice=hog_pen.slice ~/hogКаждый процесс в слайсе hog_pen.slice будет помещён в свою дочернюю cgroup, которая наследует лимиты ресурсов, заданные для слайса. Таким образом, суммарное использование CPU и памяти всеми процессами в слайсе hog_pen.slice будет ограничено 50% CPU и 100 МБ памяти.
Ещё одно крутое свойство systemd-слайсов состоит в том, что их можно использовать для управления ресурсами Docker-контейнеров. Это особенно полезно, если вам нужно настроить контроллер cgroups, для которого нет отдельного флага в docker run (например, memory.oom.group), или если вы хотите поместить несколько Docker-контейнеров в одну и ту же cgroup, имитируя pod в Kubernetes:
docker run -d --name web --cgroup-parent=hog_pen.slice nginx
docker run -d --name rdb --cgroup-parent=hog_pen.slice redisControl group /:
-.slice
├─hog_pen.slice
│ ├─docker-98684060c21bbc18e41f31926265cb092693a20fc169498e48cc2500c19bb646.scope …
│ │ ├─4156 nginx: master process nginx -g daemon off;
│ │ ├─4208 nginx: worker process
│ │ └─4209 nginx: worker process
│ └─docker-c9948bd490491b2719a8614146fb401535f6b1268b07cb430581973f7ea586b7.scope …
│ └─4292 redis-server *:6379Практика
Теперь давайте попрактикуемся в том, что мы изучили. Сможете ли вы выполнить эти задания?
Приостановка и возобновление работы процессов Linux с помощью cgroup Freezer
Завершение всей группы процессов, когда в одном из процессов заканчивается память
Настройка контейнера на завершение работы, когда у одного из его процессов заканчивается память
Ограничение использования CPU и памяти приложением Docker Compose
Подведем итоги
В этом туториале мы разобрали разные способы управления ресурсами процессов с помощью Linux cgroups. Мы начали с базовой настройки cgroups через cgroupfs, затем перешли к более удобным инструментам libcgroup и, наконец, использовали systemd как для временного, так и для постоянного управления ресурсами. Эти подходы помогают справедливо распределять ресурсы, защищать систему от чрезмерно прожорливых процессов и даже тестировать производительность приложений в условиях ограниченных ресурсов.
Экспериментируйте с этими методами и адаптируйте их под свои задачи. Удачного управления ресурсами!

Если cgroups — ваш низкоуровневый рычаг контроля ресурсов, то K8s — слой, где эти рычаги превращаются в управляемую платформу. На курсе «Инфраструктурная платформа на основе Kubernetes» разбирают эксплуатацию кластера и экосистему: от IaC-подхода до практик, которые реально работают в проде, на практике. Чтобы узнать, подойдет ли вам программа курса, пройдите вступительный тест.
Для знакомства с форматом обучения и экспертами приходите на бесплатные демо-уроки:
3 февраля, 20:00. «Kubernetes: „общение“ с приложениями внутри кластера». Записаться
4 февраля, 20:00. «Kubernetes Multi-Tenancy: как изолировать команды в одном кластере с vCluster и Capsule». Записаться
17 февраля, 20:00. «Основы безопасности в Kubernetes». Записаться
