Docker на cgroups v2: біль, помилки та шлях виправлення

Було корисно?

Ви оновили дистрибутив. Docker раніше працював. Тепер ні. Або гірше: він запускається, але обмеження ресурсів не застосовуються, CPU поводяться дивно, і ваш контейнер із «пам’ять обмежена 2G» пожирає хост. Ви дивитесь на docker run, ніби він зрадив саме вас.

cgroups v2 — це краща інженерія. Водночас це податок сумісності. Це польовий посібник для точних помилок, які ви побачите, що вони означають, і шляху виправлення, який не вимагає молитов біля /sys/fs/cgroup.

Що змінилося в cgroups v2 (і чому Docker це відчуває)

Control groups («cgroups») — це механізм ядра для обліку та обмеження ресурсів: CPU, пам’ять, IO, pids тощо. Docker використовує їх для реалізації прапорів типу --memory, --cpus, --pids-limit і щоб тримати процеси контейнерів у впорядкованому дереві.

cgroups v1 і v2 — це не просто різні версії. Це різні моделі.

cgroups v1: багато ієрархій, багато підводних каменів

v1 дозволяє кожному контролеру (cpu, memory, blkio тощо) монтуватися окремо. Така гнучкість дала дистрибутивам хобі: монтувати контролери в різні місця й дивитися, як інструменти роблять помилкові припущення. Docker виріс у цьому середовищі. Як і багато напівпрацюючих скриптів.

cgroups v2: одна ієрархія, узгоджені правила, інша семантика

v2 — це «уніфіковане дерево»: один корінь, контролери вмикаються для піддерев і є правила делегування. Це більш послідовно. Також це означає, що програмне забезпечення має бути явним у тому, як створює й керує cgroup, і чи відповідає за це systemd.

Де Docker спотикається

  • Невідповідність драйвера: Docker може сам керувати cgroups (драйвер cgroupfs) або делегувати systemd (драйвер systemd). На v2 systemd як менеджер — це бажаний шлях на більшості дистрибутивів з systemd.
  • Старі runc/containerd: початкова підтримка v2 була часткова; старі версії видають «unsupported» або мовчки ігнорують обмеження.
  • Обмеження rootless: делегування в v2 суворіше. Rootless Docker може працювати, але він залежить від systemd user сервісів і правильної делегації.
  • Гібридні режими: деякі системи працюють у «змішаному» v1/v2 режимі. Це рецепт для «працює на цьому хості, але не на тому».

Парафраз ідеї від John Allspaw (reliability engineering): «Блимання рідко вирішує інциденти; розуміння систем — так.» Це позиція, яка вам потрібна з cgroups v2: припиніть сперечатися із симптомом, замапте систему.

Факти й коротка історія, що зекономить вам час

Ось кілька малих, конкретних істин, які рятують від дебагу неправильного шару.

  1. cgroups були влиті в Linux у 2007 (в рамках зусиль «process containers»). Початкова модель передбачала багато незалежних ієрархій.
  2. cgroups v2 почали з’являтися близько 2016 року щоб виправити фрагментацію v1 і тонкі баги семантики, особливо щодо пам’яті та делегування.
  3. systemd глибоко інтегрував cgroups і став менеджером cgroup за замовчуванням у багатьох дистрибутивах; Docker спочатку опирався, потім зійшовся на драйвері systemd як розумному дефолті для сучасних систем.
  4. v2 змінює поведінку пам’яті: облік і застосування пам’яті більш чисті, але потрібно розуміти memory.max, memory.high і сигнали тиску. Старі звички «oom-kill як flow control» проявляються гостріше.
  5. IO контролі змінили назви й значення: у v1 blkio.* стає у v2 io.*, і деякі старі поради з налаштування вже не діють.
  6. v2 вимагає явного вмикання контролерів: можна мати дерево cgroup, де контролери присутні, але не увімкнені для піддерева, що породжує поведінку «файл не знайдено», яка виглядає як проблема з правами.
  7. Делегування навмисно суворе: v2 не дозволяє непривілейованим процесам створювати довільні піддерева, якщо батько не налаштований для делегування. Ось чому rootless конфігурації дають нові помилки.
  8. Деякі прапори Docker залежать від конфігурації ядра: навіть при v2 відсутні можливості ядра дають заплутані помилки «не підтримується», які виглядають як баг Docker.

Помилки, які ви побачите в реальному житті

Невдачі cgroups v2 групуються в три кошики: запуск демона, запуск контейнера і «воно працює, але обмеження — ні». Ось класика з поясненнями, що вони зазвичай означають.

Демон не стартує

  • failed to mount cgroup або cgroup mountpoint does not exist: Docker очікує макет v1, але хост — v2 unified, або монти відсутні/заблоковані.
  • cgroup2: unknown option при монтуванні: ядро занадто старе або опції монтування невідповідні для цього ядра.
  • OCI runtime create failed: ... cgroup ... під час ініціалізації демона: невідповідність runc/containerd і режиму cgroup хоста.

Контейнер не стартує (помилки OCI)

  • OCI runtime create failed: unable to apply cgroup configuration: ... no such file or directory: файли контролера недоступні в цільовому cgroup, часто тому, що контролери не увімкнені в тому піддереві.
  • permission denied при записі в /sys/fs/cgroup/...: проблема делегування (rootless або вкладений менеджер cgroup) або systemd володіє піддеревом, а Docker намагається використовувати cgroupfs.
  • cannot set memory limit: ... invalid argument: використання гвинтиків з ери v1 або ядро не підтримує конкретну опцію; також трапляється при некоректних налаштуваннях swap.

Контейнери працюють, але обмеження не застосовуються

  • docker stats показує необмежену пам’ять: Docker не може прочитати обмеження пам’яті з очікуваного шляху cgroup v2; невідповідність драйвера або застарілий Docker.
  • CPU квоти ігноруються: контролер cpu не увімкнений для того піддерева, або ви використовуєте конфігурацію драйвера cgroup, яка ніколи не прикріплює задачі туди, куди ви думаєте.
  • IO-тротлінг не працює: ви на v2, але ще намагаєтеся налаштовувати blkio; або драйвер блочного пристрою не підтримує бажану політику.

Жарт №1: cgroups v2 — як дорослішання: більше структури, менше лазівок, і все те, що «працювало», тепер незаконно.

Швидкий план діагностики (перший/другий/третій)

Якщо у вас лише п’ять хвилин до змін вікна, зробіть це в порядку.

Перше: підтвердьте, в якому режимі cgroup реально працює хост

Не робіть висновків за версією дистрибутива. Перевірте файлову систему й прапори ядра.

  • Чи тип /sys/fs/cgroupcgroup2 (уніфікований), чи багато v1-монтів?
  • Чи система в гібридному режимі?

Друге: перевірте драйвер cgroup Docker та версії runtime

Більшість «біль від cgroups v2» — це або неправильний драйвер, або старий runtime.

  • Чи Docker використовує systemd чи cgroupfs?
  • Чи containerd та runc достатньо нові для вашого ядра/дистрибутива?

Третє: перевірте наявність контролерів і делегування

Якщо конкретне обмеження не працює (пам’ять, CPU, pids), підтвердьте, що контролер увімкнений там, де Docker розміщує контейнери.

  • cgroup.controllers існує і перераховує контролер.
  • cgroup.subtree_control включає його для батьківського cgroup.
  • Права доступу й власність мають сенс (особливо в rootless режимі).

Практичні завдання: команди, значення виводу та рішення

Це «перестаньте гадати» завдання. Кожне містить: команду, що означає вивід і яке рішення прийняти далі.

Завдання 1: Визначити тип файлової системи cgroup (v2 проти v1)

cr0x@server:~$ stat -fc %T /sys/fs/cgroup
cgroup2fs

Значення: cgroup2fs означає уніфіковані cgroups v2. Якщо ви бачите tmpfs тут і окремі монти контролерів — ймовірно v1/гібрид.

Рішення: Якщо це v2, плануйте Docker + systemd драйвер (або принаймні перевірте сумісність). Якщо це v1/гібрид, вирішіть, чи мігрувати, чи примусово використовувати legacy-режим для узгодженості.

Завдання 2: Підтвердити, що змонтовано під /sys/fs/cgroup

cr0x@server:~$ mount | grep cgroup
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)

Значення: Одиночний cgroup2 монт: уніфікований режим. Якщо ви бачите багато рядків на кшталт cgroup on /sys/fs/cgroup/memory, це v1.

Рішення: Якщо уніфікований, припиніть дебажити v1-шляхи на кшталт /sys/fs/cgroup/memory. Їх там не буде.

Завдання 3: Перевірити параметри завантаження ядра, що впливають на cgroups

cr0x@server:~$ cat /proc/cmdline
BOOT_IMAGE=/vmlinuz-6.5.0 root=/dev/mapper/vg0-root ro quiet systemd.unified_cgroup_hierarchy=1

Значення: systemd.unified_cgroup_hierarchy=1 примушує уніфіковану v2. Деякі системи використовують інверсію, щоб примусити v1.

Рішення: Якщо ваша організація хоче передбачуваної поведінки, стандартизуйте цей прапор по всьому кластеру (або v2 скрізь, або v1 скрізь). Змішані кластери — джерело радості для on-call у найгіршому сенсі.

Завдання 4: Запитати у systemd, що він думає про cgroups

cr0x@server:~$ systemd-analyze --version
systemd 253 (253.5-1)
+PAM +AUDIT +SELINUX +APPARMOR +IMA +SMACK +SECCOMP +GCRYPT -GNUTLS +OPENSSL +ACL +BLKID +CURL +ELFUTILS +FIDO2 +IDN2 -IDN +IPTC +KMOD +LIBCRYPTSETUP +LIBFDISK +PCRE2 -PWQUALITY +P11KIT +QRENCODE +TPM2 +BZIP2 +LZ4 +XZ +ZLIB +ZSTD -BPF_FRAMEWORK -XKBCOMMON +UTMP +SYSVINIT default-hierarchy=unified

Значення: default-hierarchy=unified означає, що systemd очікує cgroups v2.

Рішення: Якщо systemd уніфікований, узгодьте Docker з systemd cgroup драйвером, якщо немає вагомої причини інакше.

Завдання 5: Перевірити драйвер cgroup Docker і версію cgroup

cr0x@server:~$ docker info --format '{{json .CgroupVersion}} {{json .CgroupDriver}}'
"2" "systemd"

Значення: Docker бачить cgroups v2 і використовує драйвер systemd. Це стабільна комбінація на сучасних дистрибутивах з systemd.

Рішення: Якщо ви отримаєте "2" "cgroupfs" на systemd-хості, подумайте про перехід на systemd драйвер, щоб уникнути сюрпризів з делегуванням і subtree-control.

Завдання 6: Перевірити версії компонентів runtime (containerd, runc)

cr0x@server:~$ docker info | egrep -i 'containerd|runc|cgroup'
 Cgroup Driver: systemd
 Cgroup Version: 2
 containerd version: 1.7.2
 runc version: 1.1.7

Значення: Старі комбінації containerd/runc — це місця, де підтримка v2 стає «креативною». Сучасні версії менш драматичні.

Рішення: Якщо у вас зафіксовано старий пакет Docker Engine заради «стабільності», розфіксуйте його — бо тепер він нестабільний. Оновіть engine/runc/containerd разом, де це можливо.

Завдання 7: Перевірити, що контролери існують (глобально на хості)

cr0x@server:~$ cat /sys/fs/cgroup/cgroup.controllers
cpuset cpu io memory hugetlb pids rdma misc

Значення: Це контролери, які підтримує ядро і які експонуються в уніфікованому дереві.

Рішення: Якщо потрібний контролер (наприклад memory або io) відсутній, це проблема ядра/функцій, а не налаштування Docker.

Завдання 8: Переконатися, що контролери увімкнені для піддерева, яке використовує Docker

cr0x@server:~$ cat /sys/fs/cgroup/cgroup.subtree_control
+cpu +io +memory +pids

Значення: Список з плюсом вказує, які контролери увімкнені для дочірніх cgroup на цьому рівні. Якщо +memory тут відсутній, у дочірніх cgroup не буде memory.max тощо.

Рішення: Якщо контролери не увімкнені, виправте конфігурацію батьківського cgroup (часто через налаштування systemd unit) або перемістіть розміщення Docker в делеговане піддерево (драйвер systemd допомагає).

Завдання 9: Переконатися, де Docker розміщує контейнер у дереві cgroup

cr0x@server:~$ docker run -d --name cgtest --memory 256m --cpus 0.5 busybox:latest sleep 100000
b7d32b3f6b1f3c3b2c0b9c9f8a7a6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9

cr0x@server:~$ docker inspect --format '{{.State.Pid}}' cgtest
22145

cr0x@server:~$ cat /proc/22145/cgroup
0::/system.slice/docker-b7d32b3f6b1f3c3b2c0b9c9f8a7a6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9.scope

Значення: На v2 один запис (0::). Контейнер знаходиться в systemd scope під system.slice, що й потрібно при використанні systemd драйвера cgroup.

Рішення: Якщо контейнер опиняється в несподіваному місці (або в cgroup, створеному Docker поза деревом systemd), узгодьте драйвер Docker і конфігурацію systemd.

Завдання 10: Підтвердити, що обмеження пам’яті фактично застосоване (файли v2)

cr0x@server:~$ CGPATH=/sys/fs/cgroup/system.slice/docker-b7d32b3f6b1f3c3b2c0b9c9f8a7a6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9.scope
cr0x@server:~$ cat $CGPATH/memory.max
268435456

cr0x@server:~$ cat $CGPATH/memory.current
1904640

Значення: memory.max в байтах. 268435456 — це 256 MiB. memory.current показує поточне використання.

Рішення: Якщо memory.maxmax (необмежено), незважаючи на прапори Docker, у вас невідповідність розміщення cgroup/драйвера або баг у runtime. Не налаштовуйте додаток — виправляйте cgroup plumbing.

Завдання 11: Підтвердити, що CPU-квота застосована (семантика v2)

cr0x@server:~$ cat $CGPATH/cpu.max
50000 100000

Значення: У v2 cpu.max це «квота/період». Тут: 50ms квота на 100ms період = 0.5 CPU.

Рішення: Якщо ви бачите max 100000, квота не застосована. Перевірте, чи контролер cpu увімкнений і чи Docker справді вшановував --cpus у вашій версії.

Завдання 12: Перевірити ліміт pids (часта «працює доти, доки не перестає»)

cr0x@server:~$ cat $CGPATH/pids.max
1024

cr0x@server:~$ cat $CGPATH/pids.current
3

Значення: Контроль pids працює. Без цього fork-bomb може стати «неочікуваним тестом навантаження».

Рішення: Якщо pids.max відсутній, контролери не увімкнені для піддерева. Виправляйте делегування/увімкнення, а не прапори Docker.

Завдання 13: Шукати «димову шашку» в journald

cr0x@server:~$ journalctl -u docker -b --no-pager | tail -n 20
Jan 03 08:12:11 server dockerd[1190]: time="2026-01-03T08:12:11.332111223Z" level=error msg="failed to create shim task" error="OCI runtime create failed: unable to apply cgroup configuration: mkdir /sys/fs/cgroup/system.slice/docker.service/docker/xyz: permission denied: unknown"
Jan 03 08:12:11 server dockerd[1190]: time="2026-01-03T08:12:11.332188001Z" level=error msg="failed to start daemon" error="... permission denied ..."

Значення: Docker намагається створити cgroup у місці, яке systemd не дозволяє (або процесу не вистачає прав делегування). Шлях дає підказку.

Рішення: Перейдіть на systemd драйвер cgroup, або виправте правила делегування для rootless/вкладених випадків, замість того щоб змінювати права на випадкових sysfs-файлах (це не ремонт — це визнання провалу).

Завдання 14: Підтвердити, що systemd — менеджер cgroup для Docker

cr0x@server:~$ systemctl show docker --property=Delegate,Slice,ControlGroup
Delegate=yes
Slice=system.slice
ControlGroup=/system.slice/docker.service

Значення: Delegate=yes критично: воно дозволяє systemd дозволити сервісу створювати/керувати під-cgroup. Без цього делегування v2 ламається так, що виглядає як випадкові помилки доступу.

Рішення: Якщо Delegate=no, виправте unit drop-in (або використайте пакований unit, який вже встановлює це правильно). Часто саме це і є проблемою.

Завдання 15: Виявити rootless режим (інші правила, інший біль)

cr0x@server:~$ docker info --format 'rootless={{.SecurityOptions}}'
rootless=[name=seccomp,profile=default name=rootless]

Значення: Rootless змінює, до яких cgroup ви маєте доступ, і як має бути налаштоване делегування в користувацьких сесіях systemd.

Рішення: Якщо rootless увімкнено і записи в cgroup не вдаються, припиніть ламати речі в /sys/fs/cgroup від root. Налаштуйте systemd user сервіси і делегування правильно або прийміть обмеження функціоналу.

Шлях виправлення (оберіть маршрут, не гадaйте)

Існує лише кілька стабільних кінцевих станів. Оберіть один свідомо. «Працює на моєму ноуті» — це не архітектура.

Маршрут A (рекомендовано для systemd дистрибутивів): cgroups v2 + Docker systemd драйвер

Це найчистіший варіант на сучасних Ubuntu/Debian/Fedora/RHEL-подібних системах, де systemd — PID 1 і уніфікована ієрархія за замовчуванням.

1) Переконайтеся, що Docker налаштований на systemd cgroup драйвер

Спочатку перевірте поточний стан (Завдання 5). Якщо не systemd — встановіть.

cr0x@server:~$ sudo mkdir -p /etc/docker
cr0x@server:~$ sudo tee /etc/docker/daemon.json >/dev/null <<'EOF'
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "journald"
}
EOF

Що це означає: Це каже dockerd створювати контейнери під systemd-управлінням cgroup, а не керувати власною ієрархією cgroupfs.

Рішення: Якщо ви запускаєте Kubernetes, переконайтеся, що kubelet використовує той самий драйвер. Невідповідність — класичний режим «нода виглядає нормально, поки не перестає».

2) Перезапустіть Docker і перевірте

cr0x@server:~$ sudo systemctl daemon-reload
cr0x@server:~$ sudo systemctl restart docker
cr0x@server:~$ docker info | egrep -i 'Cgroup Driver|Cgroup Version'
 Cgroup Driver: systemd
 Cgroup Version: 2

Що це означає: Ви узгоджені: systemd + v2 + Docker драйвер.

Рішення: Якщо Docker не стартує, негайно перевірте journald (Завдання 13). Не «просто перезавантажуйте». Перезавантаження перетворює відтворювану проблему на фольклор.

3) Підтвердити, що делегування правильне в docker.service unit

Більшість пакованих unit налаштовані правильно. Кастомні часто — ні.

cr0x@server:~$ systemctl show docker --property=Delegate
Delegate=yes

Рішення: Якщо Delegate=no, додайте drop-in:

cr0x@server:~$ sudo systemctl edit docker <<'EOF'
[Service]
Delegate=yes
EOF

Маршрут B: примусово включити cgroups v1 (legacy режим) щоб виграти час

Це тактичний відступ. Підходить, коли у вас сторонні агенти, старі ядра або апаратура від вендора, яка не витримує v2. Але ставте це як борг з відсотками.

1) Переключити systemd на legacy ієрархію через рядок завантаження ядра

Точний механізм залежить від дистрибутиву/завантажувача. Принцип: встановити параметр ядра, щоб відключити уніфіковану ієрархію.

cr0x@server:~$ cat /proc/cmdline
BOOT_IMAGE=/vmlinuz-6.5.0 root=/dev/mapper/vg0-root ro quiet systemd.unified_cgroup_hierarchy=0

Значення: Ви примусово переходите на v1/гібридну поведінку.

Рішення: Якщо ви так робите, документуйте це й зробіть стандартом у пулі вузлів. Напів-v2 флот — шлях до «тільки ця зона доступності зламана».

Маршрут C: rootless Docker на cgroups v2 (працює, але не романтизуйте)

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

Типові правила:

  • Використовуйте systemd user сервіси, де можливо.
  • Очікуйте, що деякі контролі ресурсів поводитимуться інакше, ніж у rootful-режимі.
  • Прийміть, що безпека та експлуатаційність — це компроміс.

Жарт №2: Rootless Docker з cgroups v2 цілком реальний, як робити еспресо під час їзди на велосипеді — можна, але не пробуйте вперше під час інциденту.

Маршрут D: containerd напряму (коли ергономіка Docker не варта витрат)

Деякі організації переходять на containerd + nerdctl (або оркестрацію), щоб зменшити кількість шарів. Якщо ви вже глибоко в Kubernetes, цінність Docker — це переважно UX для розробників. У продакшні менше шарів може означати менше сюрпризів з cgroup.

Але шлях виправлення той самий: зробіть systemd менеджером cgroup, тримайте containerd/runc актуальними і перевіряйте увімкнення контролерів.

Три корпоративні міні-історії (болісно правдоподібні)

Міні-історія 1: Інцидент через хибне припущення

Компанія мігрувала великий пакет робочих навантажень зі старішої LTS на новішу. У завданні зміни було: «Оновити Docker + патчі безпеки». Припущення було просте: «Ті самі контейнери, ті самі ліміти».

Після оновлення Docker стартував. Завдання стартували. Все виглядало зеленим. Потім хости почали свопатись, латентність зросла, і несуміжні сервіси почали таймаути. On-call робив звичне: перезапускав контейнери, зливав вузли, звинувачував «шумних сусідів». Ста́ло гірше.

Хибне припущення було в тому, що --memory застосовується. На новій ОС хост був на cgroups v2. Docker усе ще використовував драйвер cgroupfs, бо у старому golden image лишився daemon.json з минулих років. Контейнери розміщувалися в піддереві без увімкненого memory контролера. Ядро не ігнорувало обмеження зі зла — там просто не було місця, куди їх застосувати.

Вирішення не було в налаштуванні завдань. Вирішення — узгодити Docker на systemd драйвер, перевірити Delegate=yes і довести застосування, читаючи memory.max у scope cgroup контейнера. Дія у розборі — не «моніторити пам’ять сильніше», а «стандартизувати режим cgroup і драйвер на рівні флоту».

Міні-історія 2: Оптимізація, що повернулась бумерангом

Платформна команда спробувала зменшити шум від CPU-тротлінгу, «пом’якшивши» квоти. Вони перевели багато сервісів зі строгих квот на shares, вважаючи, що це зменшить конфлікти й підвищить загальний пропуск.

Спочатку все було добре: менше алертів про тротлінг, краща медіанна латентність. Потім важкий батч-сервіс вистрілив новою версією і почав періодичні сплески. Ці сплески були дозволені під shares, але вони вкрали стільки CPU, що latency-чутливе API потрапило у хвіст латентності.

Модель команди була з ери v1: «shares — м’які, quotas — жорсткі». У v2, з уніфікованим обліком і відмінною поведінкою контролерів, відсутність жорсткого ліміту дозволила батчу домінувати під час сплесків, особливо якщо cpu контролер не був увімкнений там, де вони думали. «Оптимізація» перемістила вузьке місце з видимого тротлінгу в невидиме чергування.

Виправлення було прозаїчним: відновити явні cpu.max для батчу, послідовно увімкнути cpu контролер у правильному піддереві та залишити shares для справді співпрацюючих сервісів. Також додали крок перевірки в CI, який інспектує файли cgroup після деплою. «Довіряй, але перевіряй» — кліше, яке постійно залишається правдою.

Міні-історія 3: Нудна, але правильна практика, що врятувала день

Сервіс у фінансовій сфері працював на невеликому кластері, який завжди здавався надмірно забезпеченим. Хтось запитав, чому платять за проста.

У них була практика: при кожній зміні образу вузла запускати короткий конформанс-скрипт. Не величезний набір тестів — дюжина перевірок: режим cgroup, драйвер Docker, наявність контролерів і один контейнер, який ставить ліміти пам’яті й CPU і доводить їх, читаючи memory.max і cpu.max. Це займало менше двох хвилин.

Одного дня в базовий образ прослизнув новий образ з увімкненими уніфікованими cgroups, але зі старим runtime-пакетом через pinned репозиторій. Docker і контейнери запускались, але ліміти пам’яті були нестабільні і іноді падали з invalid argument. Їхній конформанс-скрипт зловив це до продакшна.

Виправлення було нудним, але ефективним: розблокувати pinned пакети, оновити бандл runtime, перезапустити конформанс-перевірки і перекатити. Ніякого інциденту. Ніяких листів керівництву. Просто тихе задоволення від того, що були праві заздалегідь.

Типові помилки: симптом → корінна причина → виправлення

Цей розділ навмисно різкий. Ось шаблони, які я постійно бачу в полі.

1) «Docker стартує, але обмеження пам’яті не працюють»

Симптом: docker run --memory проходить; контейнер використовує більше ліміту; docker stats показує необмежено.

Корінна причина: Docker використовує драйвер cgroupfs на systemd+v2 хості, розміщуючи контейнери в піддереві без +memory у cgroup.subtree_control, або застарілий runtime неправильно відображає v2 ліміти.

Виправлення: Перейдіть на systemd cgroup драйвер, підтвердьте Delegate=yes, перевірте memory.max у шляху scope контейнера.

2) «OCI runtime create failed: permission denied» під /sys/fs/cgroup

Симптом: Контейнери падають на створенні з помилками запису в sysfs.

Корінна причина: Відсутнє делегування для сервісу Docker, непривілейований користувач не має делегованих контролерів, або конфлікти SELinux/AppArmor, що проявляються як помилки запису.

Виправлення: Забезпечте Delegate=yes у unit systemd, використовуйте systemd драйвер, перевірте вимоги rootless, перевірте логи аудиту, якщо MAC увімкнено.

3) «No such file or directory» для cgroup файлів, які мають існувати

Симптом: Помилка посилається на відсутність файлів типу cpu.max або memory.max.

Корінна причина: Контролер не увімкнений для цього піддерева. У v2 наявність контролера в cgroup.controllers не те саме, що його увімкнення для дітей.

Виправлення: Увімкніть контролери на правильному батьківському рівні (конфігурація systemd slice або правильне розміщення піддерева). Перевірте cgroup.subtree_control.

4) «CPU квоту ігнорують»

Симптом: --cpus встановлено, але контейнер використовує повні ядра.

Корінна причина: cpu контролер не увімкнений, або контейнер опинився поза керованим піддеревом через невідповідність драйвера.

Виправлення: Перевірте cpu.max у cgroup контейнера. Якщо там max, виправте увімкнення контролера і узгодження драйверів.

5) «Падає тільки на деяких вузлах»

Симптом: Ідентичний деплой поводиться по-різному на різних вузлах.

Корінна причина: Флот змішаний v1/v2, або змішані Docker cgroup драйвери, або різні версії runtime.

Виправлення: Стандартизуйте. Оберіть режим cgroup і застосуйте через побудову образу + boot прапори + конфіг менеджмент. Потім перевірте конформанс-скриптом.

6) «Ми примусово включили cgroups v1, щоб виправити, і забули»

Симптом: Нові інструменти очікують v2; агент безпеки очікує v2; у вас тепер дуель очікувань.

Корінна причина: Тактичний відкат став постійною архітектурою.

Виправлення: Задокументуйте це як технічний борг з власником і датою. Міграція планомірно, пул вузлів за пулом.

Перевірочні списки / покроковий план

Чекліст 1: Прийняття нового образу вузла (10 хвилин, економить години)

  1. Перевірте режим cgroup: stat -fc %T /sys/fs/cgroup має збігатися з вашим стандартом флоту.
  2. Перевірте systemd ієрархію: systemd-analyze --version показує default-hierarchy=unified (якщо стандарт v2).
  3. Перевірте, що Docker бачить v2: docker info показує Cgroup Version: 2.
  4. Перевірте драйвер Docker: Cgroup Driver: systemd (рекомендовано для v2+systemd).
  5. Перевірте делегування docker.service: systemctl show docker --property=Delegate — має бути yes.
  6. Запустіть тестовий контейнер з CPU і memory лімітами і прочитайте memory.max і cpu.max в його scope cgroup.
  7. Перевірте journald на попередження: journalctl -u docker -b.

Чекліст 2: План виправлення, коли Docker ламається одразу після оновлення дистрибутива

  1. Підтвердьте реальний режим cgroup (Завдання 1–3). Не покладайтесь на нотатки релізу.
  2. Перевірте драйвер/версію Docker (Завдання 5–6). Оновіть, якщо застаріло.
  3. Перевірте делегування і увімкнення контролерів (Завдання 7–8, 14).
  4. Відтворіть мінімальним контейнером (Завдання 9). Не дебагуйте ваш 4GB JVM поки що.
  5. Підтвердьте, що ліміт записаний (Завдання 10–12). Якщо його немає у файлі — його немає.
  6. Лише потім чіпайте налаштування додатка.

Чекліст 3: План стандартизації для змішаного cgroup флоту

  1. Виберіть ціль: v2 unified + Docker systemd драйвер (найпоширеніше), або v1 legacy (тимчасово).
  2. Визначте перевірку відповідності (чекліст прийняття вище) і запустіть її для кожного пулу вузлів.
  3. Оновіть Docker/containerd/runc як бандл у кожному пулі.
  4. Перемикайте режим cgroup через boot прапори тільки на межах пулів (уникайте змішаних режимів у пулі).
  5. Ролл із канарками, перевіряйте, читаючи /sys/fs/cgroup і файли scope контейнера.
  6. Документуйте рішення і впровадьте його в пайплайни образів, щоб уникнути дрейфу.

FAQ

1) Як дізнатися, чи я на cgroups v2 без читання блогу?

Запустіть stat -fc %T /sys/fs/cgroup. Якщо воно виведе cgroup2fs, ви на v2 unified. Якщо бачите багато v1-монтів контролерів — ви на v1/гібриді.

2) Чи слід використовувати Docker cgroupfs драйвер на systemd-хості?

Уникайте цього, якщо немає конкретного обмеження. На systemd + cgroups v2 драйвер systemd — стабільний вибір, бо узгоджує делегування, slices/scopes і увімкнення контролерів.

3) Чому docker stats показує неправильні ліміти на v2?

Зазвичай це невідповідність runtime (старі Docker/containerd/runc), або Docker читає дані cgroup з шляху, що не відповідає місцю фактичного розміщення контейнерів. Перевірте, читаючи memory.max напряму в scope cgroup контейнера.

4) Моя помилка каже «no such file or directory» для memory.max. Але v2 увімкнено. Чому?

Бо v2 вимагає, щоб контролери були увімкнені для піддерева. Файл не існуватиме, якщо memory контролер не увімкнений у батьківському cgroup.subtree_control.

5) Чи прийнятно примусово перевести на cgroups v1?

Як короткостроковий вихід — так. Як довготермінова стратегія — це ризик. Ви дедалі частіше натрапите на інструменти й дистрибутиви, що припускають v2. Якщо ви примусово включаєте v1, стандартизуйте це і плануйте міграцію.

6) Чи потрібно змінювати прапори ресурсів контейнера для v2?

Зазвичай ні; прапори Docker залишаються тими самими. Змінюється те, чи ці прапори транслюються у правильні v2-файли і чи ядро дозволяє їх у цьому піддереві.

7) Який єдиний найкорисніший файл дивитися при налагодженні v2 проблем з ресурсами?

Шлях cgroup контейнера з /proc/<pid>/cgroup, а потім читайте релевантні файли: memory.max, memory.current, cpu.max, pids.max. Якщо значення немає — обмеження немає.

8) Rootless Docker: чи запускати його в продакшн на cgroups v2?

Це може бути прийнятно для певних випадків, але не вважайте це легким рішенням. Потрібна коректна делегація systemd user, і деякі контролі відрізняються від rootful режиму. Тестуйте ті саме ліміти, на які ви розраховуєте.

9) Kubernetes-аспект: що якщо kubelet і Docker використовують різні драйвери cgroup?

Невідповідність — це баг надійності. Процеси опиняються в несподіваних піддеревах, облік дивний, і обмеження можуть не застосовуватись. Узгодьте їх (systemd/systemd на v2 — поширена пара).

10) А IO-обмеження — чому моє blkio налаштування перестало працювати?

Бо blkio — термінологія v1. У v2 дивіться на io.* контролі і підтверджуйте, що ядро та пристрій підтримують політику. Також перевірте, що io контролер увімкнений для піддерева.

Висновок: практичні наступні кроки

cgroups v2 — це не «Docker зламався». Це ядро, яке наполягає на чистішому контракті. Біль виникає від невідповідних очікувань: драйвери, делегування і увімкнення контролерів.

Зробіть наступне:

  1. Стандартизуйте флот: оберіть cgroups v2 unified (переважно) або v1 legacy (тимчасово) і застосуйте це на завантаженні.
  2. На v2 systemd-хостах встановіть Docker у systemd cgroup драйвер і підтвердіть Delegate=yes.
  3. Створіть невеликий конформанс-скрипт, який запускає тестовий контейнер і доводить ліміти, читаючи файли cgroup напряму.
  4. Оновлюйте Docker/containerd/runc як набір. Старі runtime — джерело «працює іноді».

Якщо взяти лише одне: коли контейнери «ігнорують» ліміти, не сперечайтеся з прапорами docker run. Читайте файли cgroup. Ядро — джерело істини, і воно не приймає вибачень.

← Попередня
Глибина черги ZFS: чому деякі SSD літають на ext4 і задухають на ZFS
Наступна →
ZFS RAIDZ3: Коли трійний паритет вартий дисків

Залишити коментар