← К программе курса

Модуль 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.