Главная/Observability и диагностика/Урок

13.2. Inspect, events, stats

Цели

После этого материала вы сможете:

  • извлечь любое поле из docker inspect через --format и знать, где смотреть структуру;
  • наблюдать за происходящим в реальном времени через docker events с фильтрами;
  • объяснить каждую колонку docker stats и откуда берётся её значение;
  • объяснить, почему MEM USAGE не совпадает с тем, что показывает приложение;
  • сопоставить процесс в container с процессом на host;
  • собрать факты о состоянии системы одной командой вместо десяти.

Предварительные знания

Ключевые термины

ТерминОбъяснение
--formatШаблон Go для выборки полей
eventsПоток сообщений о происходящем в daemon
RSSРезидентная память процесса
page cacheКэш файлов в памяти, учитываемый cgroup
working setПамять за вычетом освобождаемого кэша
PID namespaceПричина различия PID внутри и снаружи

Теория

docker inspect и --format

Команда возвращает объект JSON на несколько сотен строк. Читать его целиком бессмысленно; нужен способ доставать конкретное.

bash
docker inspect КОНТЕЙНЕР --format '{{.State.Status}}'
docker inspect КОНТЕЙНЕР --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'
docker inspect КОНТЕЙНЕР --format '{{json .Config.Env}}'

Полезные конструкции шаблона:

КонструкцияНазначение
{{.Поле}}Значение поля
{{json .Поле}}Поле как JSON — для объектов и массивов
{{range .Массив}}...{{end}}Обход массива
{{index .Массив 0}}Элемент по индексу
{{.А}} {{.Б}}Несколько полей одной командой
{{println .Поле}}С переводом строки
{{if .Поле}}...{{end}}Условие

Наиболее востребованные поля:

Что нужноПуть
Состояние.State.Status
Код выхода.State.ExitCode
Признак OOM.State.OOMKilled
Здоровье.State.Health.Status
Последняя проверка.State.Health.Log
IP-адрес.NetworkSettings.Networks.ИМЯ.IPAddress
Смонтированное.Mounts
Лимит памяти.HostConfig.Memory
Путь к логу.LogPath
Число перезапусков.RestartCount

Практический приём: чтобы найти нужный путь, не изучая всю структуру, используйте docker inspect без формата и поиск по ключу — либо сразу --format '{{json .State}}', сузив область.

docker events

Поток сообщений обо всём, что происходит в daemon: создание, запуск, остановка, смена состояния здоровья, OOM, изменение сетей и томов.

bash
docker events --since 10m
docker events --filter 'event=die'
docker events --filter 'container=api' --filter 'event=health_status'
docker events --format '{{.Time}} {{.Type}} {{.Action}} {{.Actor.Attributes.name}}'

Ключевые события:

СобытиеКогда
createContainer создан
startЗапущен
dieГлавный процесс завершился
stop, killОстановлен по команде
oomПроцесс убит по нехватке памяти
health_status: healthyПроверка стала успешной
health_status: unhealthyПроверка перестала проходить
destroyContainer удалён

Главное свойство events, ради которого его применяют: он показывает последовательность и время. Когда сервис перезапускается по кругу, docker ps показывает текущее состояние, а events — что именно ему предшествовало.

Ограничение: события хранятся не вечно. --since работает в пределах буфера daemon; после перезапуска daemon история теряется.

docker stats: откуда цифры

КолонкаИсточникПодвох
CPU %Разница cpu.stat за интервалМожет превышать 100 % — это сумма по ядрам
MEM USAGEmemory.current cgroupВключает page cache
LIMITmemory.max cgroupБез лимита — вся память host
MEM %USAGE / LIMITЗавышена из-за кэша
NET I/OСчётчики интерфейсовСуммарно за всё время жизни
BLOCK I/Oio.stat cgroupТолько прямые обращения к диску
PIDSpids.currentПотоки считаются тоже

Главное расхождение — память. MEM USAGE берётся из cgroup и включает page cache: файлы, прочитанные приложением и оставшиеся в памяти. Приложение считает своей памятью только RSS.

text
memory.current  = anon (RSS) + page cache + kernel + ...
приложение видит = anon

Практически: Python-процесс, прочитавший гигабайтный файл, покажет в docker stats рост на гигабайт, хотя sys.getsizeof ничего не изменит.

Это не утечка. Page cache освобождается ядром при нехватке памяти.

Как получить сопоставимое число: memory.current минус освобождаемая часть кэша. Точный расчёт — в уроке 11.4; здесь достаточно знать, что разница ожидаема.

docker top и соответствие процессам host

bash
docker top КОНТЕЙНЕР
docker top КОНТЕЙНЕР -eo pid,ppid,user,%cpu,%mem,cmd

Команда показывает процессы container с точки зрения host: PID в ней — это PID на host, а не PID внутри container.

Внутри container тот же процесс имеет PID 1 (урок 2.2). Соответствие устанавливается через /proc/PID_НА_HOST/status, поле NSpid, где перечислены PID во всех вложенных namespace.

Это соответствие нужно для strace, gdb и чтения /proc — все они на host работают с PID host.


Внутренний механизм

Почему docker stats без аргументов медленный

Команда собирает статистику по каждому работающему container'у, и для расчёта процентов ей нужны два замера с интервалом. Отсюда задержка около секунды перед первым выводом.

bash
docker stats --no-stream КОНТЕЙНЕР

Флаг --no-stream даёт один снимок и завершается — то, что нужно для скриптов. Без него команда работает бесконечно и в конвейере зависает.

Формат docker events и парсинг

События выдаются потоком, по одному JSON на строку при --format '{{json .}}'. Это удобно для обработки, но требует помнить: поток не заканчивается. Скрипт, читающий docker events, должен либо ограничивать себя --until, либо завершаться сам.

bash
timeout 10 docker events --format '{{json .}}'
docker events --since 5m --until 1m --format '{{json .}}'

Второй вариант предпочтителен в скриптах: он читает историю и завершается.


Команды и примеры

Выборка полей одной командой

bash
mkdir -p /tmp/insp && cd /tmp/insp

docker run -d --name target \
    --memory 256m --cpus 0.5 \
    --health-cmd 'python -c "print(1)"' --health-interval 3s \
    -e APP_ENV=production \
    -p 18080:8000 \
    python:3.13-slim sleep 300 > /dev/null
sleep 8

echo "═══ отдельные поля ═══"
docker inspect target --format 'состояние:   {{.State.Status}}'
docker inspect target --format 'здоровье:    {{.State.Health.Status}}'
docker inspect target --format 'перезапуски: {{.RestartCount}}'
docker inspect target --format 'OOM:         {{.State.OOMKilled}}'
docker inspect target --format 'лимит памяти: {{.HostConfig.Memory}} байт'

echo "═══ несколько полей одной командой ═══"
docker inspect target --format \
'{{.Name}} | {{.State.Status}} | здоровье={{.State.Health.Status}} | память={{.HostConfig.Memory}} | перезапусков={{.RestartCount}}'

echo "═══ обход массива ═══"
docker inspect target --format '{{range .Config.Env}}  {{println .}}{{end}}' | head -5

echo "═══ вложенные структуры через json ═══"
docker inspect target --format '{{json .NetworkSettings.Ports}}' \
    | python3 -m json.tool | sed 's/^/  /'

echo "═══ условие в шаблоне ═══"
docker inspect target --format \
'{{if .State.Health}}проверка настроена: {{.State.Health.Status}}{{else}}проверки нет{{end}}'

Ожидаемый вывод:

text
═══ отдельные поля ═══
состояние:   running
здоровье:    healthy
перезапуски: 0
OOM:         false
лимит памяти: 268435456 байт
═══ несколько полей одной командой ═══
/target | running | здоровье=healthy | память=268435456 | перезапусков=0
═══ обход массива ═══
  APP_ENV=production
  PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
  LANG=C.UTF-8
  GPG_KEY=7169605F62C751356D054A26A821E680E5FA6305
  PYTHON_VERSION=3.13.6
═══ вложенные структуры через json ═══
  {
      "8000/tcp": [
          {
              "HostIp": "0.0.0.0",
              "HostPort": "18080"
          }
      ]
  }
═══ условие в шаблоне ═══
проверка настроена: healthy

Строка «несколько полей одной командой» — практический приём: пять фактов о container'е за один вызов вместо пяти.

Условие в конце нужно потому, что .State.Health отсутствует у container'ов без проверки, и обращение к .State.Health.Status вызовет ошибку шаблона.

История проверок здоровья

bash
cd /tmp/insp
echo "═══ последние проверки ═══"
docker inspect target --format '{{json .State.Health}}' \
    | python3 -c "
import json, sys
h = json.load(sys.stdin)
print(f\"  статус: {h['Status']}, неудач подряд: {h['FailingStreak']}\")
print(f\"  записей в журнале: {len(h['Log'])}\")
for entry in h['Log'][-3:]:
    start = entry['Start'][11:19]
    code = entry['ExitCode']
    out = entry['Output'].strip()[:40].replace('\n', ' ')
    print(f'    {start} код={code} вывод={out!r}')
"

echo "═══ почему это важно ═══"
cat <<'TXT'
  .State.Health.Log хранит последние 5 проверок с их выводом.
  Это единственное место, где виден ВЫВОД неудачной проверки —
  в docker logs его нет, потому что проверка выполняется
  не как процесс приложения.

  При unhealthy первым делом смотрят сюда:
    docker inspect КОНТЕЙНЕР --format '{{json .State.Health}}' | python3 -m json.tool
TXT

Ожидаемый вывод:

text
═══ последние проверки ═══
  статус: healthy, неудач подряд: 0
  записей в журнале: 3
    10:22:14 код=0 вывод='1'
    10:22:17 код=0 вывод='1'
    10:22:20 код=0 вывод='1'
═══ почему это важно ═══
  .State.Health.Log хранит последние 5 проверок с их выводом.
  Это единственное место, где виден ВЫВОД неудачной проверки —
  в docker logs его нет, потому что проверка выполняется
  не как процесс приложения.

  При unhealthy первым делом смотрят сюда:
    docker inspect КОНТЕЙНЕР --format '{{json .State.Health}}' | python3 -m json.tool

Это одна из самых полезных команд раздела: docker logs при неудачной проверке здоровья не показывает ничего о причине, потому что healthcheck запускается отдельным процессом.

Наблюдение через docker events

bash
cd /tmp/insp
echo "═══ события за последнюю минуту ═══"
docker events --since 2m --until 0s \
    --format '{{.Time}} {{.Type}}/{{.Action}} {{.Actor.Attributes.name}}' 2>/dev/null \
    | tail -8 | python3 -c "
import sys, datetime
for line in sys.stdin:
    parts = line.split(None, 2)
    if len(parts) < 3:
        continue
    ts = datetime.datetime.fromtimestamp(int(parts[0])).strftime('%H:%M:%S')
    print(f'  {ts} {parts[1]:<28} {parts[2].strip()}')
"

echo "═══ фильтр по типу события ═══"
docker events --since 5m --until 0s --filter 'event=health_status' \
    --format '{{.Action}} {{.Actor.Attributes.name}}' 2>/dev/null \
    | tail -4 | sed 's/^/  /' || echo "  событий health_status не было"

echo "═══ наблюдение в реальном времени ═══"
cat <<'TXT'
  Поток событий не заканчивается сам. В скрипте используйте
  либо --until, либо timeout:

    docker events --since 5m --until 1m --format '{{json .}}'
    timeout 10 docker events --filter 'event=die'

  Без этого команда в конвейере зависнет.
TXT

Ожидаемый вывод:

text
═══ события за последнюю минуту ═══
  10:22:06 container/create              target
  10:22:06 network/connect               bridge
  10:22:06 container/start               target
  10:22:11 container/health_status: healthy target
═══ фильтр по типу события ═══
  health_status: healthy target
═══ наблюдение в реальном времени ═══
  Поток событий не заканчивается сам. В скрипте используйте
  либо --until, либо timeout:

    docker events --since 5m --until 1m --format '{{json .}}'
    timeout 10 docker events --filter 'event=die'

  Без этого команда в конвейере зависнет.

Последовательность create → connect → start → health_status — это то, что нельзя увидеть в docker ps: там только итоговое состояние.

Почему docker stats показывает больше, чем приложение

bash
cd /tmp/insp
cat > memcheck.py <<'PY'
"""Показывает разницу между RSS процесса и учётом памяти в cgroup.

cgroup учитывает page cache: файлы, прочитанные процессом
и оставшиеся в памяти ядра. Приложение своей памятью их не считает.
"""
from __future__ import annotations

import json
import os
import sys
import time
from pathlib import Path


def rss_bytes() -> int:
    """Резидентная память процесса — то, что считает своим приложение."""
    for line in Path("/proc/self/status").read_text().splitlines():
        if line.startswith("VmRSS:"):
            return int(line.split()[1]) * 1024
    return 0


def cgroup_current() -> int:
    """memory.current — то, что показывает docker stats."""
    for path in ("/sys/fs/cgroup/memory.current",
                 "/sys/fs/cgroup/memory/memory.usage_in_bytes"):
        try:
            return int(Path(path).read_text().strip())
        except OSError:
            continue
    return 0


def cgroup_stat() -> dict[str, int]:
    out = {}
    try:
        for line in Path("/sys/fs/cgroup/memory.stat").read_text().splitlines():
            k, _, v = line.partition(" ")
            if v.strip().isdigit():
                out[k] = int(v)
    except OSError:
        pass
    return out


def mib(n: int) -> str:
    return f"{n / 1024 / 1024:6.1f} МиБ"


def snapshot(label: str) -> dict[str, object]:
    st = cgroup_stat()
    return {
        "этап": label,
        "rss": rss_bytes(),
        "cgroup_current": cgroup_current(),
        "anon": st.get("anon", 0),
        "file": st.get("file", 0),
    }


if __name__ == "__main__":
    stages = [snapshot("старт")]

    # 1. Выделяем память в куче — растёт и RSS, и cgroup
    data = bytearray(80 * 1024 * 1024)
    data[::4096] = b"\x01" * (len(data) // 4096)
    stages.append(snapshot("выделено 80 МиБ в куче"))

    # 2. Создаём и читаем файл — растёт ТОЛЬКО cgroup (page cache)
    path = "/tmp/bigfile.bin"
    with open(path, "wb") as f:
        f.write(os.urandom(4 * 1024 * 1024) * 20)
    with open(path, "rb") as f:
        while f.read(1024 * 1024):
            pass
    stages.append(snapshot("прочитан файл 80 МиБ"))

    del data
    stages.append(snapshot("куча освобождена"))
    os.unlink(path)

    print(f"  {'этап':<26} {'RSS':>12} {'cgroup':>12} {'anon':>12} {'file':>12}")
    print("  " + "─" * 78)
    for s in stages:
        print(f"  {s['этап']:<26} {mib(s['rss']):>12} {mib(s['cgroup_current']):>12} "
              f"{mib(s['anon']):>12} {mib(s['file']):>12}")

    last = stages[-2]
    print()
    print(f"  Расхождение на пике: cgroup показывает "
          f"{(last['cgroup_current'] - last['rss']) / 1024 / 1024:.0f} МиБ сверх RSS.")
    print("  Это page cache — docker stats считает его использованной памятью.")
PY

echo "═══ RSS против cgroup ═══"
docker run --rm --memory 512m -v "$PWD/memcheck.py:/m.py:ro" \
    python:3.13-slim python /m.py 2>/dev/null

echo "═══ что видно снаружи ═══"
cat <<'TXT'
  docker stats берёт MEM USAGE из memory.current — колонки «cgroup».
  Приложение своей памятью считает RSS.

  Разница на пике объясняется page cache. Он не является утечкой:
  ядро освободит его при нехватке памяти. Но docker stats
  показывает его как использованную память, а MEM % считается
  именно от этого числа.

  Практическое следствие: рост MEM USAGE до предела лимита —
  сам по себе НЕ признак проблемы. Признак проблемы —
  рост anon (он же RSS), потому что anon освободить нельзя.
TXT

Ожидаемый вывод:

text
═══ RSS против cgroup ═══
  этап                                RSS       cgroup         anon         file
  ──────────────────────────────────────────────────────────────────────────────
  старт                          12.4 МиБ     14.1 МиБ     11.8 МиБ      1.6 МиБ
  выделено 80 МиБ в куче         92.6 МиБ     94.3 МиБ     91.9 МиБ      1.6 МиБ
  прочитан файл 80 МиБ           92.7 МиБ    174.8 МиБ     92.0 МиБ     81.9 МиБ
  куча освобождена               12.8 МиБ     95.1 МиБ     12.2 МиБ     81.9 МиБ

  Расхождение на пике: cgroup показывает 82 МиБ сверх RSS.
  Это page cache — docker stats считает его использованной памятью.
═══ что видно снаружи ═══
  docker stats берёт MEM USAGE из memory.current — колонки «cgroup».
  Приложение своей памятью считает RSS.

  Разница на пике объясняется page cache. Он не является утечкой:
  ядро освободит его при нехватке памяти. Но docker stats
  показывает его как использованную память, а MEM % считается
  именно от этого числа.

  Практическое следствие: рост MEM USAGE до предела лимита —
  сам по себе НЕ признак проблемы. Признак проблемы —
  рост anon (он же RSS), потому что anon освободить нельзя.
TXT

Строка «прочитан файл 80 МиБ» — суть расхождения: RSS не изменился, cgroup вырос на 82 МиБ.

Последняя строка ещё показательнее: куча освобождена, RSS вернулся к 12 МиБ, а cgroup остался на 95 МиБ — кэш никуда не делся.

Именно поэтому вопрос «почему docker stats показывает 400 МБ, а приложение говорит 120 МБ» имеет штатный ответ, а не означает утечку.

docker stats в скрипте

bash
cd /tmp/insp
echo "═══ снимок без потока ═══"
docker stats --no-stream --format \
    'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.PIDs}}' 2>/dev/null \
    | sed 's/^/  /'

echo "═══ разбор в JSON ═══"
docker stats --no-stream --format '{{json .}}' target 2>/dev/null \
    | python3 -c "
import json, sys
d = json.load(sys.stdin)
for k in ('Name', 'CPUPerc', 'MemUsage', 'MemPerc', 'NetIO', 'BlockIO', 'PIDs'):
    print(f'  {k:<10} {d.get(k, \"-\")}')
"

echo "═══ ловушка ═══"
cat <<'TXT'
  БЕЗ --no-stream команда работает бесконечно:
    docker stats | head -3        # зависнет
    docker stats --no-stream      # даст снимок и завершится

  Для расчёта CPU % нужны ДВА замера, поэтому даже --no-stream
  занимает около секунды. В цикле по многим container'ам это
  складывается — берите все сразу одним вызовом.
TXT

Ожидаемый вывод:

text
═══ снимок без потока ═══
  NAME      CPU %     MEM USAGE / LIMIT     MEM %     PIDS
  target    0.00%     1.043MiB / 256MiB     0.41%     1
═══ разбор в JSON ═══
  Name       target
  CPUPerc    0.00%
  MemUsage   1.043MiB / 256MiB
  MemPerc    0.41%
  NetIO      1.02kB / 0B
  BlockIO    0B / 0B
  PIDs       1
═══ ловушка ═══
  БЕЗ --no-stream команда работает бесконечно:
    docker stats | head -3        # зависнет
    docker stats --no-stream      # даст снимок и завершится

  Для расчёта CPU % нужны ДВА замера, поэтому даже --no-stream
  занимает около секунды. В цикле по многим container'ам это
  складывается — берите все сразу одним вызовом.

Сопоставление процессов container и host

bash
cd /tmp/insp
echo "═══ процессы container с точки зрения host ═══"
docker top target 2>/dev/null | sed 's/^/  /'

echo "═══ тот же процесс изнутри ═══"
docker exec target sh -c 'ps -o pid,ppid,cmd 2>/dev/null || cat /proc/1/comm' 2>/dev/null \
    | sed 's/^/  /' || echo "  ps в образе нет"

echo "═══ соответствие PID ═══"
host_pid="$(docker inspect target --format '{{.State.Pid}}')"
echo "  PID на host:      $host_pid"
if sudo test -r "/proc/$host_pid/status" 2>/dev/null; then
    sudo grep -E '^NSpid|^Name' "/proc/$host_pid/status" | sed 's/^/  /'
else
    cat <<'TXT'
  PID внутри:       1
  (проверка через /proc/PID/status требует прав; поле NSpid
   перечисляет PID процесса во всех вложенных namespace:
   первое число — PID на host, последнее — внутри container)
TXT
fi

echo "═══ зачем это нужно ═══"
cat <<'TXT'
  Инструменты host — strace, gdb, py-spy, чтение /proc —
  работают с PID НА HOST. Внутри container тот же процесс
  имеет PID 1.

  Получить PID на host:
    docker inspect КОНТЕЙНЕР --format '{{.State.Pid}}'

  Дальше:
    sudo strace -p $PID
    sudo cat /proc/$PID/status
    sudo ls -l /proc/$PID/fd | wc -l     # число открытых дескрипторов
TXT

Ожидаемый вывод:

text
═══ процессы container с точки зрения host ═══
  UID    PID    PPID   C   STIME   TTY   TIME       CMD
  root   48213  48190  0   10:22   ?     00:00:00   sleep 300
═══ тот же процесс изнутри ═══
  PID  PPID CMD
    1     0 sleep 300
   28     0 sh -o pid,ppid,cmd
═══ соответствие PID ═══
  PID на host:      48213
  PID внутри:       1
  (проверка через /proc/PID/status требует прав; поле NSpid
   перечисляет PID процесса во всех вложенных namespace:
   первое число — PID на host, последнее — внутри container)
═══ зачем это нужно ═══
  Инструменты host — strace, gdb, py-spy, чтение /proc —
  работают с PID НА HOST. Внутри container тот же процесс
  имеет PID 1.

  Получить PID на host:
    docker inspect КОНТЕЙНЕР --format '{{.State.Pid}}'

  Дальше:
    sudo strace -p $PID
    sudo cat /proc/$PID/status
    sudo ls -l /proc/$PID/fd | wc -l     # число открытых дескрипторов

48213 и 1 — один и тот же процесс. Забыть об этом — типичная причина того, что strace -p 1 на host трассирует init, а не приложение.

Сводка состояния одной командой

bash
cd /tmp/insp
cat > snapshot.sh <<'SH'
#!/usr/bin/env bash
# Снимок состояния всех container'ов: факты, а не догадки.
set -uo pipefail

printf '\n  %-16s %-9s %-10s %-8s %-18s %-7s %s\n' \
    "ИМЯ" "СТАТУС" "ЗДОРОВЬЕ" "ПЕРЕЗАП" "ПАМЯТЬ" "CPU" "PID host"
printf '  %s\n' "──────────────────────────────────────────────────────────────────────────────────"

# Один вызов stats на всех — экономит секунду на каждом container
stats_json="$(docker stats --no-stream --format '{{json .}}' 2>/dev/null \
    | python3 -c "
import json, sys
out = {}
for line in sys.stdin:
    line = line.strip()
    if not line:
        continue
    d = json.loads(line)
    out[d['Name']] = {'mem': d.get('MemUsage', '-'), 'cpu': d.get('CPUPerc', '-')}
print(json.dumps(out))
" 2>/dev/null || echo '{}')"

while read -r name; do
    [ -z "$name" ] && continue
    read -r status health restarts pid oom exitcode <<EOF
$(docker inspect "$name" --format \
'{{.State.Status}} {{if .State.Health}}{{.State.Health.Status}}{{else}}—{{end}} {{.RestartCount}} {{.State.Pid}} {{.State.OOMKilled}} {{.State.ExitCode}}' 2>/dev/null)
EOF
    mem="$(echo "$stats_json" | python3 -c "
import json, sys
print(json.load(sys.stdin).get('$name', {}).get('mem', '-'))
" 2>/dev/null)"
    cpu="$(echo "$stats_json" | python3 -c "
import json, sys
print(json.load(sys.stdin).get('$name', {}).get('cpu', '-'))
" 2>/dev/null)"

    printf '  %-16s %-9s %-10s %-8s %-18s %-7s %s\n' \
        "${name:0:16}" "$status" "$health" "$restarts" "${mem:--}" "${cpu:--}" "$pid"

    # Признаки, требующие внимания
    [ "$oom" = "true" ] && printf '      ⚠ убит по нехватке памяти (OOM)\n'
    [ "$restarts" -gt 3 ] 2>/dev/null && printf '      ⚠ перезапусков: %s — вероятен restart loop\n' "$restarts"
    [ "$health" = "unhealthy" ] && printf '      ⚠ проверка здоровья не проходит — см. .State.Health.Log\n'
    [ "$status" = "exited" ] && [ "$exitcode" != "0" ] && \
        printf '      ⚠ завершился с кодом %s\n' "$exitcode"
done < <(docker ps -a --format '{{.Names}}' 2>/dev/null | head -20)

printf '\n'
SH
chmod +x snapshot.sh

echo "═══ снимок состояния ═══"
docker run -d --name failed alpine:3.21 sh -c 'exit 7' > /dev/null 2>&1
sleep 2
./snapshot.sh
docker rm -f target failed > /dev/null 2>&1
cd /tmp && rm -rf /tmp/insp

Ожидаемый вывод:

text
═══ снимок состояния ═══

  ИМЯ              СТАТУС    ЗДОРОВЬЕ   ПЕРЕЗАП  ПАМЯТЬ             CPU     PID host
  ──────────────────────────────────────────────────────────────────────────────────
  failed           exited    —          0        -                  -       0
      ⚠ завершился с кодом 7
  target           running   healthy    0        1.043MiB / 256MiB  0.00%   48213

Скрипт собирает в одну таблицу то, ради чего обычно вызывают пять разных команд, и сам отмечает признаки, требующие внимания.

Ключевое решение в нём — один вызов docker stats на все container'ы вместо вызова в цикле: каждый вызов занимает около секунды из-за необходимости двух замеров.


Практическое упражнение

Задание. Соберите инструмент диагностики, дающий факты вместо догадок.

Требования:

  1. Извлечь через docker inspect --format пять разных полей одной командой.
  2. Показать вывод неудачной проверки здоровья — то, чего нет в docker logs.
  3. Показать последовательность событий при старте container через docker events.
  4. Измерить расхождение между RSS процесса и MEM USAGE в docker stats, объяснив причину.
  5. Сопоставить PID процесса внутри container с PID на host.
  6. Написать сводку состояния, отмечающую признаки проблем и завершающуюся с кодом.

Подсказки

Подсказка 1

.State.Health отсутствует у container'ов без проверки — используйте {{if .State.Health}}, иначе шаблон упадёт.

Подсказка 2

Для пункта 4 нужен процесс, который читает файл, но не держит его содержимое в своей памяти.

Подсказка 3

docker events без --until не завершается. В скрипте: --since ... --until ... или timeout.

Решение

Показать решение
bash
mkdir -p /tmp/obs && cd /tmp/obs

# ─── Приложение с управляемым здоровьем ───────────────────────────────
cat > app.py <<'PY'
"""Сервис, состояние здоровья которого переключается по файлу-флагу.

Позволяет воспроизвести переход healthy → unhealthy без правки образа.
"""
from __future__ import annotations

import http.server
import json
import os
import threading
import time
from pathlib import Path

FLAG = Path("/tmp/unhealthy")
PORT = 8000


class Handler(http.server.BaseHTTPRequestHandler):
    def do_GET(self) -> None:
        if self.path == "/health":
            if FLAG.exists():
                self.send_response(503)
                body = {"статус": "не готов", "причина": FLAG.read_text().strip()}
            else:
                self.send_response(200)
                body = {"статус": "готов"}
        else:
            self.send_response(200)
            body = {"сервис": "работает", "pid": os.getpid()}
        payload = json.dumps(body, ensure_ascii=False).encode()
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(payload)))
        self.end_headers()
        self.wfile.write(payload)

    def log_message(self, *args: object) -> None:
        pass


if __name__ == "__main__":
    print(f"сервис запущен, pid={os.getpid()}", flush=True)
    server = http.server.ThreadingHTTPServer(("0.0.0.0", PORT), Handler)
    threading.Thread(target=server.serve_forever, daemon=True).start()
    while True:
        time.sleep(1)
PY

cat > healthcheck.py <<'PY'
"""Проверка здоровья: печатает диагностику в stdout.

Вывод попадает в .State.Health.Log — единственное место,
где он виден. В docker logs его нет.
"""
import json
import sys
import urllib.error
import urllib.request

try:
    with urllib.request.urlopen("http://127.0.0.1:8000/health", timeout=2) as r:
        body = json.loads(r.read())
    print(f"OK {body}")
    sys.exit(0)
except urllib.error.HTTPError as e:
    detail = e.read().decode("utf-8", "replace")[:120]
    print(f"ОТКАЗ код={e.code} тело={detail}")
    sys.exit(1)
except Exception as e:
    print(f"ОТКАЗ недоступен: {type(e).__name__}: {e}")
    sys.exit(1)
PY

# ─── Измерение памяти ─────────────────────────────────────────────────
cat > memory.py <<'PY'
"""Разделяет вклад кучи и page cache в учёт памяти cgroup."""
from __future__ import annotations

import json
import os
import sys
import time
from pathlib import Path


def rss() -> int:
    for line in Path("/proc/self/status").read_text().splitlines():
        if line.startswith("VmRSS:"):
            return int(line.split()[1]) * 1024
    return 0


def cgroup_current() -> int:
    for p in ("/sys/fs/cgroup/memory.current",
              "/sys/fs/cgroup/memory/memory.usage_in_bytes"):
        try:
            return int(Path(p).read_text().strip())
        except OSError:
            continue
    return 0


def cgroup_stat(key: str) -> int:
    for name in ("/sys/fs/cgroup/memory.stat",
                 "/sys/fs/cgroup/memory/memory.stat"):
        try:
            for line in Path(name).read_text().splitlines():
                k, _, v = line.partition(" ")
                if k == key and v.strip().isdigit():
                    return int(v)
        except OSError:
            continue
    return 0


def probe(label: str) -> dict[str, object]:
    return {
        "этап": label,
        "rss": rss(),
        "cgroup": cgroup_current(),
        "anon": cgroup_stat("anon"),
        "file": cgroup_stat("file"),
    }


if __name__ == "__main__":
    hold_seconds = float(sys.argv[1]) if len(sys.argv) > 1 else 0.0
    stages = [probe("старт")]

    path = "/tmp/pagecache.bin"
    with open(path, "wb") as f:
        for _ in range(20):
            f.write(os.urandom(4 * 1024 * 1024))
    os.sync()
    with open(path, "rb") as f:
        while f.read(1 << 20):
            pass
    stages.append(probe("прочитан файл 80 МиБ"))

    heap = bytearray(60 * 1024 * 1024)
    heap[::4096] = b"\x02" * (len(heap) // 4096)
    stages.append(probe("выделено 60 МиБ в куче"))

    if hold_seconds:
        time.sleep(hold_seconds)

    result = {
        "этапы": stages,
        "расхождение_байт": stages[1]["cgroup"] - stages[1]["rss"],
    }
    print(json.dumps(result, ensure_ascii=False))
    os.unlink(path)
PY

# ─── Сводка состояния ─────────────────────────────────────────────────
cat > summary.sh <<'SH'
#!/usr/bin/env bash
# Сводка состояния container'ов с отметкой признаков проблем.
# Код возврата: 0 — норма, 1 — есть замечания, 2 — критично.
set -uo pipefail

warn=0
crit=0

stats="$(docker stats --no-stream --format '{{json .}}' 2>/dev/null \
    | python3 -c "
import json, sys
out = {}
for line in sys.stdin:
    if line.strip():
        d = json.loads(line)
        out[d['Name']] = d
print(json.dumps(out))
" 2>/dev/null || echo '{}')"

field() {
    echo "$stats" | python3 -c "
import json, sys
d = json.load(sys.stdin).get('$1', {})
print(d.get('$2', '-'))
" 2>/dev/null
}

printf '\n  %-14s %-9s %-10s %-6s %-20s %-8s %s\n' \
    "ИМЯ" "СТАТУС" "ЗДОРОВЬЕ" "ПЕРЕЗ" "ПАМЯТЬ" "CPU" "PID"
printf '  %s\n' "─────────────────────────────────────────────────────────────────────────────────"

while read -r name; do
    [ -z "$name" ] && continue
    info="$(docker inspect "$name" --format \
'{{.State.Status}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}—{{end}}|{{.RestartCount}}|{{.State.Pid}}|{{.State.OOMKilled}}|{{.State.ExitCode}}' 2>/dev/null)" || continue
    IFS='|' read -r status health restarts pid oom code <<EOF
$info
EOF
    printf '  %-14s %-9s %-10s %-6s %-20s %-8s %s\n' \
        "${name:0:14}" "$status" "$health" "$restarts" \
        "$(field "$name" MemUsage)" "$(field "$name" CPUPerc)" "$pid"

    if [ "$oom" = "true" ]; then
        printf '      КРИТ  убит по нехватке памяти (OOM)\n'; crit=$((crit + 1))
    fi
    if [ "$health" = "unhealthy" ]; then
        printf '      КРИТ  проверка не проходит; последний вывод:\n'
        docker inspect "$name" --format '{{json .State.Health.Log}}' 2>/dev/null \
            | python3 -c "
import json, sys
log = json.load(sys.stdin) or []
if log:
    e = log[-1]
    print('        код=' + str(e['ExitCode']) + ' ' + e['Output'].strip()[:70])
" 2>/dev/null
        crit=$((crit + 1))
    fi
    if [ "${restarts:-0}" -gt 3 ] 2>/dev/null; then
        printf '      ВАЖНО перезапусков %s — вероятен restart loop\n' "$restarts"
        warn=$((warn + 1))
    fi
    if [ "$status" = "exited" ] && [ "$code" != "0" ]; then
        printf '      ВАЖНО код выхода %s\n' "$code"; warn=$((warn + 1))
    fi
done < <(docker ps -a --format '{{.Names}}' 2>/dev/null | head -30)

printf '\n  критично=%s важно=%s\n' "$crit" "$warn"
[ "$crit" -gt 0 ] && exit 2
[ "$warn" -gt 0 ] && exit 1
exit 0
SH
chmod +x summary.sh

fail=0
ok()  { printf '  ✓ %s\n' "$1"; }
bad() { printf '  ✗ %s\n' "$1"; fail=1; }

printf '\n═══ Подготовка: запуск сервиса ═══\n'
docker run -d --name obs-app \
    --memory 512m --cpus 0.5 \
    --health-cmd 'python /hc.py' \
    --health-interval 3s --health-retries 2 --health-timeout 2s \
    -e PYTHONUNBUFFERED=1 \
    -v "$PWD/app.py:/app.py:ro" -v "$PWD/healthcheck.py:/hc.py:ro" \
    python:3.13-slim python /app.py > /dev/null
sleep 10
printf '    статус: %s\n' "$(docker inspect obs-app --format '{{.State.Status}}/{{.State.Health.Status}}')"

printf '\n═══ Требование 1: пять полей одной командой ═══\n'
one_cmd="$(docker inspect obs-app --format \
'{{.Name}}|{{.State.Status}}|{{.State.Health.Status}}|{{.RestartCount}}|{{.HostConfig.Memory}}|{{.State.Pid}}')"
printf '    %s\n' "$one_cmd"
n_fields="$(echo "$one_cmd" | awk -F'|' '{print NF}')"
printf '    полей получено: %s\n' "$n_fields"
[ "$n_fields" -ge 5 ] && ok "шесть полей одним вызовом inspect" \
    || bad "полей: $n_fields"

printf '\n═══ Требование 2: вывод неудачной проверки ═══\n'
printf '    в docker logs при healthy: %s строк\n' "$(docker logs obs-app 2>&1 | grep -c .)"
docker exec obs-app sh -c 'echo "база данных недоступна" > /tmp/unhealthy'
printf '    переключили в unhealthy, ждём...\n'
sleep 12
health_now="$(docker inspect obs-app --format '{{.State.Health.Status}}')"
printf '    статус здоровья: %s\n' "$health_now"
printf '    docker logs говорит о причине:\n'
docker logs obs-app 2>&1 | tail -2 | sed 's/^/      /'
printf '    .State.Health.Log говорит о причине:\n'
hc_out="$(docker inspect obs-app --format '{{json .State.Health.Log}}' \
    | python3 -c "
import json, sys
log = json.load(sys.stdin) or []
for e in log[-2:]:
    print('      код=' + str(e['ExitCode']) + ' ' + e['Output'].strip()[:70])
")"
echo "$hc_out"
if echo "$hc_out" | grep -q 'база данных недоступна' && [ "$health_now" = "unhealthy" ]; then
    ok "причина видна ТОЛЬКО в Health.Log, не в docker logs"
else
    bad "статус=$health_now, вывод проверки не содержит причину"
fi
docker exec obs-app rm -f /tmp/unhealthy
sleep 8

printf '\n═══ Требование 3: последовательность событий ═══\n'
docker run -d --name obs-seq \
    --health-cmd 'python -c "print(1)"' --health-interval 2s \
    python:3.13-slim sleep 60 > /dev/null
sleep 8
printf '    события за время старта:\n'
docker events --since 20s --until 0s --filter 'container=obs-seq' \
    --format '{{.Time}}|{{.Action}}' 2>/dev/null \
    | python3 -c "
import sys, datetime
prev = None
for line in sys.stdin:
    parts = line.strip().split('|', 1)
    if len(parts) != 2:
        continue
    ts, action = int(parts[0]), parts[1]
    delta = '' if prev is None else f' (+{ts - prev}с)'
    print(f\"      {datetime.datetime.fromtimestamp(ts).strftime('%H:%M:%S')} {action}{delta}\")
    prev = ts
"
n_events="$(docker events --since 20s --until 0s --filter 'container=obs-seq' \
    --format '{{.Action}}' 2>/dev/null | grep -c . || echo 0)"
has_health="$(docker events --since 20s --until 0s --filter 'container=obs-seq' \
    --format '{{.Action}}' 2>/dev/null | grep -c health || echo 0)"
[ "$n_events" -ge 3 ] && [ "$has_health" -ge 1 ] \
    && ok "последовательность видна: create → start → health_status" \
    || bad "событий: $n_events, из них health: $has_health"
docker rm -f obs-seq > /dev/null

printf '\n═══ Требование 4: RSS против MEM USAGE ═══\n'
mem_out="$(docker run --rm --memory 512m -v "$PWD/memory.py:/m.py:ro" \
    python:3.13-slim python /m.py 2>/dev/null)"
echo "$mem_out" | python3 -c "
import json, sys
d = json.load(sys.stdin)
mib = lambda n: f'{n / 1024 / 1024:6.1f}'
print(f\"    {'этап':<26} {'RSS':>9} {'cgroup':>9} {'anon':>9} {'file':>9}\")
print('    ' + '─' * 66)
for s in d['этапы']:
    print(f\"    {s['этап']:<26} {mib(s['rss']):>9} {mib(s['cgroup']):>9} \"
          f\"{mib(s['anon']):>9} {mib(s['file']):>9}\")
print(f\"    расхождение после чтения файла: {d['расхождение_байт'] / 1024 / 1024:.0f} МиБ\")
"
gap="$(echo "$mem_out" | python3 -c "
import json, sys
print(int(json.load(sys.stdin)['расхождение_байт'] / 1024 / 1024))
")"
printf '    причина: page cache учитывается cgroup, но не входит в RSS\n'
[ "$gap" -gt 30 ] \
    && ok "расхождение измерено: $gap МиБ page cache" \
    || bad "расхождение $gap МиБ — page cache не проявился"

printf '\n═══ Требование 5: PID внутри и на host ═══\n'
host_pid="$(docker inspect obs-app --format '{{.State.Pid}}')"
in_pid="$(docker exec obs-app python -c "
import os
print(os.getpid() if os.getpid() == 1 else 1)
" 2>/dev/null || echo "?")"
app_reported="$(docker logs obs-app 2>&1 | grep -o 'pid=[0-9]*' | head -1 | cut -d= -f2)"
printf '    PID на host (inspect):        %s\n' "$host_pid"
printf '    PID внутри (сообщило приложение): %s\n' "${app_reported:-?}"
printf '    docker top показывает PID host:\n'
docker top obs-app 2>/dev/null | tail -1 | awk '{ printf "      %s %s\n", $2, $NF }'
if sudo test -r "/proc/$host_pid/status" 2>/dev/null; then
    printf '    NSpid из /proc: %s\n' \
        "$(sudo grep '^NSpid' "/proc/$host_pid/status" | cut -f2-)"
else
    printf '    (NSpid недоступен без прав root)\n'
fi
[ "$app_reported" = "1" ] && [ "$host_pid" != "1" ] \
    && ok "один процесс: PID 1 внутри, $host_pid на host" \
    || bad "внутри=$app_reported, host=$host_pid"

printf '\n═══ Требование 6: сводка состояния ═══\n'
docker run -d --name obs-oom --memory 32m python:3.13-slim \
    python -c "
b = bytearray()
while True:
    b += bytearray(4 * 1024 * 1024)
" > /dev/null 2>&1
docker run -d --name obs-failed alpine:3.21 sh -c 'exit 3' > /dev/null 2>&1
sleep 8

./summary.sh; rc_problems=$?
printf '    код возврата при проблемах: %s\n' "$rc_problems"

docker rm -f obs-oom obs-failed > /dev/null 2>&1
sleep 1
./summary.sh > clean.txt 2>&1; rc_clean=$?
printf '    код возврата на исправной конфигурации: %s\n' "$rc_clean"
tail -3 clean.txt | sed 's/^/    /'

[ "$rc_problems" -ge 1 ] && [ "$rc_clean" -eq 0 ] \
    && ok "сводка различает состояния и даёт разные коды" \
    || bad "коды: $rc_problems и $rc_clean"

printf '\n═══ ИТОГ ═══\n'
[ "$fail" -eq 0 ] && echo "  все требования выполнены" || echo "  ЕСТЬ ПРОВАЛЫ"

docker rm -f obs-app > /dev/null 2>&1
cd /tmp && rm -rf /tmp/obs
exit "$fail"

Ожидаемый вывод:

text
═══ Подготовка: запуск сервиса ═══
    статус: running/healthy

═══ Требование 1: пять полей одной командой ═══
    /obs-app|running|healthy|0|536870912|51204
    полей получено: 6
  ✓ шесть полей одним вызовом inspect

═══ Требование 2: вывод неудачной проверки ═══
    в docker logs при healthy: 1 строк
    переключили в unhealthy, ждём...
    статус здоровья: unhealthy
    docker logs говорит о причине:
      сервис запущен, pid=1
    .State.Health.Log говорит о причине:
      код=1 ОТКАЗ код=503 тело={"статус": "не готов", "причина": "база данных недоступна"}
      код=1 ОТКАЗ код=503 тело={"статус": "не готов", "причина": "база данных недоступна"}
  ✓ причина видна ТОЛЬКО в Health.Log, не в docker logs

═══ Требование 3: последовательность событий ═══
    события за время старта:
      10:31:02 create
      10:31:02 start
      10:31:05 health_status: healthy
  ✓ последовательность видна: create → start → health_status

═══ Требование 4: RSS против MEM USAGE ═══
    этап                             RSS    cgroup      anon      file
    ──────────────────────────────────────────────────────────────────
    старт                           12.1      13.8      11.6       1.5
    прочитан файл 80 МиБ            12.3      95.4      11.8      82.1
    выделено 60 МиБ в куче          72.5     155.7      71.9      82.1
    расхождение после чтения файла: 83 МиБ
    причина: page cache учитывается cgroup, но не входит в RSS
  ✓ расхождение измерено: 83 МиБ page cache

═══ Требование 5: PID внутри и на host ═══
    PID на host (inspect):        51204
    PID внутри (сообщило приложение): 1
    docker top показывает PID host:
      51204 /app.py
    (NSpid недоступен без прав root)
  ✓ один процесс: PID 1 внутри, 51204 на host

═══ Требование 6: сводка состояния ═══

  ИМЯ            СТАТУС    ЗДОРОВЬЕ   ПЕРЕЗ  ПАМЯТЬ               CPU      PID
  ─────────────────────────────────────────────────────────────────────────────────
  obs-failed     exited    —          0      -                    -        0
      ВАЖНО код выхода 3
  obs-oom        exited    —          0      -                    -        0
      КРИТ  убит по нехватке памяти (OOM)
  obs-app        running   healthy    0      13.2MiB / 512MiB     0.11%    51204

  критично=1 важно=1
    код возврата при проблемах: 2
    код возврата на исправной конфигурации: 0
      критично=0 важно=0
  ✓ сводка различает состояния и даёт разные коды

═══ ИТОГ ═══
  все требования выполнены

Все требования выполнены.

Требование 2 — самый практичный результат. docker logs при unhealthy показывает одну строку о запуске и ничего о причине отказа. Причина — в .State.Health.Log, и другого места у неё нет.

Три решения, определяющие качество.

Проверка здоровья печатает диагностику в stdout, а не просто возвращает код. Healthcheck, состоящий из curl -f, при отказе даёт код 1 и ничего больше — приходится гадать. Здесь вывод содержит код ответа и тело, и всё это попадает в Health.Log. Стоимость нулевая, польза проявляется ровно в момент аварии.

Здоровье переключается файлом-флагом, а не остановкой сервиса. Проще было бы убить процесс — но тогда container перешёл бы в exited, и наблюдать переход healthy → unhealthy стало бы не на чем. Флаг оставляет сервис живым и меняет только ответ /health, воспроизводя реальный сценарий: приложение работает, но зависимость недоступна.

Сводка вызывает docker stats один раз на все container'ы. Вызов в цикле выглядел бы естественнее, но каждый занимает около секунды: для расчёта CPU % нужны два замера с интервалом. На двадцати container'ах разница — двадцать секунд против одной.

Чего решение не делает. Расхождение RSS и cgroup измерено на одном сценарии — чтение файла; вклад ядерных структур (slab, стеки потоков) не разделён. NSpid не проверен: чтение /proc/PID/status для чужого процесса требует прав root, и решение об этом сообщает, а не обходит. События читаются за фиксированное окно --since 20s; при перезапуске daemon история теряется, и этот случай не обрабатывается. Наконец, сводка отмечает признаки, но не ставит диагноз: restart loop она увидит, а причину — нет, для этого нужны логи и события.

Проверка результата

bash
docker inspect КОНТЕЙНЕР --format '{{.State.Status}} {{.State.ExitCode}} {{.State.OOMKilled}} {{.RestartCount}}'
docker inspect КОНТЕЙНЕР --format '{{json .State.Health}}' | python3 -m json.tool
docker events --since 5m --until 0s --filter "container=КОНТЕЙНЕР" --format '{{.Action}}'
docker stats --no-stream --format '{{json .}}' КОНТЕЙНЕР

Все четыре команды завершаются; ни одна не зависает.

Типичные ошибки

ОшибкаПричинаИсправление
docker stats в конвейереЗабыли --no-streamКоманда не завершается; зависает
docker events без --untilТо жеПоток бесконечен; --until или timeout
Ищут причину unhealthy в docker logsЕстественный инструментВывод проверки — в .State.Health.Log
{{.State.Health.Status}} без проверкиРаботает на одном containerУ других поля нет — ошибка шаблона
Считают MEM USAGE памятью приложенияТак называется колонкаВключает page cache; смотреть anon
strace -p 1 на hostPID внутри равен 1На host PID другой: .State.Pid
Вызов docker stats в циклеЕстественная структураСекунда на вызов; брать все сразу
Ждут историю событий после перезапуска daemonКажется, что она хранитсяБуфер теряется
Читают весь docker inspect глазамиНе знают про --formatСотни строк; выбирать поля
Считают рост MEM USAGE утечкойЧисло растётКэш освобождаем; утечка — рост anon

Контрольные вопросы

На понимание:

  1. Почему MEM USAGE в docker stats больше, чем память по мнению приложения?
  2. Где увидеть вывод неудачной проверки здоровья и почему не в docker logs?
  3. Почему docker stats --no-stream всё равно занимает около секунды?
  4. Чем PID в docker top отличается от PID внутри container?
  5. Что показывает docker events такого, чего не даёт docker ps?

На применение:

  1. Как получить состояние, код выхода, признак OOM и число перезапусков одной командой?
  2. Как в скрипте прочитать события и не зависнуть?
  3. Как отличить рост кэша от утечки памяти?

На диагностику:

  1. Container unhealthy, docker logs пуст. Следующий шаг?
  2. MEM USAGE подошёл к лимиту, приложение работает нормально. Проблема или нет?

Краткое резюме

  1. --format с шаблоном Go достаёт нужные поля вместо сотен строк JSON.
  2. Несколько полей берут одним вызовом — это быстрее и удобнее для скриптов.
  3. .State.Health отсутствует без настроенной проверки: нужен {{if}}.
  4. .State.Health.Log — единственное место, где виден вывод проверки здоровья.
  5. docker events показывает последовательность и время, а не только текущее состояние.
  6. Поток событий бесконечен: в скрипте нужен --until или timeout.
  7. История событий теряется при перезапуске daemon.
  8. MEM USAGE берётся из memory.current cgroup и включает page cache.
  9. Приложение считает своей памятью только anon (RSS) — расхождение штатно.
  10. Признак утечки — рост anon, а не рост общего числа.
  11. docker stats --no-stream обязателен в скриптах; вызов занимает около секунды.
  12. PID в docker top — это PID на host; внутри container процесс имеет PID 1.

Официальные источники

ИсточникСсылкаЧто подтверждает
Docker: docker inspecthttps://docs.docker.com/reference/cli/docker/inspect/Структура вывода, --format
Docker: format referencehttps://docs.docker.com/engine/cli/formatting/Шаблоны Go, json, range
Docker: docker eventshttps://docs.docker.com/reference/cli/docker/system/events/Типы событий, фильтры
Docker: docker statshttps://docs.docker.com/reference/cli/docker/container/stats/Колонки и их значение
Docker: docker tophttps://docs.docker.com/reference/cli/docker/container/top/Процессы container
Docker: runtime metricshttps://docs.docker.com/engine/containers/runmetrics/Источники цифр в cgroup
Linux: cgroup v2 memoryhttps://docs.kernel.org/admin-guide/cgroup-v2.html#memory-interface-filesmemory.current, memory.stat
Linux: proc(5)https://man7.org/linux/man-pages/man5/proc.5.htmlNSpid, VmRSS

Навигация

← Предыдущий материал
Вернуться к разделу
Следующий материал → Нехватка ресурсов
Главное оглавление

Markdown на GitHub ↗