Контейнеры и cgroup v2: читаем ограничения памяти и CPU
Автор: Казачкин Даниил Михайлович · Обновлено
Linux namespaces изолируют представление системы, а cgroups учитывают и ограничивают ресурсы групп процессов. Один контейнер может содержать несколько процессов: ограничение…
Linux namespaces изолируют представление системы, а cgroups учитывают и ограничивают ресурсы групп процессов. Один контейнер может содержать несколько процессов: ограничение памяти относится к группе, а не отдельно к каждому процессу. В этом уроке мы прочитаем настройки cgroup v2 у собственного учебного контейнера, не меняя параметры Docker daemon или системных служб.
Убедимся в версии
docker info --format '{{.CgroupVersion}}'
stat -fc %T /sys/fs/cgroupДля выбранного сценария ожидается версия 2 и файловая система cgroup2fs на Linux-хосте. Если Docker работает внутри Desktop VM, первая команда описывает среду Engine, а вторая — машину, на которой запущен терминал: это могут быть разные системы. При cgroup v1 имена и расположение файлов отличаются; не создавайте отсутствующие control-файлы вручную. Устройство cgroup v2.
Запустим ограниченный процесс
docker run -d --name dk-cgroup-read --memory=128m --cpus=0.5 alpine:3.22 sleep 300
docker exec dk-cgroup-read cat /proc/self/cgroup
docker exec dk-cgroup-read cat /sys/fs/cgroup/memory.current
docker exec dk-cgroup-read cat /sys/fs/cgroup/memory.max
docker exec dk-cgroup-read cat /sys/fs/cgroup/cpu.maxmemory.max должен показать 134217728, то есть 128 × 1024 × 1024 байт. memory.current — текущее потребление группы; оно меняется и не обязано совпадать с RSS одного процесса. Для CPU типичный вывод — 50000 100000: бюджет 50 000 микросекунд на период 100 000 микросекунд, то есть половина CPU в среднем за период. Конкретный период проверяйте по файлу, а не предполагайте.
--cpus=0.5 не означает закрепление за «половиной ядра» и не запрещает кратковременное исполнение на разных CPU. Это ограничение времени CPU. Для выбора набора процессоров существует отдельный параметр cpuset. Docker resource constraints.
Читаем счётчики, а не угадываем причину
docker exec dk-cgroup-read cat /sys/fs/cgroup/memory.events
docker exec dk-cgroup-read cat /sys/fs/cgroup/cpu.stat
docker stats --no-stream dk-cgroup-readВ memory.events интересны max, oom и oom_kill; наличие поля не означает, что событие уже произошло. Ненулевой oom_kill свидетельствует о завершении процессов OOM killer в учитываемой области, но для объяснения конкретного инцидента важны время и прирост счётчика. memory.events может учитывать события дочерних cgroup; для локальных событий есть memory.events.local, если он доступен.
В cpu.stat поля usage_usec, user_usec и system_usec описывают накопленное время работы. nr_throttled и throttled_usec помогают увидеть ограничение CPU. Рост throttling сам по себе не доказывает, что запросы пользователей замедлились: сопоставьте его с задержками и рабочей нагрузкой. Число «max» в memory.max или cpu.max означает отсутствие данного локального лимита, но ограничение предка всё равно может действовать.
Маленькая нагрузка и сравнение
Снимите cpu.stat, выполните конечный цикл, затем снимите счётчики снова:
docker exec dk-cgroup-read sh -c 'i=0; while [ "$i" -lt 100000 ]; do i=$((i+1)); done'
docker exec dk-cgroup-read cat /sys/fs/cgroup/cpu.statОжидайте увеличение usage_usec. Точный прирост и наличие throttling зависят от машины и длительности задачи: нулевой nr_throttled после короткой нагрузки не опровергает настройку лимита. Намеренно исчерпывать память компьютера для доказательства существования OOM не требуется.
Почему exit code 137 недостаточно
137 часто соответствует завершению SIGKILL, но этот сигнал может возникнуть по разным причинам. Посмотрите docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' dk-cgroup-read, логи и события cgroup. При системном OOM данные внутри контейнера могут не объяснять всё; нужен журнал соответствующего хоста. Не объявляйте нехватку памяти только по числу 137 и не увеличивайте лимит вслепую.
Практика
Запишите лимит в байтах и отношение CPU quota/period. До и после конечного цикла сравните счётчики и сформулируйте наблюдение отдельно от гипотезы о производительности. Затем остановите и удалите только учебный контейнер: docker stop dk-cgroup-read, docker rm dk-cgroup-read. Сохраните измерения до удаления, потому что cgroup и её счётчики исчезают вместе с окружением. Переход от обычных процессов Linux к контейнерам теперь опирается на проверяемые файлы ядра, а не на показания одной панели.