Если ты уже проходил наши курсы по Docker и Kubernetes - тут мы стыкуем теорию ограничений ресурсов с практикой их измерения. Если не проходил - не страшно, начнем с самого начала. На 2026 год это все более чем актуально: cgroup v2 стал умолчанием почти везде, а в стек диагностики прочно вошел eBPF - про него тоже поговорим.
Контейнер - это не виртуалка, а процессы хоста
Главное, что нужно уложить в голове новичку: контейнер не изолированная машина. Это обычные процессы твоего хост-ядра, которым ядро навесило две вещи.
- namespaces - изоляция видимости. Процесс в контейнере не видит чужие процессы, у него своя сеть, свой PID 1, своя файловая система. Как будто его посадили в комнату без окон в чужой мир.
- cgroups (control groups) - ограничение ресурсов. Сколько CPU, памяти, IO разрешено этой группе процессов. Это счетчик и вентиль одновременно.
Современные системы (Ubuntu 22.04+, Debian 12, RHEL 9/10, Fedora, а также Astra Linux и RED OS на свежих ядрах) используют cgroup v2 - единую иерархию вместо зоопарка контроллеров v1. Проверить, что у тебя v2, просто:
Код: Выделить всё
$ stat -fc %T /sys/fs/cgroup
cgroup2fs

Где лежат лимиты: читаем cgroups limits руками
Вся правда о лимитах и реальном потреблении - в псевдо-файлах /sys/fs/cgroup. Это не файлы на диске, это интерфейс ядра. Читаешь файл - получаешь текущую цифру.
Зайди внутрь контейнера и посмотри память:
Код: Выделить всё
$ cat /sys/fs/cgroup/memory.max
536870912
$ cat /sys/fs/cgroup/memory.current
410927104
Теперь CPU:
Код: Выделить всё
$ cat /sys/fs/cgroup/cpu.max
50000 100000
CPU throttling: почему kubernetes тормозит при живом процессоре
Самый коварный сценарий и частая причина жалоб "kubernetes тормозит, а CPU свободен". Дело в throttling - удушении по квоте механизмом CFS bandwidth control в ядре. Если процесс попытался сжечь свою CPU-квоту быстрее, чем закончился период, ядро тормозит его до начала следующего периода. Процесс физически стоит и ждет, хотя на хосте ядра простаивают. Это и есть cpu throttling, что в docker, что в k8s - механика одна.
Почему это бьет именно по latency. Период 100 мс делится на ядра: при лимите 0.5 CPU за один период ты получаешь 50 мс CPU-времени. Многопоточное приложение (а это почти любой современный сервис - GC, пул потоков, воркеры) на старте запроса параллельно жжет на нескольких ядрах и выедает квоту за первые 20-30 мс реального времени. Дальше до конца периода оно стоит. Снаружи это выглядит как непредсказуемые лаги в хвосте (p99 latency), хотя средняя загрузка CPU низкая. Классическая ловушка.
Смотрим cpu.stat - это главный файл для диагностики тормозов:
Код: Выделить всё
$ cat /sys/fs/cgroup/cpu.stat
usage_usec 9823145000
user_usec 7200100000
system_usec 2623045000
nr_periods 845200
nr_throttled 612340
throttled_usec 488300000000
nr_bursts 0
burst_usec 0
- usage_usec - всего сожжено процессорного времени, мкс. user_usec и system_usec - разбивка на код приложения и системные вызовы ядра.
- nr_periods - сколько прошло периодов планирования (с включенным лимитом).
- nr_throttled - в скольких периодах группу душили за превышение квоты.
- throttled_usec - суммарно времени простояли в наказание, мкс.
- nr_bursts / burst_usec - на 2026 год в cpu.stat есть еще эти два поля от фичи CFS burst (ядро 5.14+). Показывают, сколько раз группа залезла в "накопленный" неиспользованный лимит прошлых периодов и сколько времени так сэкономила. Если burst включен (cpu.max.burst > 0), часть всплесков гасится без throttling.
eBPF: современный взгляд на throttling и off-CPU (2026)
cpu.stat говорит "тебя душили", но не говорит "что именно стояло и сколько мс ждал конкретный запрос". На 2026 год это закрывает eBPF - он стал основой стека диагностики и для многих задач вытеснил точечный perf и strace за счет низкого оверхеда и работы по всему ядру сразу.
- runqlat (из bcc или bpftrace) - гистограмма времени ожидания в очереди планировщика. Если процессу есть что считать, но он висит в run queue - это прямой признак, что его душат квотой или хост перегружен.
- cpudist - распределение длительностей on-CPU и off-CPU. Видно, что приложение не работает, а простаивает.
- bpftrace однострочниками вешается на трейспоинты планировщика и tracepoint:sched, ловит ровно нужный процесс по cgroup.
Код: Выделить всё
$ sudo runqlat -p 18342 5 1
$ sudo bpftrace -e 'tracepoint:sched:sched_switch { @[comm] = count(); }'
docker stats, kubectl top и почему их мало
Снаружи, с хоста, картина честная, потому что инструменты читают именно cgroup. Базовый обзор по контейнерам:
Код: Выделить всё
$ docker stats --no-stream
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O PIDS
3f9a1c2b7e88 api 48.30% 392MiB / 512MiB 76.56% 1.2GB/... 24
Код: Выделить всё
$ kubectl top pod api-7d9f8 --containers
POD NAME CPU(cores) MEMORY(bytes)
api-7d9f8 api 480m 392Mi
OOM по лимиту, рестарты подов и Exit Code 137
Когда memory.current упирается в memory.max и ядру не удается отбить память реклеймом - срабатывает OOM killer внутри этой cgroup. Он убивает процесс SIGKILL. Контейнер умирает, под перезапускается. Факт OOM виден в memory.events:
Код: Выделить всё
$ cat /sys/fs/cgroup/memory.events
low 0
high 0
max 1843
oom 12
oom_kill 4
Код: Выделить всё
$ kubectl describe pod api-7d9f8
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
strace и perf к процессу в контейнере
Внутри контейнера strace и perf часто недоступны (нет пакета, нет capability, ridonly rootfs). Правильный путь - цепляться с хоста по реальному PID. Контейнер ведь это процессы хоста, помнишь? Находим PID:
Код: Выделить всё
$ docker inspect -f '{{.State.Pid}}' api
18342
$ sudo strace -f -p 18342
$ sudo perf top -p 18342
Логи: docker logs, kubectl logs против journald
docker logs api и kubectl logs api-7d9f8 показывают то, что приложение пишет в stdout/stderr - это поток самого контейнера. А journalctl на хосте - это логи systemd-юнитов хоста (включая сам демон dockerd/containerd и kubelet). Не путай: краш приложения ищи в docker/kubectl logs, а вот почему рантайм не смог запустить контейнер или прибил его - в journalctl -u containerd (или -u kubelet) на хосте. Сообщение об OOM от ядра тоже улетает в journald хоста: ищи строки с "oom-kill" и "Killed process" (например journalctl -k | grep -i oom). И помни: после рестарта пода kubectl logs покажет только новый запуск - чтобы увидеть логи упавшего контейнера, нужен флаг --previous.
Грабли и заблуждения
- "Поды тормозят, надо дать им больше CPU limit" - часто наоборот. Жесткий limit и есть причина throttling. На 2026 год общая практика для latency-критичных сервисов (API-гейтвеи, очереди): request ставить по реальному steady-state потреблению, а CPU limit убирать совсем или ставить с запасом 20-50% над request. Limit нужен прежде всего как защита соседей от "взбесившегося" пода, а не как цель.
- "top показал 90% памяти, надо тревожиться" - top внутри показал память хоста, твой лимит он не знает. Тревожиться по memory.current/memory.max.
- "OOM - значит мало памяти на ноде" - нет, OOM по cgroup-лимиту бьет даже на ноде с горой свободной памяти. Это лимит контейнера, а не нехватка на хосте.
- Сравнивать накопленный nr_throttled между разными контейнерами бессмысленно - у них разный аптайм. Считай дельту за интервал.
- Запусти контейнер с лимитами: docker run -d --name lab --memory=256m --cpus=0.5 nginx
- Зайди внутрь (docker exec -it lab bash или sh), запусти top и запиши, сколько памяти и ядер он видит. Сравни с реальностью.
- Прочитай cat /sys/fs/cgroup/memory.max и cat /sys/fs/cgroup/cpu.max - убедись, что там твои 256m (268435456) и 0.5 CPU (50000 100000).
- С хоста запусти docker stats --no-stream lab и сверь MEM с тем, что наврал top.
- Найди PID: docker inspect -f '{{.State.Pid}}' lab, и глянь sudo cat /proc/PID/cgroup - увидишь путь cgroup этого процесса.
- Создай нагрузку: docker exec lab sh -c "yes > /dev/null &" пару раз и читай cpu.stat подряд - посмотри, как растет nr_throttled.
- Почему top внутри контейнера показывает память и ядра хоста, а не лимит, и какие файлы дают правду?
- Что означают nr_periods, nr_throttled, throttled_usec и nr_bursts в cpu.stat и как по ним понять, что под душат?
- Что такое Exit Code 137 в kubectl describe pod и где на хосте искать подтверждение OOM?
- Почему docker stats и kubectl top не помогут увидеть CPU throttling и какие два пути (метрика и eBPF) дают полную картину?
Контейнер - это процессы хоста в namespaces плюс лимиты cgroups, поэтому top внутри врет. Настоящие цифры лежат в /sys/fs/cgroup (на 2026 это v2): memory.max/memory.current и memory.events по памяти, cpu.max по квоте, cpu.stat по throttling. Тормоза в Kubernetes при свободном железе - это почти всегда CPU throttling по слишком низкому limit (смотри долю nr_throttled/nr_periods, порог 5%). Рестарты подов с Exit Code 137 - это OOM по memory.max (смотри memory.events oom_kill). Для глубины бери eBPF с хоста (runqlat, cpudist, bpftrace), а strace и perf цепляй по PID из docker inspect, а не изнутри.