Механизмы контейнеризации: cgroups
![Тирекс](https://selectel.ru/blog/wp-content/uploads/2023/05/T-Rex_Autor-3-400x400.png)
Продолжаем цикл статей о механизмах контейнеризации. В прошлый раз мы говорили об изоляции процессов с помощью механизма «пространств имён» (namespaces). Но для контейнеризации одной лишь изоляции ресурсов недостаточно. Если мы запускаем какое-либо приложение в изолированном окружении, мы должны быть уверены в том, что этому приложению выделено достаточно ресурсов и что оно не будет потреблять лишние ресурсы, нарушая тем самым работу остальной системы. Для решения этой задачи в ядре […]
![Изображение записи](https://selectel.ru/blog/wp-content/uploads/2016/06/PR-2164_preview.png)
Продолжаем цикл статей о механизмах контейнеризации. В прошлый раз мы говорили об изоляции процессов с помощью механизма «пространств имён» (namespaces). Но для контейнеризации одной лишь изоляции ресурсов недостаточно. Если мы запускаем какое-либо приложение в изолированном окружении, мы должны быть уверены в том, что этому приложению выделено достаточно ресурсов и что оно не будет потреблять лишние ресурсы, нарушая тем самым работу остальной системы. Для решения этой задачи в ядре Linux имеется специальный механизм — cgroups (сокращение от control groups, контрольные группы). О нём мы расскажем в сегодняшней статье.
Тема cgroups сегодня особенно актуальна: в ядро версии 4.5, вышедшей в свет в январе текущего года, была официально добавлена новая версия этого механизма — group v2.
В ходе работы над ней cgroups был по сути переписан заново.
Почему потребовались столь радикальные изменения? Чтобы ответить на этот вопрос, рассмотрим в деталях, как была реализована первая версия cgroups.
Cgroups: краткая история
Разработка cgroups была начата в 2006 году сотрудниками Google Полом Менеджем и Рохитом Сетом. Термин «контрольная группа» тогда ещё не использовался, а вместо него употреблялся термин «контейнеры процессов» (process containers). Собственно, сначала они и не ставили перед собой цели создать cgroups в современном понимании. Изначальный замысел был гораздо скромнее: усовершенствовать механизм cpuset, предназначенный для распределения процессорного времени и памяти между задачами. Но со временем всё переросло в более масштабный проект.
В конце 2007 года название process containers было заменено на control groups. Это было сделано, чтобы избежать разночтений в толковании термина «контейнер» (в то время уже активно развивался проект OpenVZ, и слово «контейнер» стало употребляться в новом, современном значении).
В 2008 году механизм cgroups был официально добавлен в ядро Linux (версия 2.6.24). Что нового появилось в этой версии ядра по сравнению с предыдущими?
Ни одного системного вызова, предназначенного специально для работы с cgroups, добавлено не было. В числе главных изменений следует назвать файловую систему cgroups, известную также под названием cgroupfs.
В init/main.c были были добавлены отсылки к функциям для активации cgoups во время загрузки: cgroup_init и cgroup_init_early. Были незначительно изменены функции, используемые для порождения и завершения процесса — fork() и exit().
В виртуальной файловой системе /proc появились новые директории: /proc/{pid}/сgroup (для каждого процесса) и /proc/cgroups (для системы в целом).
Архитектура
Механизм cgroups состоит из двух составных частей: ядра (cgroup core) и так называемых подсистем. В ядре версии 4.4.0.21 таких подсистем 12:
Имя | Модуль ядра | Функция |
---|---|---|
blkio | block/blkcroup.c | Устанавливает лимиты на чтение и запись с блочных устройств |
cpuacct | kernel/sched/cpuacct.c | Предназначен для генерации отчётов об использовании ресурсов процессора |
cpu | kernel/sched/core.c | Обеспечивает доступ процессов в рамках контрольной группы к CPU |
cpuset | kernel/cpuset.c | Распределяет задачи в рамках контрольной группы между процессорными ядрами |
devices | security/device_cgroup.c | Разрешает или блокирует доступ к устройствам |
freezer | kernel/cgroup_freezer.c | Приостанавливает и возобновляет выполнение задач в рамках контрольной группы |
hugetlb | mm/hugetlb_cgroup.c | Активирует поддержку больших страниц памяти для контрольных групп |
memory | mm/memcontrol.c | Управляет выделением памяти для групп процессов |
net_cls | net/core/netclassid_cgroup.c | Помечает сетевые пакеты специальным тэгом, что позволяет идентифицировать пакеты, порождаемые определённой задачей в рамках контрольной группы |
net_prio | net/core/netprio_cgroup.c | Используется для динамической установки приоритетов по трафику |
perf_event | evens/kernel.c | Обеспечивает доступ контрольных групп к perf_events |
pids | kernel/cgroup_pids.c | Используется для ограничения количества процессов в рамках контрольной группы |
Вывести список подсистем на консоль можно с помощью команды:
$ ls /sys/fs/cgroup/
blkio cpu,cpuacct freezer net_cls perf_event
cpu cpuset hugetlb net_cls,net_prio pids
cpuacct devices memory net_prio systemd
Каждая подсистема представляет собой директорию с управляющими файлами, в которых прописываются все настройки. В каждой из этих директорий имеются следующие управляющие файлы:
- cgroup.clone_children — позволяет передавать дочерним контрольным группам свойства родительских;
- tasks — содержит список PID всех процессов, включённых в контрольные группы;
cgroup.procs — содержит список TGID групп процессов, включённых в контрольные группы; - cgroup.event_control — позволяет отправлять уведомления в случае изменения статуса контрольной группы;
- release_agent — содержится команда, которая будет выполнена, если включена опция notify_on_release. Может использоваться, например, для автоматического удаления пустых контрольных групп;
- notify_on_release — содержит булеву переменную (0 или 1), включающую (или наоборот отключающую), выполнение команду, указанной в release_agent.
У каждой подсистемы имеются также собственные управляющие файлы. О некоторых из них мы расскажем ниже.
Чтобы создать контрольную группу, достаточно создать вложенную директорию в любой из подсистем. В эту вложенную директорию будут автоматически добавлены управляющие файлы (ниже мы расскажем об этом более подробно). Добавить процессы в группу очень просто: нужно просто записать их PID в управляющий файл tasks.
Совокупность контрольных групп, встроенных в подсистему, называется иерархией.Попробуем разобрать принципы функционирования cgroups на простых практических примерах.
Иерархия cgroups: практическое знакомство
Пример 1: управление процессорными ресурсами
Выполним команду:
$ mkdir /sys/fs/cgroup/cpuset/group0
С помощью этой команды мы создали контрольную группу, в которой содержатся следующие управляющие файлы:
$ ls /sys/fs/cgroup/cpuset/group0
group.clone_children cpuset.memory_pressure
cgroup.procs cpuset.memory_spread_page
cpuset.cpu_exclusive cpuset.memory_spread_slab
cpuset.cpus cpuset.mems
cpuset.effective_cpus cpuset.sched_load_balance
cpuset.effective_mems cpuset.sched_relax_domain_level
cpuset.mem_exclusive notify_on_release
cpuset.mem_hardwall tasks
cpuset.memory_migrate
Пока что в нашей группе никаких процессов нет. Чтобы добавить процесс, нужно записать его PID в файл tasks, например:
$ echo $$ > /sys/fs/cgroup/cpuset/group0/tasks
Cимволами $$ обозначается PID процесса, выполняемого текущей командной оболочкой.
Этот процесс не закреплён ни за одним ядром CPU, что подтверждает следующая команда:
$ cat /proc/$$/status |grep '_allowed'
Cpus_allowed: 2
Cpus_allowed_list: 0-1
Mems_allowed: 00000000,00000001
Mems_allowed_list: 0
Вывод этой команды показывает, что для интересующего нас процесса доступны 2 ядра CPU с номерами 0 и 1.
Попробуем «привязать» этот процесс к ядру с номером 0:
$ echo 0 >/sys/fs/cgroup/cpuset/group0/cpuset.cpus
Проверим, что получилось:
$ cat /proc/$$/status |grep '_allowed'
Cpus_allowed: 1
Cpus_allowed_list: 0
Mems_allowed: 00000000,00000001
Mems_allowed_list: 0
Пример 2: управление памятью
Встроим созданную в предыдущем примере группу ещё в одну подсистему:
$ mkdir /sys/fs/cgroup/memory/group0
Далее выполним:
$ echo $$ > /sys/fs/cgroup/memory/group0/tasks
Попробуем ограничить для контрольной группы group0 потребление памяти. Для этого нам понадобится прописать соответствующий лимит в файле memory.limit_in_bytes:
$ echo 40M > /sys/fs/cgroup/memory/group0/memory.limit_in_bytes
Механизм cgroups предоставляет очень обширные возможности управления памятью. Например, с его помощью мы можем оградить критически важные процессы от попадания под горячую руку OOM-killer’a:
$ echo 1 > /sys/fs/cgroup/memory/group0/memory.oom_control
$ cat /sys/fs/cgroup/memory/group0/memory.oom_control
oom_kill_disable 1
under_oom 0
Если мы поместим в отдельную контрольную группу, например, ssh-демон и отключим для этой группы OOM-killer, то мы можем быть уверены в том, что он не будет «убит» при преувеличении потребления памяти.
Пример 3: управление устройствами
Добавим нашу контрольную группу ещё в одну иерархию:
$ mkdir /sys/fs/cgroup/devices/group0
По умолчанию у группы нет никаких ограничений доступа к устройствам:
$ cat /sys/fs/cgroup/devices/group0/devices.list
a *:* rwm
Попробуем выставить ограничения:
$ echo 'c 1:3 rmw' > /sys/fs/cgroup/devices/group0/devices.deny
Эта команда включит устройство /dev/null в список запрещённых для нашей контрольной группы. Мы записали в управляющий файл строку вида ‘c 1:3 rmw’. Сначала мы указываем тип устройства — в нашем случае это символьное устройство, обозначаемое буквой с (сокращение от character device). Два других типа устройств — это блочные (b) и все возможные устройства (а). Далее следуют мажорный и минорный номера устройства. Узнать номера можно с помощью команды вида:
$ ls -l /dev/null
Вместо /dev/null, естественно, можно указать любой другой путь. Вывод этой команды выглядит так:
crw-rw-rw- 1 root root 1, 3 May 30 10:49 /dev/null
Первая цифра в выводе — это мажорный, а вторая — минорный номер.
Три последние буквы означают права доступа: r — разрешение читать файлы с указанного устройства, w — разрешение записывать на указанное устройство, m — разрешение создавать новые файлы устройств.
Далее выполним:
$ echo $$ > /sys/fs/cgroup/devices/group0/tasks
$ echo "test" > /dev/null
При выполнении последней команды система выдаст сообщение об ошибке:
-bash: /dev/null: Operation not permitted
С устройством /dev/null мы никак взаимодействовать не можем, потому что доступ закрыт.
Восстановим доступ:
$ echo a > /sys/fs/cgroup/devices/group0/devices.allow
В результате выполнения этой команды в файл /sys/fs/cgroup/devices/group0/devices.allow будет добавлена запись a *:* rwm, и все ограничения будут сняты.
Cgroups и контейнеры
Из приведённых примеров понятно, в чём заключается принцип работы cgroups: мы помещаем определённые процессы в группу, которую затем «встраиваем» в подсистемы. Разберём теперь более сложные примеры и рассмотрим, как cgroups используются в современных инструментах контейнеризации на примере LXC.
Установим LXC и создадим контейнер:
$ sudo apt-get install lxc debootstrap bridge-utils
$ sudo lxc-create -n ubuntu -t ubuntu -f /usr/share/doc/lxc/examples/lxc-veth.conf
$ lxc-start -d -n ubuntu
Посмотрим, что изменилось в директории cgroups после создания и запуска контейнера:
$ ls /sys/fs/cgroup/memory
cgroup.clone_children memory.limit_in_bytes memory.swappiness
cgroup.event_control memory.max_usage_in_bytes memory.usage_in_bytes
cgroup.procs memory.move_charge_at_immigrate memory.use_hierarchy
cgroup.sane_behavior memory.numa_stat notify_on_release
lxc memory.oom_control release_agent
memory.failcnt memory.pressure_level tasks
memory.force_empty memory.soft_limit_in_bytes
Как видим, в каждой иерархии появилась директория lxc, которая в свою очередь содержит поддиректорию Ubuntu. Для каждого нового контейнера в директории lxc будет создаваться отдельная поддиректория. PID всех запускаемых в этом контейнере процессов будут записываться в файл /sys/fs/cgroup/cpu/lxc/[имя контейнера]/tasks
Выделять ресурсы для контейнеров можно как с помощью управляющих файлов cgroups, так и с помощью специальных команд lxc, например:
$ lxc-cgroup -n [имя контейнера] memory.limit_in_bytes 400
Аналогичным образом дело обстоит с контейнерами Docker, systemd-nspawn и другими.
Недостатки cgroups
На протяжении почти 10 лет существования механизм cgroups неоднократно подвергался критике. Как отметил автор одной статьи на LWN.net, разработчики ядра cgroups активно не любят. Причины такой нелюбви можно понять даже из приведённых в этой статье примеров, хоть мы и старались подавать их максимально нейтрально, без эмоций: встраивать контрольную группу в каждую подсистему по отдельности очень неудобно. Присмотревшись повнимательней, мы увидим, что такой подход отличается крайней непоследовательностью.
Если мы, например, создаём вложенную контрольную группу, то в некоторых подсистемах настройки родительской группы наследуются, а в некоторых — нет.
В подсистеме cpuset любое изменение в родительской контрольной группе автоматически передаётся вложенным группам, а в других подсистемах такого нет и нужно активировать параметр clone.children.
Об устранении этих и других недостатков cgroups разговоры в сообществе разработчиков ядра шли очень давно: один из первых текстов на эту тему датируется началом 2012 года.
Автор этого текста, инженер Facebook Течжен Хе, прямо указал, что главная проблема cgroups заключается в неправильной организации, при которой подсистемы подключаются к многочисленным иерархиям контрольных групп. Он предложил использовать одну и только одну иерархию, а подсистемы добавлять для каждой группы отдельно. Такой подход повлёк за собой серьёзные изменения вплоть до смены названия: механизм изоляции ресурсов теперь называется cgroup (в единственном числе), а не cgroups.
Разберёмся более подробно в сути реализованных нововведений.
Cgroup v2: что нового
Как уже было отмечено выше, сgroup v2 был включён в ядро Linux начиная с версии ядра 4.5. При этом старая версия поддерживается тоже. Для версии 4.6 уже существует патч, с помощью которого можно отключить поддержку первой версии при загрузке ядра.
На текущий момент в cgroup v2 можно работать только с тремя подсистемами: blkio, memory и PID. Уже появились (пока что в тестовом варианте) патчи, позволяющие управлять ресурсами CPU.
Cgroup v2 монтируется при помощи следующей команды:
$ mount -t cgroup2 none [точка монтирования]
Предположим, мы смонтировали cgroup 2 в директорию /cgroup2. В этой директории будут автоматически созданы следующие управляющие файлы:
- cgroup.controllers — содержит список поддерживаемых подсистем;
- cgroup.procs — по завершении монтирования содержит список всех выполняемых процессов в системе, включая процессы-зомби. Если мы создадим группу, то для неё тоже будет создан такой файл; он будет пустым, пока в группу не добавлены процессы;
- cgroup.subtree_control — содержит список подсистем, активированных для данной контрольной группы; по умолчанию пуст.
Эти же самые файлы создаются в каждой новой контрольной группе. Также в группу добавляется файл cgroup.events, который в корневой директории отсутствует.
Новая группа создаётся так:
$ mkdir /cgroup2/group1
Чтобы добавить для группы подсистему, нужно записать имя этой подсистемы в файл cgroup.subtree_control:
$ echo "+pid" > /cgroup2/group1/cgroup.subtree_control
Для удаления подсистемы используется аналогичная команда, только на место плюса ставится минус:
$ echo "-pid" > /cgroup2/group1/cgroup.subtree_control
Когда для группы активируется подсистема, в ней создаются дополнительные управляющие файлы. Например, после активации подсистемы PID в директории появятся файлы pids.max и pids.current. Первый из этих файлов используется для ограничения числа процессов в группе, а второй — содержит информацию о числе процессов, включённых в группу на текущий момент.
Внутри уже имеющихся групп можно создавать подгруппы:
$ mkdir /cgroup2/group1/subgroup1
$ mkdir /cgroup2/group1/subgroup2
$ echo "+memory" > /cgroup2/group1/cgroup.subtree_control,
Все подгруппы наследуют характеристики родительской группы. В только что приведённом примере подсистема PID будет активирована как для группы group1, так и для обеих вложенных в неё подгрупп; в них также будут добавлены файлы pids.max и pids.current. Сказанное можно проиллюстрировать с помощью схемы:
![cgroup v2](https://selectel.ru/blog/wp-content/uploads/2016/06/PR-2240-01.png)
Чтобы избежать недоразумений с вложенными группами (см. выше), в cgroup v2 действует следующее правило: нельзя добавить процесс во вложенную группу, если в ней уже активирована какая-либо подсистема:
![cgroup v2](https://selectel.ru/blog/wp-content/uploads/2016/06/PR-2240-02.png)
В первой версии cgroups процесс мог входить в несколько подгрупп одновременно, если эти подгруппы входили в разные иерархии, встроенные в разные подсистемы. Во второй версии один процесс может принадлежать только к одной подгруппе, что позволяет избежать путаницы.
Заключение
В этой статье мы рассказали, как устроен механизм cgroups и какие изменения были внесены в его новую версию. Если у вас есть вопросы и дополнения — добро пожаловать в комментарии.
Для всех, кто хочет глубже погрузиться в тему, приводим список ссылок на интересные материалы:
- https://www.kernel.org/doc/Documentation/cgroup-v1/cgroups.txt — документация первой версии cgroups;
- https://www.kernel.org/doc/Documentation/cgroup-v2.txt — документация cgroup v2;
- https://www.youtube.com/watch?v=PzpG40WiEfM — лекция Течжена Хе о нововведениях cgroup v2;
- https://events.linuxfoundation.org/sites/events/files/slides/2014-KLF.pdf — презентация доклада о cgroup v2 с подробными разъяснениями всех нововведений и изменений.