13.6. Таблица диагностики
Это справочный материал. Прочитайте его один раз целиком, чтобы знать состав, и возвращайтесь по мере необходимости.
Формат каждой записи одинаков: симптом → вероятная причина → проверка → исправление. Проверка обязательна: она отличает диагностику от угадывания.
Цели
После этого материала вы сможете:
- пройти от симптома к причине по последовательности проверок, а не перебором;
- назвать первые пять команд, которые сужают область поиска;
- найти в таблице тридцать типичных проблем и способ проверки каждой;
- понять, почему одинаковый симптом имеет разные причины, и различить их.
Предварительные знания
Разделы 04, 07, 08 и уроки 13.1–13.5.
Методика: пять вопросов
Прежде чем открывать таблицу, ответьте на пять вопросов. Каждый сужает область.
1. Container вообще создан?
docker ps -a | grep ИМЯ
├── нет → проблема при создании: логи daemon
└── да ↓
2. Он работает или завершился?
docker inspect ИМЯ --format '{{.State.Status}} {{.State.ExitCode}}'
├── exited → раздел «Запуск и остановка», смотреть код
└── running ↓
3. Приложение что-нибудь вывело?
docker logs ИМЯ
├── трассировка → проблема в приложении, не в Docker
├── пусто → буферизация или драйвер логов
└── штатный вывод ↓
4. Проверка здоровья проходит?
docker inspect ИМЯ --format '{{json .State.Health}}'
├── unhealthy → смотреть Health.Log: там вывод проверки
└── healthy или отсутствует ↓
5. Изнутри работает?
docker exec ИМЯ curl -s localhost:ПОРТ
├── работает → проблема в сети или публикации порта
└── не работает → проблема в приложении
Пять команд отделяют пять разных областей. Дальше — таблица соответствующего раздела.
Главное правило: не переходите к шагу N+1, не выполнив шаг N. Порядок выбран так, что каждый следующий шаг имеет смысл только при определённом ответе на предыдущий.
1. Запуск и остановка
| # | Симптом | Вероятная причина | Проверка | Исправление |
|---|---|---|---|---|
| 1.1 | docker run завершается ошибкой, container не создан | Ошибка на стороне daemon: монтирование, сеть, место | sudo journalctl -u docker.service --since '5 min ago' -p err | По сообщению daemon |
| 1.2 | Код выхода 137, OOMKilled: true | Исчерпан лимит памяти | docker inspect ИМЯ --format '{{.State.OOMKilled}}' | Поднять --memory или найти причину роста (13.3) |
| 1.3 | Код 137, OOMKilled: false | docker kill, таймаут stop или OOM на host | docker events --filter container=ИМЯ; sudo dmesg -T | grep -i killed | Зависит от источника |
| 1.4 | Код 143 | PID 1 умер от SIGTERM — обычно из-за --init | Норма | Обработать сигнал и выйти с кодом 0 |
| 1.5 | Код 126 | Файл найден, но не исполняемый | docker run --rm ОБРАЗ ls -l /путь | chmod +x в Dockerfile |
| 1.6 | Код 127 | Команда не найдена | docker run --rm ОБРАЗ which КОМАНДА | Проверить PATH и наличие в образе |
| 1.7 | Ошибка exec format error | Архитектура образа не совпадает с host | docker inspect ОБРАЗ --format '{{.Architecture}}' | --platform или пересобрать |
| 1.8 | Restart loop: перезапуск каждые N секунд | Процесс падает сразу после старта | docker logs --tail 50 ИМЯ; docker inspect --format '{{.RestartCount}}' | По содержимому логов |
| 1.9 | Container running, но приложение не отвечает | Процесс жив, но завис | docker top ИМЯ; sidecar с py-spy dump | По стеку процесса |
| 1.10 | docker stop длится 10 секунд и завершается кодом 137 | Приложение не обрабатывает SIGTERM | docker logs — есть ли сообщение о завершении | Установить обработчик (6.7) |
| 1.11 | Приложение получает SIGTERM, но не завершается | SIGTERM уходит оболочке, а не приложению | docker inspect --format '{{json .Config.Entrypoint}}' | Форма exec: CMD ["python", "app.py"] |
| 1.12 | Container завершается сразу с кодом 0 | Главный процесс отработал и вышел | docker logs ИМЯ | Процесс должен работать в переднем плане |
Развёрнуто: restart loop
Симптом одинаков, причин три. Различает их время до падения:
| Время жизни | Причина | Проверка |
|---|---|---|
| Меньше секунды | Ошибка команды или отсутствие файла | docker logs; код выхода 126/127 |
| 1–10 секунд | Ошибка инициализации: конфигурация, подключение к БД | docker logs --tail 50 |
| Стабильные 30–60 секунд | Провал проверки здоровья или OOM | docker inspect --format '{{json .State.Health}}'; OOMKilled |
Третий случай самый коварный: интервал кажется «периодическим», и его связывают с задачей по расписанию, а не с проверкой здоровья.
docker inspect ИМЯ --format '{{.RestartCount}} перезапусков'
docker events --since 30m --filter container=ИМЯ --format '{{.Time}} {{.Action}}'
Вывод events даёт интервалы. Стабильный интервал — признак таймаута, а не случайного сбоя.
2. Логи
| # | Симптом | Вероятная причина | Проверка | Исправление |
|---|---|---|---|---|
| 2.1 | docker logs пуст, приложение работает | Буферизация stdout | docker inspect --format '{{json .Config.Env}}' | grep -o PYTHONUNBUFFERED | PYTHONUNBUFFERED=1 |
| 2.2 | Логи появляются с задержкой в минуты | То же | То же | То же |
| 2.3 | При падении последние строки отсутствуют | Буфер не сброшен | Воспроизвести с PYTHONUNBUFFERED=1 | Тот же |
| 2.4 | docker logs не работает вовсе | Драйвер none или удалённый без кэша | docker inspect --format '{{.HostConfig.LogConfig.Type}}' | Сменить драйвер или включить dual logging |
| 2.5 | Диск заполнен, docker system df показывает мало | Логи без ротации | sudo du -sh /var/lib/docker/containers/*/ | sort -h | tail | max-size и max-file (13.1) |
| 2.6 | Агрегатор получает битые записи JSON | Строка длиннее 16 КиБ разрезана | Длина строк в выводе; attrs.partial_id в файле лога | Ограничить длину полей в приложении |
| 2.7 | Приложение зависает под нагрузкой, стек в write | Блокирующий режим драйвера логов | docker inspect --format '{{json .HostConfig.LogConfig.Config}}' | mode=non-blocking |
| 2.8 | Часть строк пропадает | non-blocking с малым буфером | Сравнить счётчик приложения с docker logs | wc -l | Увеличить max-buffer-size |
3. Сеть
| # | Симптом | Вероятная причина | Проверка | Исправление |
|---|---|---|---|---|
| 3.1 | Порт опубликован, снаружи не отвечает | Приложение слушает 127.0.0.1 | docker exec ИМЯ ss -tlnp или sidecar | Слушать 0.0.0.0 |
| 3.2 | connection refused между сервисами | Целевой сервис ещё не запущен | docker ps; depends_on с условием | condition: service_healthy (9.3) |
| 3.3 | Имя сервиса не разрешается | Сеть bridge по умолчанию вместо пользовательской | docker inspect --format '{{json .NetworkSettings.Networks}}' | Пользовательская сеть (8.4) |
| 3.4 | localhost внутри container'а не находит соседа | localhost — это сам container | Проверить обращение по имени сервиса | Использовать имя сервиса |
| 3.5 | Порт занят на host | Другой процесс слушает его | sudo ss -tlnp | grep :ПОРТ | Сменить порт публикации |
| 3.6 | Работает с --network host, не работает без него | Приложение привязано к 127.0.0.1 | Как 3.1 | Как 3.1, а не переход на host |
| 3.7 | DNS не работает внутри container'а | Проблема 127.0.0.11 или настроек daemon | docker exec ИМЯ cat /etc/resolv.conf | Проверить dns в daemon.json |
| 3.8 | Соединение к внешнему миру не устанавливается | Правила брандмауэра host | docker exec ИМЯ python -c "import socket; socket.create_connection(('1.1.1.1',443),3)" | Правила iptables/nftables |
| 3.9 | Медленное разрешение имён | Поиск по search-доменам | docker exec ИМЯ cat /etc/resolv.conf; замер времени | Полное имя с точкой на конце |
| 3.10 | Сервисы видят друг друга, снаружи недоступны | Сеть internal: true | docker network inspect ИМЯ --format '{{.Internal}}' | Так задумано; публиковать через прокси |
| 3.11 | После пересоздания сети container'ы потеряли связь | Container'ы остались в старой сети | docker inspect --format '{{json .NetworkSettings.Networks}}' | Пересоздать container'ы |
Развёрнуто: сервис недоступен
Лестница из семи шагов (8.6), от процесса к внешнему миру:
1. Процесс жив? docker top ИМЯ
2. Слушает порт? sidecar: ss -tlnp
3. На 0.0.0.0, не 127.0.0.1? там же
4. Отвечает изнутри? docker exec ИМЯ curl -s localhost:ПОРТ
5. Отвечает из соседа? sidecar --network container: → curl
6. Порт опубликован? docker port ИМЯ
7. Отвечает с host? curl localhost:ОПУБЛИКОВАННЫЙ
Первый шаг, давший отрицательный ответ, указывает область. Проверять с конца бессмысленно: отказ на шаге 7 не говорит ничего о причине.
4. Storage
| # | Симптом | Вероятная причина | Проверка | Исправление |
|---|---|---|---|---|
| 4.1 | Данные исчезли после пересоздания container'а | Запись в файловую систему container'а | docker diff ИМЯ | Volume для данных (7.2) |
| 4.2 | Смонтированный каталог пуст | Bind mount закрыл содержимое образа | docker inspect --format '{{json .Mounts}}' | Bind mount всегда затеняет |
| 4.3 | Volume не заполнился данными образа | Автозаполнение произошло однажды | docker volume inspect; создан ли volume ранее | Пересоздать volume |
| 4.4 | Permission denied при записи в volume | Несовпадение UID | docker exec ИМЯ id; ls -ln каталога | Согласовать UID (7.5) |
| 4.5 | Permission denied при верных правах Unix | Метки SELinux | getenforce; ls -Z | Суффикс :z или :Z (12.5) |
| 4.6 | Слой container'а растёт | Приложение пишет мимо volume | docker ps -s; docker diff ИМЯ | Направить запись в volume |
| 4.7 | Диск заполнен, system df показывает немного | Логи (см. 2.5) или build cache | docker system df; du каталога Docker | По категории (13.4) |
| 4.8 | Удалили образ — место не освободилось | Общие слои используются другими | docker system df -v: UNIQUE SIZE | Ожидаемо; смотреть UNIQUE SIZE |
| 4.9 | RECLAIMABLE нулевой при множестве образов | Их удерживают остановленные container'ы | docker ps -a | Сначала container prune |
| 4.10 | Данные пропали после prune | docker volume prune или --volumes | Восстановления нет | Резервные копии (7.6) |
5. Ресурсы
| # | Симптом | Вероятная причина | Проверка | Исправление |
|---|---|---|---|---|
| 5.1 | Высокая задержка при низкой загрузке CPU | Throttling по квоте CFS | docker exec ИМЯ cat /sys/fs/cgroup/cpu.stat | Поднять --cpus или сгладить всплески |
| 5.2 | MEM USAGE близок к лимиту, приложение в порядке | Page cache в учёте cgroup | Сравнить anon из memory.stat с RSS | Норма (13.2) |
| 5.3 | Память растёт монотонно | Утечка или фрагментация | Сравнить anon с tracemalloc | По результату сравнения |
| 5.4 | can't start new thread | Исчерпан --pids-limit | docker exec ИМЯ cat /sys/fs/cgroup/pids.current /sys/fs/cgroup/pids.max | Поднять лимит или сократить потоки |
| 5.5 | Too many open files | Исчерпан RLIMIT_NOFILE | docker exec ИМЯ sh -c 'ls /proc/1/fd | wc -l' | --ulimit nofile; искать утечку |
| 5.6 | os.cpu_count() возвращает число ядер host | Python не видит --cpus | docker exec ИМЯ python -c "import os; print(os.cpu_count())" | Читать cpu.max (6.13) |
| 5.7 | Приложение медленное только под нагрузкой | Throttling (5.1) или блокирующий драйвер логов (2.7) | Обе проверки | По результату |
| 5.8 | OOM убил процесс, container продолжает работать | Убит не PID 1 | sudo dmesg -T | grep -i 'killed process' | Имя процесса — только в журнале ядра |
6. Сборка
| # | Симптом | Вероятная причина | Проверка | Исправление |
|---|---|---|---|---|
| 6.1 | Кэш не срабатывает при неизменном коде | Изменился слой выше | docker build --progress=plain; порядок COPY | Зависимости копировать до кода (5.5) |
| 6.2 | Сборка долгая, контекст большой | Нет .dockerignore | Строка transferring context в выводе | .dockerignore (5.3) |
| 6.3 | Секрет виден в готовом образе | ARG или ENV вместо secret mount | docker history --no-trunc ОБРАЗ | RUN --mount=type=secret (12.6) |
| 6.4 | COPY не находит файл | Файл вне контекста сборки | Путь относительно контекста, не Dockerfile | Перенести или сменить контекст |
| 6.5 | Стадия multi-stage не выполняется | BuildKit пропускает недостижимые от цели | --target; граф зависимостей | Ожидаемо; явная зависимость |
| 6.6 | Образ больше ожидаемого | Кэш пакетов остался в слое | docker history ОБРАЗ | Очистка в той же инструкции RUN |
| 6.7 | Build cache занял десятки гигабайт | Нет GC | docker system df | builder.gc в daemon.json (13.4) |
| 6.8 | Сборка воспроизводится по-разному | Базовый образ по тегу, не по digest | docker inspect ОБРАЗ --format '{{index .RepoDigests 0}}' | Закрепить digest (11.6) |
| 6.9 | pip install каждый раз скачивает заново | Нет cache mount | Вывод сборки | RUN --mount=type=cache,target=/root/.cache/pip |
Быстрый справочник команд
| Задача | Команда |
|---|---|
| Состояние и код выхода | docker inspect ИМЯ --format '{{.State.Status}} {{.State.ExitCode}} {{.State.OOMKilled}}' |
| Вывод проверки здоровья | docker inspect ИМЯ --format '{{json .State.Health}}' | python3 -m json.tool |
| Последовательность событий | docker events --since 30m --until 0s --filter container=ИМЯ --format '{{.Time}} {{.Action}}' |
| Что записал container | docker diff ИМЯ |
| Размер writable layer | docker ps -s |
| Файл из образа без shell | docker cp ИМЯ:/путь - | tar -xO |
| Сеть из соседа | docker run --rm --network container:ИМЯ nicolaka/netshoot ss -tlnp |
| Утилита host в сети цели | sudo nsenter -t $(docker inspect ИМЯ --format '{{.State.Pid}}') -n -- ss -tlnp |
| Throttling CPU | docker exec ИМЯ cat /sys/fs/cgroup/cpu.stat |
| Память по составу | docker exec ИМЯ cat /sys/fs/cgroup/memory.stat | head -5 |
| Логи daemon | sudo journalctl -u docker.service --since '10 min ago' -p err |
| След OOM в ядре | sudo dmesg -T | grep -i 'killed process' |
| Расход диска по категориям | docker system df -v |
Логи, не видные в system df | sudo du -sh /var/lib/docker/containers/*/ | sort -h | tail |
Практическое упражнение
Задание. Соберите инструмент, проходящий методику из пяти вопросов автоматически.
Требования:
- Реализовать пять шагов методики в правильном порядке, с остановкой на первом отрицательном ответе.
- Для каждого исхода выдавать раздел таблицы и конкретную следующую команду.
- Проверить инструмент на четырёх разных неисправностях.
- Показать, что одинаковый симптом (код
137) приводит к разным диагнозам. - Показать, что инструмент не даёт ложного диагноза на исправном сервисе.
- Различать «проверка не выполнена» и «проверка пройдена».
Подсказки
Подсказка 1
Шаг 5 требует выполнить запрос изнутри container'а. В образе может не быть curl — используйте Python или sidecar.
Подсказка 2
Для пункта 4 нужны два container'а с кодом 137: один убитый по OOM, другой — docker kill.
Подсказка 3
Инструмент должен останавливаться на первом отрицательном ответе — иначе он выдаст несколько противоречивых диагнозов.
Решение
Показать решение
mkdir -p /tmp/diaglab && cd /tmp/diaglab
# ─── Исправный сервис ─────────────────────────────────────────────────
cat > good.py <<'PY'
"""Исправный сервис: отвечает на / и на /health."""
import http.server, json, os, threading, time
class H(http.server.BaseHTTPRequestHandler):
def do_GET(self):
body = json.dumps({"ок": True, "pid": os.getpid()}, ensure_ascii=False).encode()
self.send_response(200)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *a): pass
print(f"сервис готов, pid={os.getpid()}", flush=True)
threading.Thread(
target=http.server.ThreadingHTTPServer(("0.0.0.0", 8000), H).serve_forever,
daemon=True).start()
while True:
time.sleep(1)
PY
# ─── Сервис, слушающий только localhost ───────────────────────────────
cat > loopback.py <<'PY'
"""Ошибка 3.1: привязка к 127.0.0.1 вместо 0.0.0.0."""
import http.server, os, threading, time
class H(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200); self.end_headers(); self.wfile.write(b"ok")
def log_message(self, *a): pass
print(f"слушаю 127.0.0.1:8000, pid={os.getpid()}", flush=True)
threading.Thread(
target=http.server.ThreadingHTTPServer(("127.0.0.1", 8000), H).serve_forever,
daemon=True).start()
while True:
time.sleep(1)
PY
# ─── Сервис, падающий при старте ──────────────────────────────────────
cat > crashing.py <<'PY'
"""Ошибка 1.8: падение при инициализации."""
import sys, time
print("инициализация...", flush=True)
time.sleep(1)
print("ОШИБКА: не удалось подключиться к базе данных", file=sys.stderr, flush=True)
sys.exit(1)
PY
# ─── Потребитель памяти ───────────────────────────────────────────────
cat > hungry.py <<'PY'
"""Ошибка 1.2: исчерпание лимита памяти."""
import time
blocks = []
n = 0
while True:
blocks.append(bytearray(4 * 1024 * 1024))
blocks[-1][::4096] = b"\x01" * (len(blocks[-1]) // 4096)
n += 4
print(f"выделено {n} МиБ", flush=True)
time.sleep(0.15)
PY
# ─── Инструмент диагностики по методике ───────────────────────────────
cat > triage.sh <<'SH'
#!/usr/bin/env bash
# Проходит методику из пяти вопросов, останавливаясь на первом
# отрицательном ответе. Выдаёт раздел таблицы и следующую команду.
set -uo pipefail
TARGET="${1:?укажите container}"
PORT="${2:-8000}"
verdict() {
printf '\n ДИАГНОЗ: %s\n' "$1"
printf ' РАЗДЕЛ: %s\n' "$2"
printf ' ДАЛЬШЕ: %s\n\n' "$3"
}
printf '\n Диагностика: %s\n\n' "$TARGET"
# ── Шаг 1: создан ли container ────────────────────────────────────────
if ! docker inspect "$TARGET" > /dev/null 2>&1; then
printf ' [1] container создан? НЕТ\n'
verdict "container не создан — ошибка на стороне daemon" \
"1. Запуск и остановка, запись 1.1" \
"sudo journalctl -u docker.service --since '5 min ago' -p err"
exit 1
fi
printf ' [1] container создан? да\n'
# ── Шаг 2: работает или завершился ────────────────────────────────────
read -r status code oom restarts <<EOF
$(docker inspect "$TARGET" --format '{{.State.Status}} {{.State.ExitCode}} {{.State.OOMKilled}} {{.RestartCount}}')
EOF
printf ' [2] статус? %s (код %s, перезапусков %s)\n' "$status" "$code" "$restarts"
if [ "$status" != "running" ]; then
if [ "$oom" = "true" ]; then
verdict "убит по нехватке памяти в cgroup" \
"1.2 и раздел 5. Ресурсы" \
"docker exec/логи: найти причину роста; поднять --memory"
exit 2
fi
case "$code" in
137)
stops="$(docker events --since 30m --until 0s --filter "container=$TARGET" \
--format '{{.Action}}' 2>/dev/null | grep -c '^stop$' || echo 0)"
if [ "${stops:-0}" -gt 0 ]; then
verdict "не завершился по SIGTERM за отведённое время" \
"1.10 и 1.11" \
"проверить обработчик SIGTERM и форму exec в CMD"
else
verdict "SIGKILL извне; возможен OOM на уровне host" \
"1.3" \
"sudo dmesg -T | grep -i 'killed process'"
fi
exit 2 ;;
143) verdict "штатное завершение по SIGTERM" "1.4" "норма"; exit 0 ;;
126) verdict "файл найден, но не исполняемый" "1.5" \
"docker run --rm ОБРАЗ ls -l /путь"; exit 2 ;;
127) verdict "команда не найдена" "1.6" \
"docker run --rm ОБРАЗ which КОМАНДА"; exit 2 ;;
0) verdict "процесс отработал и вышел" "1.12" \
"главный процесс должен работать в переднем плане"; exit 0 ;;
*) verdict "завершился с кодом $code — ошибка приложения" \
"1. Запуск и остановка" \
"docker logs --tail 50 $TARGET"; exit 2 ;;
esac
fi
# ── Шаг 3: что вывело приложение ──────────────────────────────────────
loglines="$(docker logs "$TARGET" 2>&1 | grep -c . || echo 0)"
printf ' [3] строк в логах? %s\n' "$loglines"
if [ "${loglines:-0}" -eq 0 ]; then
unbuf="$(docker inspect "$TARGET" --format '{{range .Config.Env}}{{println .}}{{end}}' \
| grep -c PYTHONUNBUFFERED || echo 0)"
drv="$(docker inspect "$TARGET" --format '{{.HostConfig.LogConfig.Type}}')"
if [ "${unbuf:-0}" -eq 0 ]; then
verdict "логи пусты: вероятна буферизация stdout" "2.1" \
"перезапустить с PYTHONUNBUFFERED=1"
else
verdict "логи пусты при заданном PYTHONUNBUFFERED; драйвер: $drv" "2.4" \
"docker inspect --format '{{.HostConfig.LogConfig.Type}}'"
fi
exit 1
fi
if docker logs "$TARGET" 2>&1 | grep -qE 'Traceback|Error|ОШИБКА|Exception'; then
printf ' в логах есть признаки ошибки:\n'
docker logs "$TARGET" 2>&1 | grep -E 'Traceback|Error|ОШИБКА|Exception' \
| tail -2 | sed 's/^/ /'
fi
# ── Шаг 4: проверка здоровья ──────────────────────────────────────────
health="$(docker inspect "$TARGET" --format \
'{{if .State.Health}}{{.State.Health.Status}}{{else}}нет{{end}}')"
printf ' [4] проверка здоровья? %s\n' "$health"
if [ "$health" = "unhealthy" ]; then
printf ' последний вывод проверки:\n'
docker inspect "$TARGET" --format '{{json .State.Health.Log}}' \
| python3 -c "
import json, sys
log = json.load(sys.stdin) or []
if log:
e = log[-1]
print(' код=' + str(e['ExitCode']) + ' ' + e['Output'].strip()[:60])
" 2>/dev/null
verdict "проверка здоровья не проходит" "1.8 и 3. Сеть" \
"причина — в выводе выше; проверить зависимости сервиса"
exit 2
fi
# ── Шаг 5: отвечает ли изнутри ────────────────────────────────────────
inside="$(docker exec "$TARGET" python -c "
import socket, sys
s = socket.socket(); s.settimeout(3)
try:
s.connect(('127.0.0.1', $PORT)); print('да')
except OSError as e:
print('нет:' + (e.strerror or str(e)))
finally:
s.close()
" 2>/dev/null)"
if [ -z "$inside" ]; then
printf ' [5] отвечает изнутри? ПРОВЕРКА НЕ ВЫПОЛНЕНА (нет python в образе)\n'
verdict "шаг 5 не выполнен — инструмент недоступен в образе" \
"13.5. Глубокая диагностика" \
"sidecar: docker run --rm --network container:$TARGET ОБРАЗ ..."
exit 1
fi
printf ' [5] отвечает изнутри? %s\n' "$inside"
case "$inside" in
да)
# Отвечает внутри — проверяем снаружи
published="$(docker port "$TARGET" "$PORT" 2>/dev/null | head -1)"
if [ -z "$published" ]; then
verdict "работает внутри, порт не опубликован" "3. Сеть" \
"docker run -p ХОСТ:$PORT ... или ports в compose.yaml"
exit 1
fi
hostport="${published##*:}"
outside="$(python3 -c "
import socket
s = socket.socket(); s.settimeout(3)
try:
s.connect(('127.0.0.1', $hostport)); print('да')
except OSError as e:
print('нет:' + (e.strerror or str(e)))
finally:
s.close()
" 2>/dev/null)"
printf ' [6] отвечает с host? %s\n' "$outside"
if [ "$outside" = "да" ]; then
verdict "сервис работает и доступен снаружи" "—" "проблем не обнаружено"
exit 0
fi
verdict "работает внутри, недоступен снаружи" "3.1 и 3.6" \
"проверить привязку: слушает ли 0.0.0.0, а не 127.0.0.1"
exit 1 ;;
*)
listen="$(docker exec "$TARGET" python -c "
import socket
s = socket.socket(); s.settimeout(2)
try:
s.bind(('0.0.0.0', $PORT)); print('порт свободен — приложение НЕ слушает')
except OSError:
print('порт занят — приложение слушает, но не на 0.0.0.0')
finally:
s.close()
" 2>/dev/null)"
printf ' уточнение: %s\n' "$listen"
case "$listen" in
*"не на 0.0.0.0"*)
verdict "приложение привязано к 127.0.0.1" "3.1" \
"слушать 0.0.0.0 вместо 127.0.0.1" ;;
*)
verdict "приложение не слушает порт $PORT" "1.9 и 3. Сеть" \
"docker top $TARGET; проверить, что процесс дошёл до listen" ;;
esac
exit 1 ;;
esac
SH
chmod +x triage.sh
fail=0
ok() { printf ' ✓ %s\n' "$1"; }
bad() { printf ' ✗ %s\n' "$1"; fail=1; }
printf '\n═══ Подготовка: четыре неисправности и один исправный ═══\n'
docker run -d --name t-good -e PYTHONUNBUFFERED=1 -p 18100:8000 \
-v "$PWD/good.py:/a.py:ro" python:3.13-slim python /a.py > /dev/null
docker run -d --name t-loopback -e PYTHONUNBUFFERED=1 -p 18101:8000 \
-v "$PWD/loopback.py:/a.py:ro" python:3.13-slim python /a.py > /dev/null
docker run -d --name t-crash -e PYTHONUNBUFFERED=1 \
-v "$PWD/crashing.py:/a.py:ro" python:3.13-slim python /a.py > /dev/null
docker run -d --name t-oom --memory 64m --memory-swap 64m -e PYTHONUNBUFFERED=1 \
-v "$PWD/hungry.py:/a.py:ro" python:3.13-slim python /a.py > /dev/null
docker run -d --name t-killed python:3.13-slim sleep 300 > /dev/null
sleep 3 && docker kill t-killed > /dev/null
sleep 12
printf ' пять container'"'"'ов созданы\n'
printf '\n═══ Требования 1-3: методика на четырёх неисправностях ═══\n'
printf '\n ── упавший при старте ──'
./triage.sh t-crash; rc_crash=$?
printf ' ── исчерпал память ──'
./triage.sh t-oom; rc_oom=$?
printf ' ── привязан к 127.0.0.1 ──'
./triage.sh t-loopback; rc_loop=$?
printf ' ── убит командой kill ──'
./triage.sh t-killed; rc_kill=$?
d_crash="$(./triage.sh t-crash | grep 'ДИАГНОЗ' | sed 's/.*ДИАГНОЗ: *//')"
d_oom="$(./triage.sh t-oom | grep 'ДИАГНОЗ' | sed 's/.*ДИАГНОЗ: *//')"
d_loop="$(./triage.sh t-loopback | grep 'ДИАГНОЗ' | sed 's/.*ДИАГНОЗ: *//')"
d_kill="$(./triage.sh t-killed | grep 'ДИАГНОЗ' | sed 's/.*ДИАГНОЗ: *//')"
uniq_count="$(printf '%s\n%s\n%s\n%s\n' "$d_crash" "$d_oom" "$d_loop" "$d_kill" \
| sort -u | grep -c .)"
printf '\n различных диагнозов на четыре неисправности: %s\n' "$uniq_count"
[ "$uniq_count" -eq 4 ] \
&& ok "каждая неисправность получила свой диагноз и раздел таблицы" \
|| bad "диагнозов: $uniq_count из 4"
printf '\n═══ Требование 4: один код 137 — разные диагнозы ═══\n'
printf ' %-14s %-6s %-11s %s\n' "container" "код" "OOMKilled" "диагноз"
printf ' %s\n' "──────────────────────────────────────────────────────────────────────────"
for c in t-oom t-killed; do
info="$(docker inspect "$c" --format '{{.State.ExitCode}}|{{.State.OOMKilled}}')"
d="$(./triage.sh "$c" | grep 'ДИАГНОЗ' | sed 's/.*ДИАГНОЗ: *//')"
printf ' %-14s %-6s %-11s %s\n' "$c" "${info%%|*}" "${info##*|}" "${d:0:44}"
done
[ "$d_oom" != "$d_kill" ] \
&& ok "одинаковый код 137 разведён на два диагноза" \
|| bad "диагнозы совпали"
printf '\n═══ Требование 5: исправный сервис ═══\n'
./triage.sh t-good; rc_good=$?
d_good="$(./triage.sh t-good | grep 'ДИАГНОЗ' | sed 's/.*ДИАГНОЗ: *//')"
printf ' код возврата: %s\n' "$rc_good"
case "$d_good" in
*"работает и доступен"*) ok "на исправном сервисе ложного диагноза нет (код $rc_good)" ;;
*) bad "ложный диагноз: $d_good" ;;
esac
printf '\n═══ Требование 6: непроверенное отличается от пройденного ═══\n'
docker run -d --name t-noshell --entrypoint sleep alpine:3.21 300 > /dev/null
sleep 2
noshell_out="$(./triage.sh t-noshell 2>&1)"
echo "$noshell_out" | grep -E '\[5\]|ДИАГНОЗ' | sed 's/^/ /'
case "$noshell_out" in
*"ПРОВЕРКА НЕ ВЫПОЛНЕНА"*)
ok "шаг, который не удалось выполнить, помечен явно, а не засчитан" ;;
*) bad "невыполненный шаг не отмечен" ;;
esac
docker rm -f t-noshell > /dev/null 2>&1
printf '\n═══ Проверка остановки на первом отрицательном ответе ═══\n'
steps_crash="$(./triage.sh t-crash | grep -c '^ \[[0-9]\]')"
steps_good="$(./triage.sh t-good | grep -c '^ \[[0-9]\]')"
printf ' шагов пройдено на упавшем: %s\n' "$steps_crash"
printf ' шагов пройдено на исправном: %s\n' "$steps_good"
[ "$steps_crash" -lt "$steps_good" ] \
&& ok "инструмент останавливается на первом отрицательном ответе" \
|| bad "шагов: $steps_crash и $steps_good"
printf '\n═══ ИТОГ ═══\n'
[ "$fail" -eq 0 ] && echo " все требования выполнены" || echo " ЕСТЬ ПРОВАЛЫ"
docker rm -f t-good t-loopback t-crash t-oom t-killed > /dev/null 2>&1
cd /tmp && rm -rf /tmp/diaglab
exit "$fail"
Ожидаемый вывод:
═══ Подготовка: четыре неисправности и один исправный ═══
пять container'ов созданы
═══ Требования 1-3: методика на четырёх неисправностях ═══
── упавший при старте ──
Диагностика: t-crash
[1] container создан? да
[2] статус? exited (код 1, перезапусков 0)
ДИАГНОЗ: завершился с кодом 1 — ошибка приложения
РАЗДЕЛ: 1. Запуск и остановка
ДАЛЬШЕ: docker logs --tail 50 t-crash
── исчерпал память ──
Диагностика: t-oom
[1] container создан? да
[2] статус? exited (код 137, перезапусков 0)
ДИАГНОЗ: убит по нехватке памяти в cgroup
РАЗДЕЛ: 1.2 и раздел 5. Ресурсы
ДАЛЬШЕ: docker exec/логи: найти причину роста; поднять --memory
── привязан к 127.0.0.1 ──
Диагностика: t-loopback
[1] container создан? да
[2] статус? running (код 0, перезапусков 0)
[3] строк в логах? 1
[4] проверка здоровья? нет
[5] отвечает изнутри? да
[6] отвечает с host? нет:Connection refused
ДИАГНОЗ: работает внутри, недоступен снаружи
РАЗДЕЛ: 3.1 и 3.6
ДАЛЬШЕ: проверить привязку: слушает ли 0.0.0.0, а не 127.0.0.1
── убит командой kill ──
Диагностика: t-killed
[1] container создан? да
[2] статус? exited (код 137, перезапусков 0)
ДИАГНОЗ: SIGKILL извне; возможен OOM на уровне host
РАЗДЕЛ: 1.3
ДАЛЬШЕ: sudo dmesg -T | grep -i 'killed process'
различных диагнозов на четыре неисправности: 4
✓ каждая неисправность получила свой диагноз и раздел таблицы
═══ Требование 4: один код 137 — разные диагнозы ═══
container код OOMKilled диагноз
──────────────────────────────────────────────────────────────────────────
t-oom 137 true убит по нехватке памяти в cgroup
t-killed 137 false SIGKILL извне; возможен OOM на уровне ho
✓ одинаковый код 137 разведён на два диагноза
═══ Требование 5: исправный сервис ═══
Диагностика: t-good
[1] container создан? да
[2] статус? running (код 0, перезапусков 0)
[3] строк в логах? 1
[4] проверка здоровья? нет
[5] отвечает изнутри? да
[6] отвечает с host? да
ДИАГНОЗ: сервис работает и доступен снаружи
РАЗДЕЛ: —
ДАЛЬШЕ: проблем не обнаружено
код возврата: 0
✓ на исправном сервисе ложного диагноза нет (код 0)
═══ Требование 6: непроверенное отличается от пройденного ═══
[5] отвечает изнутри? ПРОВЕРКА НЕ ВЫПОЛНЕНА (нет python в образе)
ДИАГНОЗ: шаг 5 не выполнен — инструмент недоступен в образе
✓ шаг, который не удалось выполнить, помечен явно, а не засчитан
═══ Проверка остановки на первом отрицательном ответе ═══
шагов пройдено на упавшем: 2
шагов пройдено на исправном: 6
✓ инструмент останавливается на первом отрицательном ответе
═══ ИТОГ ═══
все требования выполнены
Все требования выполнены.
Строки требования 4 — суть методики. Код 137 в обоих случаях, OOMKilled различается — и диагнозы получаются разные. Без второго поля оба случая слились бы в один бесполезный вывод «убит сигналом».
Три решения, определяющие качество.
Инструмент останавливается на первом отрицательном ответе. Соблазн — выполнить все проверки и выдать полный отчёт. Но у упавшего container'а шаги 3–5 бессмысленны: логи есть, здоровья нет, exec невозможен. Полный отчёт выдал бы три «проблемы» вместо одной причины. Числа в конце — 2 шага против 6 — подтверждают, что остановка работает.
Каждый диагноз называет раздел таблицы, а не только причину. «Убит по нехватке памяти» — это ответ, но не следующее действие. Ссылка на запись 1.2 и раздел 5 даёт продолжение: там перечислены проверки, различающие утечку, фрагментацию и кэш. Инструмент передаёт эстафету справочнику, а не пытается заменить его.
Шаг 5 различает три исхода, а не два. «Отвечает» и «не отвечает» — недостаточно: третий случай, когда проверку не удалось выполнить (в образе нет python), при бинарной логике был бы засчитан как отказ и дал бы ложный диагноз о неработающем приложении. Отдельная ветка выводит на урок 13.5 с sidecar.
Чего решение не делает. Проверено четыре неисправности из тридцати в таблице; разделы «Storage» и «Сборка» инструментом не покрыты — они требуют контекста, которого у автоматической проверки нет (какие данные важны, какой размер образа ожидался). Шаг 5 использует python из образа приложения; для образов без него нужен sidecar, и решение об этом сообщает, но не реализует. Диагноз «возможен OOM на уровне host» не подтверждается: для этого нужен dmesg и права root. Наконец, инструмент не различает случай, когда причин несколько, — он находит первую и останавливается, что для диагностики правильно, но не полно.
Как пользоваться таблицей
| Ситуация | Действие |
|---|---|
| Симптом знаком | Найти запись, выполнить проверку из колонки «Проверка» |
| Симптом неясен | Пройти методику из пяти вопросов; она укажет раздел |
| Проверка подтвердила причину | Применить исправление, повторить проверку |
| Проверка не подтвердила | Вернуться к методике — гипотеза была неверна |
| Причин может быть несколько | Исправлять по одной, проверяя после каждой |
Последняя строка важна: одновременное исправление трёх вещей не даёт знания о том, какая из них была причиной.
Контрольные вопросы
- Пять вопросов методики — назовите по порядку и объясните, почему порядок именно такой.
- Код
137встречается в трёх записях таблицы. Чем они различаются? docker logsпуст. Две возможные причины и как их различить?- Приложение отвечает изнутри container'а, но не снаружи. Первая гипотеза?
- Диск заполнен,
docker system dfпоказывает немного. Где искать? - Restart loop со стабильным интервалом 40 секунд. О чём это говорит?
- Почему не следует исправлять несколько вещей одновременно?
Краткое резюме
- Методика из пяти вопросов сужает область до перехода к таблице.
- Не переходить к следующему шагу, не выполнив предыдущий.
- Одинаковый симптом имеет разные причины: код
137— три, пустые логи — две. - Container не создан — искать в логах daemon, а не в
docker logs. - Стабильный интервал restart loop указывает на таймаут, а не на случайный сбой.
docker logsприunhealthyне содержит причины — она в.State.Health.Log.- Работает изнутри, не работает снаружи — почти всегда привязка к
127.0.0.1. - Расхождение
docker system dfи размера каталога — это логи. - Проверка обязательна: без неё исправление становится угадыванием.
- Исправлять по одной причине за раз, проверяя после каждой.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
| Docker: troubleshooting | https://docs.docker.com/engine/daemon/troubleshoot/ | Диагностика daemon |
| Docker: container logs | https://docs.docker.com/engine/logging/ | Драйверы и их поведение |
| Docker: networking troubleshooting | https://docs.docker.com/engine/network/ | Сетевая модель |
| Docker: resource constraints | https://docs.docker.com/engine/containers/resource_constraints/ | Лимиты и их проявления |
| Docker: prune | https://docs.docker.com/engine/manage-resources/pruning/ | Очистка по категориям |
| Linux: exit codes и сигналы | https://man7.org/linux/man-pages/man7/signal.7.html | Соответствие 128 + N |
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий материал → Практические задания
Главное оглавление