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 control group v2 visualized.

Для тех, кто хочет изучить и закрепить базу по Linux, есть (почти бесплатный) мини-курс: 26 часов видеолекций с практикой. Подробности

Что такое cgroup, если совсем коротко?

Определение cgroups с man7.org:

Контрольные группы, обычно называемые cgroups, — это возможность ядра Linux, которая позволяет объединять процессы в иерархические группы, а затем ограничивать и отслеживать потребление ими разных типов ресурсов.

Единственный интерфейс ядра для работы с cgroups — это псевдофайловая система под названием cgroupfs. Отдельных системных вызовов для создания, изменения или удаления cgroup’ов нет: все операции с ними выполняются через создание каталогов и запись в файлы со специальными именами внутри псевдофайловой системы cgroup.

На современных дистрибутивах Linux эта псевдофайловая система обычно примонтирована в /sys/fs/cgroup.

mount -l | grep cgroup
cgroup2 on /sys/fs/cgroup type cgroup2 ...

Создание подкаталога в cgroupfs приводит к созданию новой cgroup:

mkdir /sys/fs/cgroup/my_cgroup

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

ls /sys/fs/cgroup/my_cgroup
cgroup.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_pen2
cgset -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 redis
Control 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 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». Записаться