← Linux: изоляция
12 мин · средне · Урок 2 из 4

cgroups: лимиты ресурсов

cgroups: как ограничивают CPU, память и IO.

Namespaces про видимость, cgroups про ресурсы

В прошлом уроке namespaces определяли, что процесс видит: свой список процессов, свою сеть, свою файловую систему. Но видеть лишь кусок системы не значит потреблять её ресурсы скромно. Процесс с урезанной видимостью всё ещё может занять все ядра CPU и всю память хоста.

cgroups (control groups) — механизм ядра, который ограничивает, сколько ресурсов группа процессов может потребить: процессорное время, память, дисковый ввод-вывод, число процессов. Namespaces отвечают на вопрос «что я вижу», cgroups на вопрос «сколько мне можно взять».

Иерархия групп

Процессы объединяются в дерево групп. Каждая группа — каталог в специальной файловой системе, смонтированной в /sys/fs/cgroup. В файлах внутри каталога лежат лимиты и текущие счётчики.

$ ls /sys/fs/cgroup
cgroup.controllers  cpu.max        memory.max     pids.max
cgroup.procs        io.max         memory.current pids.current

Современная версия — cgroup v2, единая иерархия для всех контроллеров. Старая cgroup v1 с отдельным каталогом на каждый контроллер осталась в легаси: все актуальные дистрибутивы грузятся с v2 по умолчанию. Проверить, что у тебя, можно по типу файловой системы:

$ stat -fc %T /sys/fs/cgroup
cgroup2fs                  # v2; tmpfs означает старую v1

Файл cgroup.procs содержит PID-ы процессов группы, а файлы вида memory.max и cpu.max задают лимиты:

$ cat /sys/fs/cgroup/memory.max
max                 # max значит «без лимита»
$ cat /sys/fs/cgroup/cpu.max
max 100000          # первое число — квота, второе — период в микросекундах

Как это связано с контейнерами

Флаги ограничения ресурсов у docker run под капотом создают cgroup и пишут в неё числа. Команда --memory 512m выставляет memory.max, а --cpus 1 настраивает cpu.max:

docker run --memory 512m --cpus 1 nginx

Проверить лимит можно из хоста. У Docker группа контейнера лежит внутри /sys/fs/cgroup, и там видно ровно те 512 мегабайт:

$ cat /sys/fs/cgroup/system.slice/docker-<id>.scope/memory.max
536870912           # 512 * 1024 * 1024 байт

Когда процесс внутри группы пробует занять больше памяти, чем разрешает memory.max, ядро запускает OOM-killer и убивает процесс в этой группе. Соседи на хосте при этом не страдают: лимит сработал локально.

Ловушка первая: namespaces и cgroups это одно и то же

Их часто путают, потому что оба участвуют в изоляции контейнера. Разница в задаче. Отними namespaces, и процесс увидит все процессы и сеть хоста, но лимиты памяти останутся. Без cgroups он не увидит лишнего, зато сможет занять всю память и CPU.

$ cat /proc/self/cgroup        # в какой cgroup я сейчас
0::/user.slice/user-1000.slice
$ ls -l /proc/self/ns          # какие namespaces у меня
lrwxrwxrwx net -> 'net:[4026531840]'
lrwxrwxrwx pid -> 'pid:[4026531836]'

Один вывод отвечает за ресурсы, другой за видимость. Это два разных механизма ядра, работающих параллельно.

Ловушка вторая: контейнер не уронит хост по памяти

Уронит, если лимит не задан. Контейнер без --memory попадает в cgroup с memory.max равным max, то есть без потолка. Утечка памяти внутри такого контейнера растёт, пока OOM-killer хоста не начнёт убивать процессы, и под удар попадут соседние контейнеры или сам хост.

docker run nginx                  # без лимита: memory.max = max
docker run --memory 512m nginx    # с лимитом: своя группа, свой потолок

Ограничение ресурсов не появляется само. Его задаёт тот, кто запускает контейнер, через флаги или манифест.

Контейнер глазами хоста

Сложить картину помогает один факт. С точки зрения хостовой ОС контейнер — обычный процесс. Его особенности: он помещён в отдельные namespaces, ему урезали capabilities (набор привилегий процесса), и он приписан к cgroup с лимитами.

$ ps aux | grep nginx
root  4821  0.0  0.1 nginx: master process

В ps контейнерный процесс стоит рядом с остальными процессами хоста, с обычным PID. Магии нет: namespaces прячут от него лишнее, cgroups ограничивают ресурсы, capabilities режут права.

Что важно запомнить

cgroups (control groups) ограничивают ресурсы группы процессов: CPU, память, IO, число процессов.

Лимиты живут файлами в /sys/fs/cgroup; современная версия — cgroup v2.

docker run --memory 512m --cpus 1 под капотом создаёт cgroup и пишет в memory.max и cpu.max.

namespaces про видимость, cgroups про ресурсы. Это разные механизмы.

Без лимита контейнер может занять всю память хоста и спровоцировать OOM у соседей. Лимит задаёт тот, кто запускает.

В следующем уроке — capabilities: как урезают привилегии процесса, чтобы root внутри контейнера не был root на хосте.

Прошли материал до конца? Отметьте урок пройденным.