docker stats и top
После logs, exec, inspect остаётся ещё один важный класс debug-команд — про ресурсы: сколько контейнер ест CPU, RAM, сети, дисковых операций. Это docker stats и docker top. Они отвечают на вопросы «почему хост тормозит» и «какой процесс внутри контейнера съел всё».
docker stats
Базовый вызов:
docker stats
Откроется realtime-таблица:
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
5fa3d8e9b1c0 pg 0.05% 34.2MiB / 7.7GiB 0.43% 12.3MB / 5.2MB 0B / 8.2MB 12
ad34e2f5c6d8 minio 0.12% 158MiB / 7.7GiB 2.00% 2.4MB / 1.1MB 0B / 234kB 22
7f3e1a2b3c4d app 5.67% 412MiB / 512MiB 80.5% 45MB / 23MB 0B / 0B 8
Обновляется раз в секунду. Ctrl+C — выход.
Колонки:
- CPU % — процент использования CPU. Важно: 100% = одно ядро. Если у тебя 8 ядер и контейнер показывает 400% — он жрёт 4 ядра.
- MEM USAGE / LIMIT — текущая память / лимит. Лимит =
--memoryили дефолт «вся RAM хоста». - MEM % — процент от лимита. 80.5% — близко к OOM, время бить тревогу.
- NET I/O — байты RX / TX по сети (с момента старта контейнера, не дельта).
- BLOCK I/O — байты read / write на блочные устройства (диск).
- PIDS — количество процессов/тредов внутри.
Один shot, не realtime:
docker stats --no-stream
Полезно в скриптах — выдаст текущий snapshot и сразу вернётся.
CPU % > 100% и почему это нормально
docker stats показывает CPU% по той же логике, что и top на Linux:
- 100% = одно ядро полностью загружено.
- 200% = два ядра.
- 800% на 8-core хосте = вся CPU.
Это не баг, это nginx/Postgres/etc используют столько ядер, сколько могут (потоки разлетаются по доступным CPU). Если хочешь ограничить — --cpus при запуске:
docker run -d --cpus=2 --name app my-image
# CPU% будет максимум 200%
--cpus=2 означает «контейнер получает 2 ядра эквивалентно». Это cgroup CPU quota, не affinity (не привязывает к конкретным ядрам).
Кастомизация формата
docker stats поддерживает --format с Go-шаблоном — полезно для скриптов:
docker stats --no-stream --format "{{.Name}}: CPU {{.CPUPerc}}, MEM {{.MemPerc}}"
# pg: CPU 0.05%, MEM 0.43%
# minio: CPU 0.12%, MEM 2.00%
# app: CPU 5.67%, MEM 80.50%
Все доступные поля: {{.Container}}, {{.Name}}, {{.ID}}, {{.CPUPerc}}, {{.MemUsage}}, {{.MemPerc}}, {{.NetIO}}, {{.BlockIO}}, {{.PIDs}}.
Для мониторинга — пихаешь в файл, парсишь в Python, шлёшь в Prometheus.
docker top — процессы внутри
Если хочется увидеть что именно работает внутри контейнера — docker top:
docker top pg
Вывод:
USER PID PPID C STIME TTY TIME CMD
postgres 12345 12300 0 09:42 ? 00:00:01 postgres
postgres 12356 12345 0 09:42 ? 00:00:00 postgres: checkpointer
postgres 12357 12345 0 09:42 ? 00:00:00 postgres: background writer
postgres 12358 12345 0 09:42 ? 00:00:00 postgres: walwriter
postgres 12359 12345 0 09:42 ? 00:00:00 postgres: autovacuum launcher
postgres 12360 12345 0 09:42 ? 00:00:00 postgres: logical replication launcher
Важно: PID здесь — это PID на хосте, не PID внутри контейнера. Внутри контейнера postgres имеет PID 1, но на хосте — 12345 (зависит от того, какой process IDs выдал ядро).
docker top под капотом запускает ps -ef для PID контейнера на хосте через ps --pid или просмотр /proc. Альтернатива — ps aux | grep postgres на хосте, но docker top фильтрует только процессы этого контейнера.
В отличие от docker exec ps, docker top работает даже если внутри контейнера нет ps (distroless, scratch). Запускается на хосте.
docker top pg aux
# С дополнительными опциями ps (USER, %CPU, %MEM, STAT, etc.)
docker stats vs docker top
docker stats | docker top | |
|---|---|---|
| Что показывает | агрегированные метрики (CPU, MEM, IO) | список процессов |
| Realtime | да (обновляется каждую секунду) | snapshot |
| Уровень | контейнер целиком | процессы внутри |
| Нужен shell внутри? | нет | нет (запускается на хосте) |
| Работает с distroless? | да | да |
Они дополняющие. stats отвечает «что-то жрёт CPU», top — «вот этот конкретный процесс».
Как поймать OOM
Когда контейнер падает по OOM (out of memory), это происходит так:
- Процесс внутри запросил больше памяти, чем доступно (лимит контейнера или RAM хоста).
- Ядро (или Docker daemon) шлёт SIGKILL.
- Процесс умирает с кодом 137 (= 128 + 9).
- Если PID 1 умер — контейнер останавливается.
- Если restart policy = always/on-failure — контейнер перезапускается, цикл повторяется.
Признаки в docker stats: MEM% растёт к 100%, потом контейнер исчезает из списка (или скачет с 100% обратно к 0% при рестарте).
В docker inspect — .State.OOMKilled = true, .State.ExitCode = 137.
В dmesg (на Linux-хосте, с sudo):
Out of memory: Killed process 12345 (python) total-vm:1024MB, anon-rss:768MB, file-rss:0KB, shmem-rss:0KB, UID:1000 pgtables:1556kB oom_score_adj:0
Это и есть точный момент OOM. Дата, PID, имя, сколько памяти жрал.
Без --memory лимита OOM Killer убьёт любой процесс на хосте — может быть Postgres из соседнего контейнера, или sshd. Хост окажется в недоступном состоянии. Поэтому всегда ставь --memory лимит на контейнеры с непредсказуемой памятью (Python ETL, Java apps).
Реальные кейсы
«Контейнер тормозит, но я не знаю почему»
docker stats --no-stream
Видим:
NAME CPU % MEM USAGE / LIMIT
app 180% 2.1GiB / 4GiB
CPU 180% при 8-core хосте — это 2 ядра целиком, не катастрофа. Но MEM 2.1GiB, и если это Python ETL с pandas — возможно, грузит весь датасет в RAM. Решение: смотреть код или включить профайлер.
«Хост тормозит, я подозреваю Docker»
docker stats --no-stream | sort -k 4 -h -r | head
Топ-5 контейнеров по CPU. Сразу видно виновника.
«Контейнер ничего не делает, но висит»
docker top app
Видим внутри процесс с CMD sleep infinity. Это значит, что приложение упало, но контейнер с restart policy запустил entrypoint, который вместо приложения держит контейнер живым (плохой паттерн в каких-нибудь debug-Dockerfile). Лезем в docker logs, смотрим, почему main process не стартанул.
Альтернативные инструменты
docker stats — realtime в терминале. Для долгого мониторинга есть лучше:
- ctop (
brew install ctop/ GitHubbcicen/ctop) — TUI на базе stats, удобнее отображает данные. - cAdvisor (Google) — экспортирует метрики контейнеров в Prometheus. Стандарт для production-мониторинга.
- Portainer — Web UI для Docker, показывает то же самое в браузере.
- OrbStack / Docker Desktop — встроенный мониторинг в Mac-приложении.
Для команды на пять серверов — ставь cAdvisor + Prometheus + Grafana. Для одного dev-ноута — docker stats или ctop.
Попробуй сам
- Запусти Postgres, открой
docker statsв одном терминале. В другом —docker exec pg pgbench -i -U postgres(или просто N коннектов). Посмотри, как меняется CPU/MEM/IO. - Запусти
docker top pgи посмотри, какие процессы крутятся внутри (postgres + куча worker-процессов). - Запусти контейнер с искусственно ограниченной памятью и проверь OOM:
Контейнер должен умереть.docker run --rm --memory 64m python:3.13-slim python -c "x = ' ' * 100_000_000; print(len(x))"docker ps -a-> exit 137. Если этого не произошло (например, на macOS Docker Desktop), попробуй увеличить размер строки. - Сравни
docker top pg(показывает PID на хосте) сdocker exec pg ps aux(показывает PID внутри namespaces). Разница в PID — это user namespace в действии. - Поставь
ctop(если естьbrew), открой — увидишь то жеdocker statsв красивом TUI.