← До програми курсу

Модуль 7 / 18

Ресурси: cgroups, CPU, пам’ять, PIDs і OOM

DOCKER

Зміст
01

1. Цілі та модель ресурсів

Після модуля ви зможете перекласти Docker resource flags у налаштування cgroup v2, відрізнити hard limit від відносної ваги, підтвердити CPU throttling, розпізнати container OOM та обмежити число kernel tasks.

  • cgroup вимірює та обмежує групу процесів
  • CPU quota задає стелю, weight — частку за конкуренції
  • memory.max обмежує memory cgroup
  • memory.high і reservation створюють тиск, але не гарантують обсяг
  • pids.max захищає від fork/thread exhaustion
  • docker stats є представленням, а не єдиним джерелом істини
02

2. Теорія: cgroup v2 та effective configuration

Docker створює cgroup контейнера й записує значення controllers. В unified cgroup v2 використовуються cpu.max, cpu.weight, memory.max, memory.current, memory.events, pids.max та pids.current. Точний host path залежить від cgroup driver і systemd hierarchy, тому надійніше починати з inspect та файлів, видимих усередині контейнера.

Limit стосується всіх процесів контейнера разом. Потоки також враховуються як kernel tasks, тому PIDS у docker stats може бути більшим за число рядків звичного process view.

03

3. Теорія: CPU quota, weight та affinity

--cpus задає hard ceiling через quota/period: 0.5 означає до половини одного CPU часу, а не закріплене ядро. Після кожного вичерпання quota cgroup throttled до наступного period; nr_throttled і throttled_usec показують ефект.

--cpu-shares перетворюється на відносний weight і впливає лише за конкуренції за CPU. Це не reservation і не стеля. --cpuset-cpus обмежує дозволені logical CPUs; його використовують з огляду на NUMA, topology та host workload, а не як універсальну оптимізацію.

04

4. Теорія: memory, reservation, swap та OOM

--memory задає hard limit. --memory-reservation є soft hint нижче hard limit і проявляється за host pressure. --memory-swap означає спільний budget memory плюс swap; значення, рівне memory, забороняє container swap, якщо kernel підтримує керування swap.

Коли reclaim неможливий, kernel може завершити процес у cgroup. Docker фіксує State.OOMKilled для container main process і зазвичай видно exit 137. Не можна вимикати OOM killer без hard memory limit: це переносить ризик із workload на весь host. Swap може відкласти OOM ціною непередбачуваної latency.

05

5. Теорія: PIDs, threads та ulimits

--pids-limit обмежує processes і threads усієї cgroup та захищає host від fork bomb. Після досягнення ліміту clone/fork повертає Resource temporarily unavailable; уже запущений PID 1 може лишитися живим, але healthcheck і docker exec теж можуть не створити новий процес.

RLIMIT_NPROC рахує процеси користувача, іноді разом між кількома контейнерами з одним UID, тому не замінює cgroup PIDs. Ulimits обмежують ресурси процесу: open files, core size, locked memory та інші, і мають проєктуватися разом із лімітами cgroup.

06

6. Теорія: метрики та інтерпретація

docker stats зручний для швидкої перевірки CPU %, memory usage/limit, block/network I/O та PIDS. На Linux CLI віднімає page cache від memory usage, тоді як Engine API та memory.current можуть показувати інше значення. Порівнюйте визначення метрик, а не лише числа.

Для capacity planning потрібні time series: CPU throttling, working set, memory events, PSI, OOM count та saturation. Одноразовий snapshot не показує burst, leak або кореляцію з latency. Limit без alert і load test лишається недоведеним припущенням.

07

7. Методичні вказівки

  • Спочатку виміряйте baseline застосунку без обмеження
  • Задавайте limit зі SLO та load profile, а не довільного відсотка host
  • Залишайте ресурси daemon, kernel і системним службам
  • Не запускайте stress без строгого cgroup limit
  • Підтверджуйте OOM через State.OOMKilled та events
  • Враховуйте threads у PIDS
  • Після docker update перевіряйте effective HostConfig і cgroup files
  • Не використовуйте unlimited swap як лікування memory leak
08

8. Лабораторна робота: CPU, memory та PIDs

Зафіксуйте можливості host. Навантаження обмежені й використовують лише Alpine.

terminal
docker info --format '{{json .}}' | jq '{CgroupDriver,CgroupVersion,NCPU,MemTotal,OperatingSystem,KernelVersion}'
stat -fc '%T' /sys/fs/cgroup
cat /proc/swaps
docker system df

Створіть CPU-bound контейнер зі стелею 0.25 CPU, прочитайте counters та змініть limit на льоту.

terminal
docker run --detach --name docker-m7-cpu --cpus 0.25 --cpu-shares 512 alpine:3.21 sh -c 'while :; do :; done'
sleep 3
docker inspect docker-m7-cpu --format 'nano_cpus={{.HostConfig.NanoCpus}} cpu_shares={{.HostConfig.CpuShares}} cpuset={{json .HostConfig.CpusetCpus}}'
docker stats docker-m7-cpu --no-stream --format '{{json .}}' | jq
docker exec docker-m7-cpu sh -c 'cat /sys/fs/cgroup/cpu.max; cat /sys/fs/cgroup/cpu.weight; cat /sys/fs/cgroup/cpu.stat'
docker update --cpus 0.50 docker-m7-cpu
sleep 2
docker exec docker-m7-cpu cat /sys/fs/cgroup/cpu.max

Створіть memory pressure з hard limit 32 MiB і без додаткового swap. Очікується OOMKill лише тестового контейнера.

terminal
docker run --detach --name docker-m7-memory --memory 32m --memory-swap 32m alpine:3.21 awk 'BEGIN { for (i=0;;i++) a[i]=sprintf("%0100000d",i) }'
docker wait docker-m7-memory || true
sleep 1
docker inspect docker-m7-memory --format 'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} memory={{.HostConfig.Memory}} swap={{.HostConfig.MemorySwap}}'
docker logs docker-m7-memory
docker events --since 10m --until 0s --filter container=docker-m7-memory
docker inspect docker-m7-memory --format '{{json .State}}' | jq

Обмежте kernel tasks, наблизьтеся до межі та порівняйте PIDS із configured limit та перевірте, чи може runtime створити docker exec process.

terminal
docker run --detach --name docker-m7-pids --pids-limit 16 alpine:3.21 sh -c 'for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14; do sleep 60 & done; wait'
sleep 2
docker stats docker-m7-pids --no-stream --format 'name={{.Name}} pids={{.PIDs}} memory={{.MemUsage}}'
docker inspect docker-m7-pids --format 'pids_limit={{.HostConfig.PidsLimit}} status={{.State.Status}}'
docker exec docker-m7-pids true || true
docker top docker-m7-pids -eo pid,ppid,user,stat,args

Нарешті дослідіть і змініть кілька обмежень запущеного CPU-контейнера.

terminal
docker inspect docker-m7-cpu --format '{{json .HostConfig}}' | jq '{NanoCpus,CpuShares,Memory,MemoryReservation,MemorySwap,PidsLimit,OomKillDisable}'
docker update --memory 64m --memory-swap 64m --pids-limit 64 docker-m7-cpu
docker inspect docker-m7-cpu --format '{{json .HostConfig}}' | jq '{NanoCpus,Memory,MemorySwap,PidsLimit}'
docker stats docker-m7-cpu --no-stream

Критерій: cpu.max відображає quota; throttling спостерігається; memory container має OOMKilled=true; PIDS не перевищує configured limit; live update підтверджено inspect.

09

9. Діагностика: exit 137 та деградація

Exit 137 означає SIGKILL, але не доводить OOM: таким самим буде docker kill --signal KILL або завершення stop timeout. Зберіть State, events, kernel evidence та resource timeline.

terminal
docker container ls --all --filter name=docker-m7
docker inspect docker-m7-memory --format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}} error={{json .State.Error}} finished={{.State.FinishedAt}}'
docker events --since 15m --until 0s --filter type=container | tail -80
dmesg --ctime 2>/dev/null | tail -80 || true
docker info --format '{{json .Warnings}}' | jq
  • OOMKilled=true — сильне evidence container OOM
  • oom/oom_kill у memory.events — лічильники cgroup v2
  • nr_throttled — факт CPU throttling, не обов’язково проблема
  • fork error — перевірити pids.current/pids.max та ulimits
  • висока latency за низького CPU — перевірити throttling, I/O та swap
  • daemon warnings — відсутність controller або unsupported configuration
10

10. Контроль, cleanup і шпаргалка

  • Чим CPU quota відрізняється від shares/weight?
  • Чому --cpus 0.5 не закріплює половину ядра?
  • Що означає memory-swap, рівний memory?
  • Чому exit 137 не доводить OOM?
  • Чому PIDS враховує threads?
  • Чим RLIMIT_NPROC відрізняється від pids.max?
  • Чому docker stats і memory.current відрізняються?
  • Які метрики підтверджують CPU throttling?
terminal
docker container rm --force docker-m7-cpu docker-m7-memory docker-m7-pids 2>/dev/null || true
docker container ls --all --filter name=docker-m7
docker system df

Модуль засвоєно, якщо ви пов’язуєте Docker flags із cgroup files, безпечно відтворюєте pressure та встановлюєте причину resource failure за кількома незалежними evidence.