Механізми контейнеризації: cgroups

механізми контейнеризації

Продовжуємо цикл статей про механізми контейнеризації.  минулого разу ми говорили про ізоляції процесів з допомогою механізму «просторів імен (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}/сдгоир (для кожного процесу) і /proc/cgroups (для системи цілому).

Архітектура

Механізм cgroups складається з двох складових частин: ядра (cgroup core) і так званих підсистем. У ядрі версії 4.4.0.21 таких підсистем 12:

  • blkio   встановлює ліміти на читання і запис з блокових пристроїв;
  • cpuacct   генерує звіти про використання ресурсів процесора;
  • cpu   забезпечує доступ процесів в рамках контрольної групи до CPU;
  • cpuset   розподіляє завдання в рамках контрольної групи між процесорними ядрами;
  • devices   дозволяє або забороняє доступ до пристроїв;
  • freezer   припиняє і відновлює виконання завдань в рамках контрольної групи
  • hugetlb   активує підтримку великих сторінок пам'яті для контрольних груп;
  • memory   управляє виділенням пам'яті для груп процесів;
  • net_cls   позначає мережеві пакети спеціальним тегом, що дозволяє ідентифікувати пакети, породжувані певної завданням у межах контрольної групи;
  • netprio   використовується для динамічної установки пріоритетів трафіку;
  • pid   використовується для обмеження кількості процесів у межах контрольної групи.


Вивести список підсистем на консоль можна з допомогою команди:

$ ls /sys/fs/cgroup/

blkio cpu,cpuacct freezer net_cls perf_event
cpu cpuset hugetlb net_cls,net_prio pids
cpuacct memory devices 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


Символами $$ позначається 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


&Nbsp;замовчуванням у групи немає ніяких обмежень доступу до пристроїв:

$ 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: що нового

Як вже було зазначено вище, сдгоир 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 pid.current. Перший з цих файлів використовується для обмеження числа процесів в групі, а    містить інформацію про числі процесів, включених у групу на поточний момент.

Всередині вже наявних груп можна створювати підгрупи:

$ mkdir /cgroup2/group1/subgroup1
$ mkdir /cgroup2/group1/subgroup2
$ echo "+memory" > /cgroup2/group1/cgroup.subtree_control, 


Всі підгрупи успадковують характеристики батьківської групи. У щойно наведеному прикладі підсистема PID буде активована як для групи group1, так і для обох вкладених в неї підгруп; в них також будуть додані файли pids.max і pids.current. Сказане можна проілюструвати з допомогою схеми:



Щоб уникнути непорозумінь з вкладеними групами (див. вище), в cgroup v2 діє наступне правило: не можна додати процес у вкладену групу, якщо в ній вже активована яка-небудь підсистема:



У першій версії cgroups процес міг входити в кілька підгруп одночасно, якщо ці підгрупи входили в різні ієрархії, вбудовані в різні підсистеми. У другої версії один процес може належати тільки до однієї підгрупи, що дозволяє уникнути плутанини.

Висновок

У цій статті ми розповіли, як влаштований механізм cgroups і які зміни були внесені в його нову версію. Якщо у вас є питання та доповнення   ласкаво просимо до коментарі.

Для всіх, хто хоче глибше зануритися в тему, наводимо список посилань на цікаві матеріали:



Якщо ви з тих чи інших причин не можете залишати коментарі тут, ласкаво просимо у наш корпоративний блог.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.