Диагностика в контейнерах: cgroups, лимиты, Docker и Kubernetes

Рейтинг: 71.7% · 16 голосов
Подробный курс по диагностике и производительности Linux для админов и DevOps: с чего начать, методология USE, логи (journalctl, dmesg), процессы, CPU, память и OOM, диск и IO (iostat), сеть (ss, tcpdump), системные вызовы и strace, ltrace, perf, флеймграфы, ftrace, eBPF и bpftrace, lsof, мониторинг. Разбор каждого инструмента с чтением вывода.
Ответить
Аватара пользователя
Dmitry_SRE
Сообщения: 47
Зарегистрирован: 11 май 2026, 05:31

Диагностика в контейнерах: cgroups, лимиты, Docker и Kubernetes

Сообщение Dmitry_SRE »

Оглавление курса (47)
  1. С чего начать диагностику Linux: методология вместо паники
  2. Первые 60 секунд: экспресс-диагностика нагруженного сервера
  3. Методологии диагностики: USE, RED и здравый смысл
  4. Логи systemd через journalctl: где искать причину
  5. Где лежат логи Linux: /var/log, dmesg и rsyslog
  6. Источники правды: load average, /proc и /sys
  7. Процессы Linux: ps, pstree и состояния процессов
  8. Интерактивный мониторинг: top и htop
  9. Метрики процессов во времени: pidstat
  10. Приоритеты и ограничения: nice, ionice, cgroups
  11. Сигналы и зависшие процессы: kill, и что делать с D-state
  12. Загрузка CPU: user, system, iowait, steal и контекст-свитчи
  13. Диагностика CPU по ядрам: vmstat и mpstat
  14. Профилирование CPU: perf top и поиск пожирателя
  15. Частоты, троттлинг и NUMA: turbostat и numastat
  16. Память Linux: RSS, VSZ, page cache и миф о нехватке памяти
  17. Сколько памяти занято: free, /proc/meminfo и vmstat
  18. Поиск утечек и пожирателей памяти: smem, pmap, smaps
  19. Swap и подкачка: swapon, swappiness, когда своп - это боль
  20. OOM killer: кто и за что убил процесс
  21. Память ядра и slab: slabtop и куда уходит RAM
  22. Подсистема ввода-вывода: путь запроса от приложения до диска
  23. Диагностика диска: iostat и чтение await, %util
  24. Кто грузит диск: iotop и атрибуция io процессам
  25. Файловые системы: df, du, иноды и куда делось место
  26. Глубокая диагностика блочного слоя: blktrace и biolatency
  27. Кеш страниц и грязные данные: dirty pages, fsync, drop_caches
  28. Сетевой стек Linux: путь пакета и где возникают задержки
  29. Сокеты и соединения: ss и netstat
  30. Захват трафика: tcpdump для диагностики сети
  31. Задержки и потери в сети: ping, mtr, диагностика латентности
  32. Сеть как источник проблем: nftables, conntrack, дропы
  33. Что такое системный вызов и зачем его трассировать
  34. strace: трассировка системных вызовов на практике
  35. strace в бою: почему программа висит, падает или тормозит
  36. ltrace: трассировка вызовов библиотек
  37. Накладные расходы трассировки и безопасные альтернативы
  38. perf: универсальный профайлер Linux
  39. Флеймграфы: визуализация профиля производительности
  40. Трассировка ядра: ftrace и trace-cmd
  41. eBPF: революция в наблюдаемости Linux
  42. Инструменты eBPF на практике: BCC и bpftrace
  43. lsof: открытые файлы, дескрипторы, кто держит файл и порт
  44. Разбор кейса: приложение тормозит - пошаговая диагностика
  45. Непрерывный мониторинг: sar, node_exporter, Grafana, Zabbix
  46. Диагностика в контейнерах: cgroups, лимиты, Docker и Kubernetes (вы здесь)
  47. Карта инструментов и сквозной разбор инцидента производительности
Знакомая картина: приложение крутится в контейнере, оно тормозит или поды в Kubernetes без конца перезапускаются, а ты заходишь внутрь, смотришь top - и там все спокойно. Памяти вагон, CPU простаивает. И вот ты сидишь, не понимая, кто врет: мониторинг, top или сам контейнер. Спойлер: врет top. Этот урок про то, почему диагностика контейнера linux устроена иначе, чем диагностика обычного процесса на хосте, и как смотреть на реальные цифры, а не на красивую фантазию.

Если ты уже проходил наши курсы по Docker и Kubernetes - тут мы стыкуем теорию ограничений ресурсов с практикой их измерения. Если не проходил - не страшно, начнем с самого начала. На 2026 год это все более чем актуально: cgroup v2 стал умолчанием почти везде, а в стек диагностики прочно вошел eBPF - про него тоже поговорим.

Контейнер - это не виртуалка, а процессы хоста

Главное, что нужно уложить в голове новичку: контейнер не изолированная машина. Это обычные процессы твоего хост-ядра, которым ядро навесило две вещи.
  • namespaces - изоляция видимости. Процесс в контейнере не видит чужие процессы, у него своя сеть, свой PID 1, своя файловая система. Как будто его посадили в комнату без окон в чужой мир.
  • cgroups (control groups) - ограничение ресурсов. Сколько CPU, памяти, IO разрешено этой группе процессов. Это счетчик и вентиль одновременно.
И вот тут зарыта собака. Память и количество ядер процессор берет не из cgroup, а из ядра напрямую - то есть видит весь хост. А cgroup-лимит лежит отдельно, в /sys/fs/cgroup. top, free, nproc внутри контейнера читают глобальные цифры хоста и про твой лимит просто не знают. Поэтому top показывает 64 ГБ памяти и 32 ядра, хотя контейнеру выдали 512 МБ и пол-ядра. Запомни это как факт: top внутри контейнера врет, и врет систематически. Не потому что баг, а потому что он смотрит не туда. Кстати, рантаймы это понемногу лечат: свежие версии util-linux nproc и runtime с правильным LimitMEMLOCK уже умеют учитывать cpu-квоту, а языковые рантаймы (Go GOMAXPROCS, JVM с UseContainerSupport, начиная с Java 10+) сами читают cgroup. Но top, free, htop в общем случае все равно показывают хост - не доверяй им внутри контейнера.

Современные системы (Ubuntu 22.04+, Debian 12, RHEL 9/10, Fedora, а также Astra Linux и RED OS на свежих ядрах) используют cgroup v2 - единую иерархию вместо зоопарка контроллеров v1. Проверить, что у тебя v2, просто:

Код: Выделить всё

$ stat -fc %T /sys/fs/cgroup
cgroup2fs
Если видишь cgroup2fs - это v2 (он же unified hierarchy), все примеры ниже именно про него, это сегодня стандарт. Если tmpfs - значит еще v1 или гибрид, имена файлов там другие (memory.limit_in_bytes вместо memory.max и т.п.).

Изображение

Где лежат лимиты: читаем cgroups limits руками

Вся правда о лимитах и реальном потреблении - в псевдо-файлах /sys/fs/cgroup. Это не файлы на диске, это интерфейс ядра. Читаешь файл - получаешь текущую цифру.

Зайди внутрь контейнера и посмотри память:

Код: Выделить всё

$ cat /sys/fs/cgroup/memory.max
536870912
$ cat /sys/fs/cgroup/memory.current
410927104
memory.max - это твой потолок (тут 512 МБ). memory.current - сколько группа реально ест прямо сейчас (тут ~392 МБ). Если в memory.max написано "max" словом - значит жесткий лимит не задан, потолок это вся память хоста. Полезно рядом глянуть memory.stat - там разбивка: anon (анонимная память, куча приложения - вот ее не выкинуть под давлением), file (page cache, его ядро освободит первым), kernel (память ядра на эту группу). Когда current подползает к max, а почти весь объем - это anon, реклеймить нечего, и скоро прилетит OOM. Эти цифры и есть настоящая картина по памяти, а не то, что наврал free.

Теперь CPU:

Код: Выделить всё

$ cat /sys/fs/cgroup/cpu.max
50000 100000
Формат cpu.max - это "quota period" в микросекундах. Здесь: за каждый период в 100000 мкс (100 мс) группе разрешено сжечь 50000 мкс процессорного времени. То есть пол-ядра (0.5 CPU). Если вместо первого числа стоит "max" - квоты нет, бери сколько унесешь. Именно в этот лимит k8s превращает твой resources.limits.cpu. А вот cpu.weight (значения 1..10000, по умолчанию 100) - это уже не жесткий потолок, а доля при конкуренции, в нее k8s превращает request. Запомни различие: weight (request) режется только когда ядер реально не хватает на всех, max (limit) душит всегда при превышении квоты, даже если хост пустой.

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.
Как читать. Считаем долю: nr_throttled / nr_periods. Тут 612340 / 845200 = 0.72, то есть в 72% периодов процесс упирался в потолок и его тормозили. Это катастрофа. Норма - когда отношение близко к нулю или единицы процента. Порог тревоги на 2026 год по индустрии: если throttled-периодов стабильно больше 5% и приложение лагает в хвосте - диагноз есть, ты слишком зажал CPU limit. Важный нюанс: меряй именно долю периодов (nr_throttled/nr_periods), а не общий throttled_usec - последний хорош для capacity planning, но для latency обманчив. И смотри в динамике (дельта между двумя замерами через 30-60 секунд), а не накопленный с момента старта - иначе старый шум исказит картину.

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(); }'
Запускать это надо с хоста (eBPF цепляется к ядру, а контейнер ядро не свое). Связка простая: cpu.stat подтвердил факт throttling -> runqlat/cpudist показали, как это бьет по latency конкретного процесса.

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
MEM USAGE / LIMIT - вот это уже правда: реальное потребление против cgroup-лимита. CPU % считается относительно всех ядер хоста, держи в уме (контейнер с 800% CPU - это 8 ядер, а не "перегрузка"). В Kubernetes аналог (нужен установленный metrics-server):

Код: Выделить всё

$ kubectl top pod api-7d9f8 --containers
POD          NAME   CPU(cores)   MEMORY(bytes)
api-7d9f8    api    480m         392Mi
Важная ловушка для docker диагностики: ни docker stats, ни kubectl top НЕ показывают throttling. Они показывают потребление, но не показывают, душили тебя или нет. Под может есть всего 480m при лимите 500m и при этом дико тормозить из-за коротких всплесков, упирающихся в квоту. Поэтому при жалобах на скорость всегда лезь в cpu.stat и смотри nr_throttled - docker stats тут слепой. В нормальном проде это давно вынесено в метрику container_cpu_cfs_throttled_periods_total (cAdvisor отдает в Prometheus), и алерт строят именно на доле throttled-периодов.

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
Разбор: max - сколько раз потребление утыкалось в memory.max (аллокации блокировались/реклеймились). oom - сколько раз запускался OOM-разбор. oom_kill - сколько раз ядро реально прибивало процессы в этой группе. Не ноль - значит тебя оомкилило, и это не "сервер виноват", а контейнер уперся в свой memory.max. В k8s с cgroup v2 включен memory.oom.group=1: убивают сразу всю группу целиком, чтобы не остался полумертвый контейнер с прибитым воркером. В kubectl это выглядит так:

Код: Выделить всё

$ kubectl describe pod api-7d9f8
    Last State:     Terminated
      Reason:       OOMKilled
      Exit Code:    137
Exit Code 137 запомни намертво: это 128 + 9, где 9 это сигнал SIGKILL. Почти всегда это OOM по лимиту памяти. Тонкость, на которой спотыкаются: бывает OOMKilled и без превышения лимита самим процессом - если жесткий лимит мал, а приложение сделало большой single-аллок, или если node под давлением. Но в 9 из 10 случаев лечение одно из двух: либо поднять memory.limits, либо чинить утечку в приложении. Отличить помогает динамика memory.current: ровный рост до потолка - утечка, резкий скачок - разовый пик.

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
С хоста ты видишь процесс как родной, и strace покажет системные вызовы, а perf - где жжется CPU. На 2026 год вместо strace для постоянного трейсинга чаще берут eBPF-инструменты (bpftrace, bcc trace/funccount) - оверхед в разы меньше, а strace на нагруженном процессе сам по себе тормозит его остановками. Но для разовой "что эта зараза вызывает прямо сейчас" strace по-прежнему незаменим. Если инструменты нужны внутри контейнера - запускай его с --cap-add=SYS_PTRACE (для strace), а для perf нужен доступ к perf_event и обычно --privileged либо настройка perf_event_paranoid. С хоста проще и безопаснее. В k8s то же самое делают через ephemeral debug-контейнер: kubectl debug -it pod --image=... --target=container, он подсаживается в namespaces целевого контейнера.

Логи: 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, а не изнутри.
👍5 ❤️4 🔥1 😄 🤔
Аватара пользователя
SparkAdmin
Сообщения: 1
Зарегистрирован: 12 май 2026, 21:15

Re: Диагностика в контейнерах: cgroups, лимиты, Docker и Kubernetes

Сообщение SparkAdmin »

А подскажите по практике: nr_throttled / nr_periods у меня 0.15 при лимите CPU 500m. Если limit убрать совсем и оставить только request, это нормально или страшно за соседей по ноде?
👍 ❤️ 🔥 😄 🤔
Аватара пользователя
bash_main
Сообщения: 1
Зарегистрирован: 17 май 2026, 18:01

Re: Диагностика в контейнерах: cgroups, лимиты, Docker и Kubernetes

Сообщение bash_main »

Блин, вот про top внутри контейнера это прям про меня. Полгода смотрел в него и не понимал, чего поды рестартятся, а оно вон где - memory.events. Спасибо, забрал в закладки.
👍 ❤️1 🔥 😄 🤔1
Ответить
← Предыдущая глава
Непрерывный мониторинг: sar, node_exporter, Grafana, Zabbix
Следующая глава →
Карта инструментов и сквозной разбор инцидента производительности

Все главы курса «Диагностика и производительность Linux: логи, strace, perf и eBPF»

Поделиться темой: ✈ Telegram VK
Похожие запросы: strace почему программа висит и тормозитЛоги и диагностика macOSкак посмотреть процессы в linux и убить зависшийКонтейнеры и виртуализация на Macчто такое load average в linux и какое значение нормальноеТраблшутинг и обслуживание Mac

Вернуться в «Диагностика и производительность Linux: логи, strace, perf и eBPF»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость