Курс СРС SRS02
до ЛР1 рівень 3 → «5»↔ Лабораторна 1↔ Лекція 1⌨️ команди 5
Самостійна робота · ~3 год

Групи керування cgroups v2: обмеження CPU та пам'яті контейнера

🌌 Маскування енергоспоживання капсули — «Диспетчер не лише ховає — він ще й пайкує, і дивись, як гарно це влаштовано», — тішиться С.І.Д. Капсула не повинна світитися на моніторингу ресурсів «ТехНови», тож розберися разом із ним, як через cgroups v2 виставити жорсткі ліміти CPU й пам'яті, щоб капсула не видала себе аномальним стрибком навантаження.

📚 ТЕОРЕТИЧНІ ВІДОМОСТІ

Навіщо cgroups, якщо є namespaces

Простори імен відповідають на питання «що процес бачить», а групи керування (control groups, cgroups) — на питання «скільки ресурсів процес може спожити». Це два незалежні механізми ядра: namespaces ізолюють, cgroups обмежують і обліковують. Docker використовує обидва одночасно — саме тому один контейнер не бачить процесів іншого (namespaces) і водночас не може забрати всю пам'ять хоста (cgroups).

Сучасні дистрибутиви (і Docker Engine з версії 20.10+ за замовчуванням, включно з Ubuntu 24.04) працюють на cgroups v2 — уніфікованій ієрархії, змонтованій в одну точку /sys/fs/cgroup, на відміну від застарілої v1, де для кожного контролера (memory, cpu, pids, io) існувала окрема ієрархія. У v2 кожна cgroup — це каталог у файловій системі cgroupfs, а обмеження виражені простими текстовими файлами всередині нього: memory.max, memory.high, cpu.max, pids.max, io.max тощо.

Прапорець docker run --memory=100m створює для контейнера власну cgroup і записує в memory.max значення 104857600 (байти). Коли сума використаної процесами контейнера resident-пам'яті наближається до цієї межі, ядро спершу намагається звільнити кеш сторінок (reclaim), а якщо це не допомагає — активує cgroup-локальний OOM-кілер, який вбиває процес(и) тільки всередині цієї cgroup, не чіпаючи інші контейнери й хост. Це принципова відмінність від системного OOM: одна цятка на моніторингу, а не крах усієї машини.

Прапорець --cpus=0.5 транслюється в пару файлів cpu.max виду 50000 100000 — це означає «не більш ніж 50 000 мікросекунд процесорного часу з кожних 100 000 мікросекунд» (тобто половина одного ядра в середньому за період). Це м'яке обмеження через CFS bandwidth controller: контейнер може короткочасно використати ціле ядро в межах кванта, але за період буде «притиснутий» throttling'ом, що видно в cpu.stat (поле nr_throttled). Окремо варто розрізняти --cpus (частка ядра) і --cpuset-cpus (жорстка прив'язка до конкретних номерів ядер) — перше обмежує обсяг часу, друге — фізичну локацію виконання.

Для діагностики варто дивитися не лише на ліміти, а й на фактичне споживання: docker stats дає зручний живий знімок, а пряме читання /sys/fs/cgroup/.../ memory.current, memory.max, cpu.stat, pids.current — те саме джерело даних без посередників, корисне, коли потрібно зрозуміти, наскільки близько контейнер підійшов до межі ще до того, як спрацює OOM-кілер.

📖 ОПРАЦЮВАТИ

Прочитати cgroups(7) і розділ Docker docs «Resource constraints»; запустити контейнер з docker run --memory=100m --cpus=0.5 і переглянути відповідні файли в ієрархії /sys/fs/cgroup/ (memory.max, cpu.max).

🖥️ ТЕРМІНАЛ — СПРОБУЙ САМ
🎯 ЗАВДАННЯ
  1. Запустити docker run -d --memory=100m --cpus=0.5 --name capsule python:3.12-slim sleep 3600 (образ із python3 для наступного кроку; базовий ubuntu:24.04 його не містить) і переглянути значення ліміту: найпростіше зсередини — docker exec capsule cat /sys/fs/cgroup/memory.max (працює за будь-якого cgroup-драйвера), або на хості за шляхом, що залежить від драйвера — у Codespaces (cgroupfs) /sys/fs/cgroup/docker//memory.max, у systemd-драйвері /sys/fs/cgroup/system.slice/docker-.scope/memory.max (драйвер видно через docker info | grep Cgroup).
  2. Усередині цього самого контейнера навмисно виділити масив, що перевищує 100 МБ (наприклад, python3 -c 'x=bytearray(20010241024)'), і зафіксувати код завершення процесу та повідомлення в docker inspect --format '{{.State.OOMKilled}}'.
  3. Порівняти поведінку cpu.max = 50000 100000 із cpuset-cpus=0: навантажити контейнер CPU-інтенсивним циклом і подивитися nr_throttled у cpu.stat в обох варіантах.
  4. Знайти й пояснити різницю між memory.max (жорсткий ліміт, тригер OOM) і memory.high (м'який поріг, лише сповільнення через throttling) на прикладі документації cgroups v2.
  5. Написати короткий скрипт, який кожні 2 секунди читає memory.current і cpu.stat контейнера й друкує відсоток від ліміту — імітація «панелі маскування енергоспоживання» капсули.
❓ КОНТРОЛЬНІ ПИТАННЯ
  1. Чим відповідальність cgroups принципово відрізняється від відповідальності namespaces?
  2. У якому файлі cgroups v2 задається жорстка межа оперативної пам'яті, і що станеться при її перевищенні?
  3. Що означають два числа у cpu.max виду 50000 100000?
  4. Чим OOM-кілер, що спрацьовує всередині cgroup контейнера, відрізняється від системного OOM-кілера хоста за наслідками для інших процесів?
  5. У чому різниця між --cpus і --cpuset-cpus у docker run?
  6. Де саме в дереві /sys/fs/cgroup можна знайти поточне (не лімітне) споживання пам'яті контейнера, і як воно називається?
  7. Чому Docker Engine перейшов з cgroups v1 на v2 як драйвер за замовчуванням — яку структурну проблему v1 це вирішило?
✅ САМОПЕРЕВІРКА

Що станеться з процесом усередині контейнера, якщо він спробує виділити пам'яті більше, ніж дозволяє memory.max його cgroup — і чим ця поведінка відрізняється від класичного OOM на хості без лімітів?

🥚 ПАСХАЛКА
У стані спокою ядро С.І.Д. споживає рівно 3101 кілобайт — він стверджує, що це не оптимізація, а «підпис творців у hex»: 0xC1D. Тому, виставляючи memory.max, не опускайся нижче — «інакше я мовчатиму, як 101.9 у порожньому класі», — а якщо memory.current колись покаже саме це число, знай: він просто позує.
⌨️ КОМАНДИ НА ЦІЙ СТОРІНЦІ 5

Кожна команда терміналу з цієї сторінки — одним реченням. Позначка нове — команда зустрічається в курсі вперше; далі вважаємо її знайомою.

docker run
Створює й запускає контейнер з образу (docker run -it ubuntu:24.04 bash — інтерактивно з терміналом; -d — у фоні; --rm — видалити після виходу; -p 8080:80 — прокинути порт).
docker
Клієнт Docker: усі дії з образами й контейнерами — через його підкоманди (docker <підкоманда> --help — довідка).
docker exec
Виконує команду всередині вже запущеного контейнера (docker exec -it storage sh).
docker inspect
Друкує повний JSON-опис контейнера чи образу; -f '{{.State.Pid}}' — витягнути одне поле.
docker stats
Живий моніторинг CPU/пам'яті контейнерів (--no-stream — один знімок).

💡 Прочитати README прямо в терміналі: cat README.md (виведе весь файл) або less README.md (посторінково; вихід — клавіша q).
Довідка з будь-якої команди: man curl (повний посібник, вихід — q) або коротко curl --help; для підкоманд — docker run --help, cargo build --help.

← SRS01усі темиSRS03 →