Главная/Проверка знаний/Проверка знаний

Checkpoints

Четыре контрольные точки после логических блоков курса. Каждая состоит из трёх частей: диагностическая задача, самостоятельное задание и критерии приёмки.

Checkpoint отличается от exercises.md раздела тем, что не подсказывает, какой материал применить. В упражнении раздела 08 понятно, что задача про сеть. В checkpoint дан симптом, и часть работы — определить, где искать.

Правило приёмки: checkpoint не пройден, пока не выполнены все критерии. Частичное выполнение не засчитывается — частично работающий сервис в эксплуатации равен неработающему.

Правило честности: диагностическая задача решается до чтения разбора. Прочитанный разбор даёт знание конкретной поломки, но не навык расследования, а проверяется именно он.

Docker на машине, где готовился курс, не установлен. Конфигурации ниже написаны так, чтобы воспроизводить описанный симптом, но фактически не запускались (README). Разборы опираются на механизмы, разобранные в соответствующих уроках.


Checkpoint 1: Container как процесс

После разделов: 0104
Время: 2 часа
Проверяет: понимание того, что container — это процесс, а не машина

Часть A. Диагностика

Дан образ, собранный из такого Dockerfile:

dockerfile
FROM python:3.13-slim
WORKDIR /app
COPY worker.py .
CMD python worker.py

worker.py:

python
import signal
import sys
import time


def on_term(signum, frame):
    print("получен SIGTERM, завершаюсь", flush=True)
    sys.exit(0)


signal.signal(signal.SIGTERM, on_term)
print("работаю", flush=True)
while True:
    time.sleep(1)

Симптомы:

console
$ docker run -d --name w worker
a1b2c3d4e5f6

$ docker logs w
работаю

$ time docker stop w
w

real    0m10.412s

Остановка занимает ровно десять секунд. Строка «получен SIGTERM» в логах не появляется — при том, что обработчик написан и заведомо работает при запуске скрипта напрямую.

Задача. Назвать причину, подтвердить её командой и исправить.

Подсказка (открывать после 20 минут)

Вопрос, с которого стоит начать: какой процесс имеет PID 1 внутри container'а?

bash
docker exec w ps -eo pid,ppid,cmd
Разбор

Причина. CMD записан в shell-форме. Docker разворачивает её в /bin/sh -c "python worker.py", поэтому PID 1 — оболочка, а Python работает её дочерним процессом.

docker stop посылает SIGTERM процессу с PID 1, то есть оболочке. sh не пересылает сигнал потомкам и сам по SIGTERM не завершается, пока ждёт дочерний процесс. Через grace period (по умолчанию 10 секунд) приходит SIGKILL, который убивает всё разом — отсюда ровно десять секунд.

Подтверждение:

bash
docker exec w ps -eo pid,ppid,cmd
text
  PID  PPID CMD
    1     0 /bin/sh -c python worker.py
    7     1 python worker.py

PID 1 — оболочка. Это и есть ответ.

Исправление — exec-форма:

dockerfile
CMD ["python", "worker.py"]
text
  PID  PPID CMD
    1     0 python worker.py

Проверка исправления:

bash
time docker stop w

Меньше секунды, и в логах появилась строка обработчика.

Что здесь легко перепутать. Симптом «десять секунд» иногда приписывают отсутствию обработчика сигнала. Обработчик есть — проблема в том, что сигнал до него не доходит. Различить два случая позволяет только проверка PID 1: при отсутствии обработчика PID 1 был бы самим Python.

Разбор: урок 4.6, урок 5.5.

Часть B. Задание

Собрать образ инструмента, который:

  1. принимает аргументы через docker run;
  2. читает стандартный ввод;
  3. возвращает три различимых кода возврата;
  4. корректно завершается по docker stop менее чем за секунду;
  5. печатает результат в stdout, диагностику в stderr.

Приложение может быть любым — достаточно тридцати строк Python.

Критерии приёмки

КритерийПроверка
1PID 1 — само приложениеdocker exec + ps, первая строка не sh
2Аргументы доходятdocker run образ --флаг меняет поведение
3Стандартный ввод работаетecho данные | docker run -i образ
4Три кода возврата различимыТри запуска, три разных $?
5Остановка быстрее секундыtime docker stop
6Потоки разделены2>/dev/null и 2>&1 >/dev/null дают разное

Шесть из шести — checkpoint пройден. Пять из шести — не пройден.


Checkpoint 2: Сборка и многокомпонентная система

После разделов: 0509
Время: 3 часа
Проверяет: оптимизацию сборки, сети, тома, зависимости по готовности

Часть A. Диагностика

Дан compose.yaml:

yaml
services:
  api:
    build: .
    ports:
      - "8080:8000"
    environment:
      DATABASE_URL: postgresql://app:secret@localhost:5432/appdb
    depends_on:
      - db

  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret

Симптомы. docker compose up запускает оба сервиса, db работает, а api перезапускается с ошибкой:

text
api-1  | connection to server at "localhost" (127.0.0.1), port 5432 failed:
api-1  | Connection refused
api-1  |     Is the server running on that host and accepting connections?

При этом docker compose exec api ping db отвечает.

Задача. Найти две независимые ошибки. Симптом виден один, причин две, и исправление только одной оставит проблему.

Подсказка (открывать после 20 минут)

Первая ошибка видна в самой строке подключения. Вторая проявится, когда первая будет исправлена, — и только на медленной машине или при холодном старте.

Разбор

Ошибка 1: localhost вместо имени сервиса.

Каждый container имеет собственный network namespace (урок 17.3). localhost внутри api указывает на сам api, а не на хост и не на db.

Подтверждение — ping db работает, а подключение к localhost:5432 нет: значит, разрешение имён исправно, а адрес указан неверный.

Исправление:

yaml
DATABASE_URL: postgresql://app:secret@db:5432/appdb

Ошибка 2: depends_on без условия готовности.

depends_on в форме списка ждёт запуска container'а, а не готовности процесса внутри. PostgreSQL после старта container'а ещё несколько секунд инициализирует кластер и не принимает соединения.

После исправления первой ошибки симптом станет плавающим: на прогретой машине успевает, на холодной — нет. Плавающие ошибки дороже воспроизводимых.

Исправление:

yaml
services:
  api:
    depends_on:
      db:
        condition: service_healthy

  db:
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 10s

Что остаётся неисправленным и это правильно заметить. Даже с condition: service_healthy приложение обязано переживать отказ базы в работе, а не только при старте. depends_on решает задачу первой минуты; повторы подключения в коде решают её всегда (урок 18.3).

Третья проблема, не входящая в задачу, но заметная: пароль записан в compose.yaml открытым текстом. Симптомов не даёт, найтись должна (урок 12.6).

Разбор: урок 8.4, урок 9.3.

Часть B. Задание

Собрать стек из трёх сервисов: приложение, база данных, кэш.

Требования:

  1. Multi-stage сборка приложения; инструментов сборки в итоговом образе нет.
  2. Изменение кода приложения не пересобирает зависимости.
  3. Две сети: база и кэш недоступны снаружи.
  4. Именованный том для данных базы.
  5. Healthcheck у каждого сервиса; зависимости по готовности.
  6. Приложение переживает перезапуск базы без перезапуска себя.

Критерии приёмки

КритерийПроверка
1Кэш сборки работаетtouch файла в src/, пересборка занимает секунды
2Компилятора нет в образеdocker run --rm образ which gcc не находит
3.dockerignore исключает .git и кэшиРазмер контекста сборки
4База недоступна с хостаПодключение к порту базы снаружи не проходит
5Приложение доступно с хостаcurl localhost:порт отвечает
6Данные переживают docker compose downЗапись, down, up, чтение
7Порядок старта соблюдаетсяdocker compose up из чистого состояния десять раз подряд
8Приложение переживает docker compose restart dbЗапрос после перезапуска проходит

Восемь из восьми — checkpoint пройден.

Про критерий 7. Один успешный запуск ничего не доказывает: гонка проявляется не всегда. Десять запусков подряд — минимальная проверка воспроизводимости.


Checkpoint 3: Production и диагностика

После разделов: 1013
Время: 3 часа
Проверяет: готовность к эксплуатации и умение расследовать

Часть A. Диагностика

Симптом. Сервис работает часами, затем исчезает. В docker logs — ничего необычного: последняя строка выглядит как обычный обработанный запрос. Перезапуск помогает на несколько часов.

console
$ docker inspect api --format '{{.State.Status}} {{.State.ExitCode}} {{.State.OOMKilled}}'
exited 137 true

Задача. Объяснить, что произошло, почему в логах приложения ничего нет, и назвать, какие данные нужны, чтобы предсказать это заранее.

Подсказка (открывать после 20 минут)

Код 137 — это 128 + 9. Кто послал сигнал 9 и почему приложение не могло об этом сообщить?

Разбор

Что произошло. Процесс превысил ограничение памяти cgroup, и ядро завершило его через OOM killer. SIGKILL не перехватывается — обработчика для него не существует в принципе, поэтому приложение физически не могло записать ничего в лог.

Код 137 = 128 + 9, где 9 — номер SIGKILL. Поле OOMKilled: true подтверждает причину.

Почему в логах приложения пусто. Запись делает ядро, а не процесс. Она видна:

bash
dmesg | grep -i 'killed process'
docker events --filter 'event=oom' --since 24h

Отсюда общее правило: docker logs показывает то, что сказал процесс. То, что сделали с процессом, приходится узнавать в другом месте (урок 13.5).

Как предсказать заранее. Нужны данные о давлении памяти до отказа:

bash
docker exec api cat /sys/fs/cgroup/memory.events
text
low 0
high 0
max 4213
oom 12
oom_kill 3

Ненулевой max означает, что процесс уже упирался в предел и его тормозили; oom_kill — сколько раз убивали. Эти счётчики растут задолго до того, как отказ станет заметным (урок 17.4).

Docker задаёт только memory.max, но не memory.high — то есть торможения перед отказом нет: процесс работает нормально и убивается мгновенно. У systemd для этого есть MemoryHigh (урок 19.1).

Что делать дальше — три разных ответа, и выбор между ними требует данных:

ГипотезаКак отличить
Утечка памяти в приложенииПотребление растёт монотонно между перезапусками
Лимит заниженПотребление выходит на плато выше лимита
Всплеск на редком запросеПотребление стабильно, скачок перед отказом

Поднять лимит без различения этих случаев — обычная ошибка: при утечке это отодвигает отказ, не устраняя его.

Разбор: урок 11.4, урок 13.3.

Часть B. Задание

Привести сервис из checkpoint 2 в состояние, пригодное для эксплуатации:

  1. Три пробы с разной логикой: жив, готов, инициализирован.
  2. Структурированные логи в stdout с идентификатором запроса.
  3. Ограничения памяти и процессора, подобранные по измерению, а не наугад.
  4. Ротация логов.
  5. Мягкое завершение: активные запросы дорабатываются.
  6. Раздел README с методикой диагностики трёх типовых отказов.

Критерии приёмки

КритерийПроверка
1/healthz не зависит от базыОстановить базу — отвечает 200
2/readyz зависит от базыОстановить базу — отвечает 503
3Логи разбираются как JSONdocker logs пропустить через json.tool
4Идентификатор запроса проходит через все записиОдин запрос — один идентификатор во всех строках
5Лимиты заданы и обоснованы измерениемЧисло в README со ссылкой на замер
6Ротация логов настроенаdocker inspect показывает max-size
7Активный запрос доживает до конца при остановкеЗапрос на 3 секунды + docker stop
8memory.events читается изнутриКоманда в README работает

Восемь из восьми — checkpoint пройден.

Про критерии 1 и 2. Это единственная пара, которую проверяют вместе: если обе пробы ведут себя одинаково, разделения нет, сколько бы эндпоинтов ни было объявлено.


Checkpoint 4: Доставка

После разделов: 1416
Время: 3 часа
Проверяет: воспроизводимость доставки от коммита до образа

Часть A. Диагностика

Симптом. В эксплуатации работает не тот код, который ожидали. Развёртывание выполнено командой:

bash
docker pull registry.example.com/team/api:prod
docker compose up -d

docker compose ps показывает container запущенным, образ — team/api:prod. Разработчик утверждает, что исправление в prod собрано и опубликовано час назад.

console
$ docker image inspect team/api:prod --format '{{.Id}}'
sha256:9f8e7d6c...

$ docker image inspect team/api:prod --format '{{index .RepoDigests 0}}'
registry.example.com/team/api@sha256:1a2b3c4d...

Задача. Назвать не менее трёх мест, где могло разойтись, и предложить изменение, устраняющее весь класс.

Подсказка (открывать после 20 минут)

Тег — изменяемая ссылка. Сколько шагов между «собрали» и «запущено», и на каком из них тег мог указывать на разное?

Разбор

Три места расхождения.

Первое: pull выполнен, но container не пересоздан. docker compose up -d пересоздаёт container, только если изменилась конфигурация сервиса. Само по себе обновление образа с тем же тегом изменением конфигурации не является — работает старый container со старым образом.

Подтверждение:

bash
docker inspect container --format '{{.Image}}'
docker image inspect team/api:prod --format '{{.Id}}'

Разные значения означают, что container работает не на том образе, который сейчас помечен тегом.

Второе: тег перезаписан после pull. Между pull и up прошло время. Если сборка опубликовала prod повторно, локальная копия устарела мгновенно.

Третье: опубликовано не то, что собрано. Сборка пометила образ тегом prod до прохождения тестов, или два конвейера опубликовали prod одновременно — побеждает последний.

Изменение, устраняющее класс. Развёртывание по digest, а не по тегу:

yaml
services:
  api:
    image: registry.example.com/team/api@sha256:1a2b3c4d...

Digest вычисляется из содержимого: одно значение — один набор байтов, всегда (урок 14.3). Подменить его нельзя, устареть он не может.

Дополнительно: неизменяемые теги по SHA коммита (api:g1a2b3c4) вместо перезаписываемого prod. Тогда откат означает «развернуть предыдущий тег», а не «надеяться, что старый образ ещё не вытеснен из кэша».

Что при этом теряется. Развёртывание по digest требует шага, обновляющего digest в конфигурации, — это работа. Обычное возражение: «неудобно». Правильный ответ: неудобно ровно настолько, насколько удобно было не знать, что запущено.

Разбор: урок 14.3, урок 11.6.

Часть B. Задание

Построить конвейер доставки для стека из checkpoint 3:

  1. Проверки, тесты и сборка — по возрастанию времени и убыванию вероятности отказа.
  2. Логика в скриптах, а не в шагах YAML.
  3. Кэш сборки, переносимый между запусками.
  4. Сканирование с тремя кодами возврата: чисто, находки, не проверено.
  5. SBOM, приложенный к образу.
  6. Теги: SHA коммита и digest; перезаписываемых тегов нет.
  7. План отката с указанием, что именно откатывается.

Критерии приёмки

КритерийПроверка
1Быстрые проверки идут первымиСломать линтер — конвейер падает за минуту
2Скрипты запускаются локально./ci/проверка.sh работает без CI
3Кэш переносится между запускамиВторой запуск быстрее первого
4Отсутствие сканера отличимо от чистого результатаУбрать сканер — код возврата 3, не 0
5SBOM создан и содержит зависимостиЧисло записей больше нуля
6Образ помечен SHA коммитаТег соответствует git rev-parse --short HEAD
7Развёртывание по digestВ конфигурации @sha256:
8Откат описан командойКоманда, а не «развернуть предыдущую версию»

Восемь из восьми — checkpoint пройден.

Про критерий 4. Он проверяет то, что обычно не проверяют: поведение конвейера при отсутствии инструмента. Сканер, которого нет, не должен выглядеть как сканер, не нашедший проблем (урок 16.4).


Сводка

ТемаПосле разделовДиагностикаЗаданиеКритериев
1Container как процесс01–04Shell-форма и PID 1Инструмент командной строки6
2Сборка и система05–09localhost и порядок стартаСтек из трёх сервисов8
3Production и диагностика10–13OOM без записи в логеГотовность к эксплуатации8
4Доставка14–16Тег указывает не тудаКонвейер доставки8

Разделы 17–19 контрольных точек не имеют: они не добавляют новых навыков сборки и эксплуатации, а объясняют механизмы и границы применимости. Проверяются quiz'ами и заданиями самих разделов.

Что checkpoints проверяют, чего не проверяет остальное

Диагностические части всех четырёх устроены одинаково: симптом не указывает на раздел. Ошибка Connection refused в checkpoint 2 может быть про сеть, про порядок старта, про конфигурацию или про сам процесс — и половина работы состоит в том, чтобы это сузить.

Упражнения разделов такого дать не могут: они лежат внутри раздела, и область поиска известна заранее. В работе она неизвестна почти никогда.

Второе, общее для всех четырёх: в каждой диагностической задаче больше одной причины или больше одного правильного ответа. Checkpoint 2 содержит две независимые ошибки и третью, не дающую симптомов. Checkpoint 3 заканчивается тремя гипотезами, различить которые можно только измерением. Это ближе к тому, как выглядит расследование, чем задача с единственным ответом.


Навигация

Вернуться к системе проверки
Quizzes по разделам
Debugging challenges
Grading rubric
Главное оглавление

Markdown на GitHub ↗