Представь типичную ночь: прод лёг, в чате паника, ты заходишь на хост и видишь, что контейнер с API то живой, то мёртвый, рестартится по кругу. Логи говорят только "Killed". Кто его убил? Сколько он жрал памяти перед смертью? Упирался ли в лимит CPU? Без метрик ты гадаешь на кофейной гуще. С метриками - открываешь дашборд и видишь: за 30 секунд до OOM рабочий набор памяти упёрся в 512 МБ лимита, ядро прислало SIGKILL. Диагноз за пять секунд вместо часа.
Мониторинг контейнеров - это не "красивые графики для отчёта". Это способность ответить на три вопроса: сколько ресурсов реально потребляет контейнер, упирается ли он в назначенные лимиты, и почему он умер. В этом уроке разберём путь от простого docker stats до полноценного стека Prometheus плюс Grafana плюс cAdvisor, поймём, откуда метрики берутся под капотом (спойлер - из cgroups), и подведём это к тому, как мониторинг устроен в Kubernetes.

docker stats - первое, что под рукой, и его потолок
Самый быстрый инструмент - встроенный. Команда docker stats показывает живые метрики всех запущенных контейнеров: CPU, память, сеть, дисковый ввод-вывод.
Код: Выделить всё
docker stats --no-stream
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
9f3a1b2c4d5e api 142.50% 438.2MiB / 512MiB 85.59% 1.2GB / 880MB 0B / 4.1MB 38
7a8b9c0d1e2f postgres 3.10% 210.5MiB / 2GiB 10.28% 44MB / 51MB 33MB / 1.2GB 27- CPU % - может быть больше 100%. Это нормально: 142.50% означает, что контейнер использует почти полтора ядра. docker stats считает процент относительно одного ядра, а не относительно всей машины. На 8-ядерном хосте потолок тут 800%.
- MEM USAGE / LIMIT - usage это рабочий набор (working set), а не сырой счётчик. Docker вычитает из общего использования кэш страниц, который ядро может выкинуть под давлением. Поэтому число тут заметно ниже, чем RSS из top. Если LIMIT показывает весь объём хоста - значит у контейнера лимит памяти не задан, и это уже сигнал.
- MEM % - usage делёный на limit. Вот ЭТО поле надо ловить: подбирается к 100% - жди OOM kill.
- NET I/O и BLOCK I/O - накопленные суммы с момента старта контейнера, а не скорость. Чтобы увидеть скорость, смотри как меняется число между обновлениями.
- PIDS - число процессов/потоков внутри. Резко растёт - возможна форк-бомба или утечка потоков.
- Нет истории. Это live-режим. Контейнер умер минуту назад - данные испарились вместе с ним. Расследовать инцидент постфактум нечем.
- Нет агрегации. Десять хостов - десять отдельных терминалов. Никакого общего вида на флот.
- Нет алертов. Никто не разбудит тебя ночью, ты должен сам пялиться в экран.
- Дорогой опрос. Под капотом stats дёргает API демона по каждому контейнеру, на сотнях контейнеров это ощутимая нагрузка.
Откуда метрики на самом деле: cgroups под капотом
Прежде чем ставить cAdvisor, надо понять, откуда вообще берутся цифры. Магии нет - всё лежит в псевдофайловой системе ядра. Контейнер это процесс, помещённый в cgroup (control group), и ядро ведёт по этой группе счётчики. На cgroup v2 (дефолт на современных дистрибутивах) они тут:
Код: Выделить всё
# найти cgroup контейнера и заглянуть в счётчики памяти
cat /sys/fs/cgroup/system.slice/docker-9f3a1b2c4d5e....scope/memory.current
459276288
cat /sys/fs/cgroup/.../memory.max
536870912
cat /sys/fs/cgroup/.../memory.stat | head
anon 312401920
file 98635776
inactive_file 71434240
...Счётчики CPU там же: cpu.stat содержит usage_usec (сколько процессорного времени съедено), а главное - nr_throttled и throttled_usec. Вот это золото для прода, к нему вернёмся в разделе про троттлинг. Любой инструмент мониторинга контейнеров - cAdvisor, node-exporter, сам Docker - это всего лишь сборщик и переводчик этих файлов в формат Prometheus.
cAdvisor и метрики Docker для Prometheus
cAdvisor (Container Advisor от Google) - это демон, который сам находит все контейнеры на хосте, читает их cgroups и отдаёт метрики в формате Prometheus на эндпоинте /metrics. Это де-факто стандарт для per-container метрик. Запускают его самого в контейнере, монтируя нужные хостовые пути:
Код: Выделить всё
docker run -d --name cadvisor \
--restart unless-stopped \
-p 8080:8080 \
--volume /:/rootfs:ro \
--volume /var/run:/var/run:ro \
--volume /sys:/sys:ro \
--volume /var/lib/docker/:/var/lib/docker:ro \
--volume /dev/disk/:/dev/disk:ro \
--device /dev/kmsg \
gcr.io/cadvisor/cadvisor:v0.49.1Проверяем, что метрики льются:
Код: Выделить всё
curl -s localhost:8080/metrics | grep '^container_cpu_cfs_throttled_seconds_total' | head -2
container_cpu_cfs_throttled_seconds_total{id="/docker/9f3a...",image="myapp:1.4",name="api"} 8.41
container_cpu_cfs_throttled_seconds_total{id="/docker/7a8b...",image="postgres:16",name="postgres"} 0Параллельно есть второй источник - встроенный metrics endpoint самого демона dockerd. Он отдаёт метрики про сам движок (операции с образами, состояние демона, длительность build). Включается в /etc/docker/daemon.json:
Код: Выделить всё
{
"metrics-addr": "127.0.0.1:9323",
"experimental": true
}Стек docker prometheus grafana cAdvisor целиком
Соберём боевой стек одним compose. Prometheus тянет метрики (pull-модель: сам ходит и скрейпит), Grafana рисует, cAdvisor и node-exporter поставляют данные.
Код: Выделить всё
services:
prometheus:
image: prom/prometheus:v2.54.1
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prom_data:/prometheus
ports:
- "9090:9090"
restart: unless-stopped
cadvisor:
image: gcr.io/cadvisor/cadvisor:v0.49.1
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
devices:
- /dev/kmsg
restart: unless-stopped
node-exporter:
image: prom/node-exporter:v1.8.2
pid: host
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.sysfs=/host/sys'
restart: unless-stopped
grafana:
image: grafana/grafana:11.2.0
ports:
- "3000:3000"
volumes:
- grafana_data:/var/lib/grafana
restart: unless-stopped
volumes:
prom_data:
grafana_data:Код: Выделить всё
global:
scrape_interval: 15s
scrape_configs:
- job_name: cadvisor
static_configs:
- targets: ['cadvisor:8080']
- job_name: node
static_configs:
- targets: ['node-exporter:9100']Что реально мониторить: throttling, память vs лимит, OOM, рестарты
Графики ради графиков бесполезны. Вот четыре метрики, ради которых всё затевалось, с готовыми PromQL.
1. CPU throttling - тихий убийца latency. Это самая коварная штука. Контейнер с лимитом CPU не "падает", когда упирается в потолок - ядро просто притормаживает его планировщиком CFS. Снаружи это выглядит как загадочные задержки в ответах сервиса при низкой загрузке CPU. Ловится так:
Код: Выделить всё
rate(container_cpu_cfs_throttled_periods_total{name="api"}[5m])
/
rate(container_cpu_cfs_periods_total{name="api"}[5m])2. Память против лимита. Заранее видеть приближение к OOM:
Код: Выделить всё
container_memory_working_set_bytes{name="api"}
/
container_spec_memory_limit_bytes{name="api"} > 0.93. OOM kill. Факт убийства по памяти cAdvisor отдаёт счётчиком:
Код: Выделить всё
increase(container_oom_events_total{name="api"}[10m]) > 04. Рестарты. Контейнер, который циклически перезапускается (crash loop), - симптом номер один больного сервиса. Метрику счётчика рестартов удобнее брать с dockerd-эндпоинта или через cAdvisor по container_start_time_seconds (время старта скакнуло - был рестарт). Резкий рост рестартов = алерт немедленно.
Алерты. Графики ты не смотришь в 4 утра - тебя должен будить алерт. Правило для Alertmanager выглядит так:
Код: Выделить всё
groups:
- name: containers
rules:
- alert: ContainerHighCpuThrottling
expr: |
rate(container_cpu_cfs_throttled_periods_total[5m])
/ rate(container_cpu_cfs_periods_total[5m]) > 0.25
for: 10m
labels:
severity: warning
annotations:
summary: "Контейнер {{ $labels.name }} троттлится по CPU"
- alert: ContainerOOMKilled
expr: increase(container_oom_events_total[5m]) > 0
labels:
severity: critical
annotations:
summary: "Контейнер {{ $labels.name }} убит по OOM"Типичные грабли и антипаттерны
- Путать RSS из top с working set. Цифры не сойдутся - они меряют разное. Для решений про лимиты смотри working_set, его же видит OOM killer.
- CPU % больше 100 принимают за баг. Это не баг, это многоядерность. На 4 ядрах потолок 400%.
- Запускать контейнеры вообще без лимитов памяти, а потом удивляться, что один сервис сожрал хост и положил соседей. Без лимита нет порога OOM, и под раздачу попадёт случайный процесс по выбору ядра.
- Скрейпить cAdvisor раз в секунду. cAdvisor сам по себе прожорлив, агрессивный интервал плюс куча контейнеров - и мониторинг ест больше, чем приложение. 15s достаточно.
- Хранить метрики локально без диска под Prometheus. Перезапустил стек без volume - история сгорела. Volume под /prometheus обязателен, а для долгого хранения смотри в сторону VictoriaMetrics или Thanos.
- Игнорировать node-exporter. Контейнер может задыхаться из-за исчерпания хоста, а не своего лимита. Без метрик хоста этого не увидишь.
- Запусти cAdvisor одной командой docker run из урока, открой localhost:8080 - там есть и встроенный веб-интерфейс с живыми графиками.
- Подними нагруженный контейнер с жёстким лимитом: docker run -d --name load --cpus=0.5 --memory=128m progrium/stress --cpu 2. Он намеренно упрётся в лимиты.
- В соседнем терминале запусти docker stats - посмотри, как CPU % застывает у потолка ~50%, а MEM % растёт.
- Открой curl -s localhost:8080/metrics | grep throttled и убедись, что container_cpu_cfs_throttled_periods_total для load растёт - вот он, троттлинг вживую.
- Подними полный compose-стек, импортируй в Grafana дашборд 14282 и найди свой контейнер load на графиках. Сравни working set из Grafana с числом из docker stats - должны сойтись.
- Почему docker stats не годится для расследования инцидента, который случился час назад, и что решает эту проблему?
- Чем working set отличается от полного usage памяти, и почему именно по working set ядро решает, кого убить по OOM?
- Что такое CPU throttling, по какой метрике cAdvisor ты его ловишь и почему он опасен при низкой загрузке CPU?
- Зачем в стеке держать одновременно cAdvisor, node-exporter и dockerd metrics endpoint - что меряет каждый?
docker stats - быстрый стетоскоп "что сейчас", но без истории, агрегации и алертов. Настоящая наблюдаемость контейнеров строится на cgroups как источнике правды, cAdvisor как переводчике этих счётчиков в Prometheus, и связке Prometheus плюс Grafana для хранения, визуализации и алертов. Мониторь не графики ради графиков, а четыре вещи: CPU throttling, память против лимита, OOM kills и рестарты. Всё, что ты тут собрал руками, почти один в один переезжает в Kubernetes - там те же cAdvisor (встроенный в kubelet) и Prometheus, только метрики собираются со всего кластера через kube-state-metrics и Prometheus Operator. Логика та же, масштаб больше. В следующих уроках по оркестрации это пригодится сразу.