Модуль 7 / 18
Ресурсы: cgroups, CPU, память, PIDs и OOM
Содержание
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 является представлением, а не единственным источником истины
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.
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, а не как универсальную оптимизацию.
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.
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.
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 остаётся недоказанным предположением.
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
8. Лабораторная работа: CPU, memory и PIDs
Зафиксируйте возможности host. Нагрузки ограничены и используют только Alpine.
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 на лету.
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 только тестового контейнера.
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.
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-контейнера.
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.
9. Диагностика: exit 137 и деградация
Exit 137 означает SIGKILL, но не доказывает OOM: таким же будет docker kill --signal KILL или истечение stop timeout. Соберите State, events, kernel evidence и resource timeline.
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. Контроль, 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?
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.