13.2. Inspect, events, stats
Цели
После этого материала вы сможете:
- извлечь любое поле из
docker inspectчерез--formatи знать, где смотреть структуру; - наблюдать за происходящим в реальном времени через
docker eventsс фильтрами; - объяснить каждую колонку
docker statsи откуда берётся её значение; - объяснить, почему
MEM USAGEне совпадает с тем, что показывает приложение; - сопоставить процесс в container с процессом на host;
- собрать факты о состоянии системы одной командой вместо десяти.
Предварительные знания
- 4.2. Состояния container;
- 2.3. Cgroups — откуда берутся цифры;
- 11.4. Лимиты ресурсов.
Ключевые термины
| Термин | Объяснение |
|---|---|
--format | Шаблон Go для выборки полей |
events | Поток сообщений о происходящем в daemon |
RSS | Резидентная память процесса |
page cache | Кэш файлов в памяти, учитываемый cgroup |
working set | Память за вычетом освобождаемого кэша |
PID namespace | Причина различия PID внутри и снаружи |
Теория
docker inspect и --format
Команда возвращает объект JSON на несколько сотен строк. Читать его целиком бессмысленно; нужен способ доставать конкретное.
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, изменение сетей и томов.
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}}'
Ключевые события:
| Событие | Когда |
|---|---|
create | Container создан |
start | Запущен |
die | Главный процесс завершился |
stop, kill | Остановлен по команде |
oom | Процесс убит по нехватке памяти |
health_status: healthy | Проверка стала успешной |
health_status: unhealthy | Проверка перестала проходить |
destroy | Container удалён |
Главное свойство events, ради которого его применяют: он показывает последовательность и время. Когда сервис перезапускается по кругу, docker ps показывает текущее состояние, а events — что именно ему предшествовало.
Ограничение: события хранятся не вечно. --since работает в пределах буфера daemon; после перезапуска daemon история теряется.
docker stats: откуда цифры
| Колонка | Источник | Подвох |
|---|---|---|
CPU % | Разница cpu.stat за интервал | Может превышать 100 % — это сумма по ядрам |
MEM USAGE | memory.current cgroup | Включает page cache |
LIMIT | memory.max cgroup | Без лимита — вся память host |
MEM % | USAGE / LIMIT | Завышена из-за кэша |
NET I/O | Счётчики интерфейсов | Суммарно за всё время жизни |
BLOCK I/O | io.stat cgroup | Только прямые обращения к диску |
PIDS | pids.current | Потоки считаются тоже |
Главное расхождение — память. MEM USAGE берётся из cgroup и включает page cache: файлы, прочитанные приложением и оставшиеся в памяти. Приложение считает своей памятью только RSS.
memory.current = anon (RSS) + page cache + kernel + ...
приложение видит = anon
Практически: Python-процесс, прочитавший гигабайтный файл, покажет в docker stats рост на гигабайт, хотя sys.getsizeof ничего не изменит.
Это не утечка. Page cache освобождается ядром при нехватке памяти.
Как получить сопоставимое число: memory.current минус освобождаемая часть кэша. Точный расчёт — в уроке 11.4; здесь достаточно знать, что разница ожидаема.
docker top и соответствие процессам host
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'у, и для расчёта процентов ей нужны два замера с интервалом. Отсюда задержка около секунды перед первым выводом.
docker stats --no-stream КОНТЕЙНЕР
Флаг --no-stream даёт один снимок и завершается — то, что нужно для скриптов. Без него команда работает бесконечно и в конвейере зависает.
Формат docker events и парсинг
События выдаются потоком, по одному JSON на строку при --format '{{json .}}'. Это удобно для обработки, но требует помнить: поток не заканчивается. Скрипт, читающий docker events, должен либо ограничивать себя --until, либо завершаться сам.
timeout 10 docker events --format '{{json .}}'
docker events --since 5m --until 1m --format '{{json .}}'
Второй вариант предпочтителен в скриптах: он читает историю и завершается.
Команды и примеры
Выборка полей одной командой
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}}'
Ожидаемый вывод:
═══ отдельные поля ═══
состояние: 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 вызовет ошибку шаблона.
История проверок здоровья
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
Ожидаемый вывод:
═══ последние проверки ═══
статус: 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
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
Ожидаемый вывод:
═══ события за последнюю минуту ═══
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 показывает больше, чем приложение
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
Ожидаемый вывод:
═══ 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 в скрипте
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
Ожидаемый вывод:
═══ снимок без потока ═══
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
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
Ожидаемый вывод:
═══ процессы 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, а не приложение.
Сводка состояния одной командой
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
Ожидаемый вывод:
═══ снимок состояния ═══
ИМЯ СТАТУС ЗДОРОВЬЕ ПЕРЕЗАП ПАМЯТЬ CPU PID host
──────────────────────────────────────────────────────────────────────────────────
failed exited — 0 - - 0
⚠ завершился с кодом 7
target running healthy 0 1.043MiB / 256MiB 0.00% 48213
Скрипт собирает в одну таблицу то, ради чего обычно вызывают пять разных команд, и сам отмечает признаки, требующие внимания.
Ключевое решение в нём — один вызов docker stats на все container'ы вместо вызова в цикле: каждый вызов занимает около секунды из-за необходимости двух замеров.
Практическое упражнение
Задание. Соберите инструмент диагностики, дающий факты вместо догадок.
Требования:
- Извлечь через
docker inspect --formatпять разных полей одной командой. - Показать вывод неудачной проверки здоровья — то, чего нет в
docker logs. - Показать последовательность событий при старте container через
docker events. - Измерить расхождение между RSS процесса и
MEM USAGEвdocker stats, объяснив причину. - Сопоставить PID процесса внутри container с PID на host.
- Написать сводку состояния, отмечающую признаки проблем и завершающуюся с кодом.
Подсказки
Подсказка 1
.State.Health отсутствует у container'ов без проверки — используйте {{if .State.Health}}, иначе шаблон упадёт.
Подсказка 2
Для пункта 4 нужен процесс, который читает файл, но не держит его содержимое в своей памяти.
Подсказка 3
docker events без --until не завершается. В скрипте: --since ... --until ... или timeout.
Решение
Показать решение
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"
Ожидаемый вывод:
═══ Подготовка: запуск сервиса ═══
статус: 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 она увидит, а причину — нет, для этого нужны логи и события.
Проверка результата
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 на host | PID внутри равен 1 | На host PID другой: .State.Pid |
Вызов docker stats в цикле | Естественная структура | Секунда на вызов; брать все сразу |
| Ждут историю событий после перезапуска daemon | Кажется, что она хранится | Буфер теряется |
Читают весь docker inspect глазами | Не знают про --format | Сотни строк; выбирать поля |
Считают рост MEM USAGE утечкой | Число растёт | Кэш освобождаем; утечка — рост anon |
Контрольные вопросы
На понимание:
- Почему
MEM USAGEвdocker statsбольше, чем память по мнению приложения? - Где увидеть вывод неудачной проверки здоровья и почему не в
docker logs? - Почему
docker stats --no-streamвсё равно занимает около секунды? - Чем PID в
docker topотличается от PID внутри container? - Что показывает
docker eventsтакого, чего не даётdocker ps?
На применение:
- Как получить состояние, код выхода, признак OOM и число перезапусков одной командой?
- Как в скрипте прочитать события и не зависнуть?
- Как отличить рост кэша от утечки памяти?
На диагностику:
- Container
unhealthy,docker logsпуст. Следующий шаг? MEM USAGEподошёл к лимиту, приложение работает нормально. Проблема или нет?
Краткое резюме
--formatс шаблоном Go достаёт нужные поля вместо сотен строк JSON.- Несколько полей берут одним вызовом — это быстрее и удобнее для скриптов.
.State.Healthотсутствует без настроенной проверки: нужен{{if}}..State.Health.Log— единственное место, где виден вывод проверки здоровья.docker eventsпоказывает последовательность и время, а не только текущее состояние.- Поток событий бесконечен: в скрипте нужен
--untilилиtimeout. - История событий теряется при перезапуске daemon.
MEM USAGEберётся изmemory.currentcgroup и включает page cache.- Приложение считает своей памятью только
anon(RSS) — расхождение штатно. - Признак утечки — рост
anon, а не рост общего числа. docker stats --no-streamобязателен в скриптах; вызов занимает около секунды.- PID в
docker top— это PID на host; внутри container процесс имеет PID 1.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
Docker: docker inspect | https://docs.docker.com/reference/cli/docker/inspect/ | Структура вывода, --format |
| Docker: format reference | https://docs.docker.com/engine/cli/formatting/ | Шаблоны Go, json, range |
Docker: docker events | https://docs.docker.com/reference/cli/docker/system/events/ | Типы событий, фильтры |
Docker: docker stats | https://docs.docker.com/reference/cli/docker/container/stats/ | Колонки и их значение |
Docker: docker top | https://docs.docker.com/reference/cli/docker/container/top/ | Процессы container |
| Docker: runtime metrics | https://docs.docker.com/engine/containers/runmetrics/ | Источники цифр в cgroup |
| Linux: cgroup v2 memory | https://docs.kernel.org/admin-guide/cgroup-v2.html#memory-interface-files | memory.current, memory.stat |
Linux: proc(5) | https://man7.org/linux/man-pages/man5/proc.5.html | NSpid, VmRSS |
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий материал → Нехватка ресурсов
Главное оглавление