Лимиты CPU и памяти в cgroup v2: как это работает на самом деле
Лимиты CPU и памяти в cgroup v2 работают по-разному: cpu.max в cgroup v2 задаёт квоту, memory.high вызывает reclaim, а memory.max может привести к OOM. Результат зависит от ядра, дерева systemd, swap, OOM-политики и состава cgroup.
Чем cpu.max отличается от cpu.weight?
cpu.max задаёт квоту CPU на период, и после её исчерпания группа ждёт следующего периода. cpu.weight делит время между соседями, которые одновременно конкурируют за процессор. Без конкуренции высокий вес задачу не ускоряет, тогда как квота всё равно остаётся абсолютной границей для группы.
Где cgroup v2 действительно применяет контроллер
Одного файла cpu.max в cgroup v2 мало, путь процесса проверяют командами:
stat -fc %T /sys/fs/cgroup
systemctl show -p ControlGroup app.service
systemd-cgls --unit app.service
Контроллер должен входить в cgroup.controllers родителя и cgroup.subtree_control. Для доменных контроллеров действует no internal process: у родителя с дочерними cgroup не должно быть процессов.
cpu.max, cpu.weight и конкуренция за время процессора
cpu.max хранит квоту и период в микросекундах. cpu.weight (1–10000, по умолчанию 100) распределяет время между активными соседями одной ветви. Affinity и лимит родителя сужают доступный CPU. memory.max в cgroup v2 не ограничивает процессор.
Как увидеть исчерпание CPU-квоты в cpu.stat
Нагрузку создают через stress-ng в изолированном unit с неизменным cpuset, меняя между ступенями только cpu.max. Показатели usage_usec, nr_periods, nr_throttled и throttled_usec из cpu.stat сопоставляют с throughput и p99. memory.high против memory.max тестируют отдельно, чтобы reclaim не исказил результат.
Как memory.high и memory.max ведут себя под давлением?
memory.high замедляет процессы через reclaim, причём потребление может временно оставаться выше порога. memory.max задаёт жёсткую верхнюю границу, и если память освободить не удаётся, начинается cgroup OOM. Разницу видно по high, max, oom и oom_kill в memory.events.local, а также по memory PSI.
memory.low, memory.high и memory.max без смешения ролей
memory.low защищает рабочий набор в пределах бюджета родителя, memory.high усиливает reclaim, memory.max ставит жёсткую границу. При росте resident set пишут memory.current, anon, file и пределы родителя. В unit лимиты systemd cgroup v2 сверяют с эффективными значениями.
Замедление до OOM как отдельный режим
Память наращивают ступенями, записывая high в memory.events.local, pgscan, PSI memory и latency. Reclaim может нарушить SLO до OOM. CPU weight в cgroup при этом не трогают. anon и file разделяют анонимную и файловую память.
Что происходит при достижении memory.max
События max, oom и oom_kill сверяют с журналом ядра. memory.oom.group=1 убивает все задачи группы разом, кроме процессов с oom_score_adj=-1000. Проверять это можно только на изолированном стенде.
Как проверить, что лимиты cgroup v2 действительно работают?
1. systemd-cgls и ControlGroup подтверждают путь процесса.
2. Файлы содержат нужные значения, а счётчики растут под нагрузкой.
3. Throughput и latency меняются одновременно с throttling, PSI или OOM.
В отчёт идут дерево cgroup, лимиты, swap, ряды cpu.stat и memory.events, PSI, журнал OOM и версия ядра.
memory.swap.max, latency и ложное ощущение запаса
Для каждого прогона указывают swap, memory.swap.max, swap in/out и p99. Swap отодвигает OOM killer ценой задержки, поэтому прогоны со swap и без него не смешивают. Отсутствие OOM не означает выполнения SLO.
Как перенести измеренные границы в unit и мониторинг
В systemd slices CPUQuota, CPUWeight, MemoryHigh и MemoryMax задают параметры cgroup. Мониторинг берёт PSI, throttled_usec, high, oom и oom_kill. После изменения unit или ядра запускают canary-тест.
Лимиты CPU и памяти в cgroup v2 задают вместе с сигналами срабатывания: throttling в cpu.stat, pressure в PSI и события OOM. Проверяют их в той же systemd-иерархии, где работает служба. Настройка годится, когда приложение выполняет SLO при включённых лимитах.