Проект 3. Multi-service stack
Выполнять после: раздела 10
Опирается на разделы: 07, 08, 09, 10
Ориентировочное время: 12 часов
Оценивание: rubric, проходной результат 70
Задача
Собрать систему из пяти компонентов вокруг сервиса из проекта 2.
| Компонент | Роль |
|---|---|
api | HTTP-сервис из проекта 2 |
worker | Фоновая обработка накопленных событий |
migrate | Применение миграций схемы |
db | PostgreSQL |
cache | Redis |
Проект впервые требует проектировать систему, а не отдельный container. Отсюда и главная его особенность: большинство ошибок здесь — не в коде, а в порядке, зависимостях и границах.
Что переносится из проекта 2
Хранилище там было отделено протоколом Storage — ради именно этой замены. MemoryStorage уступает место PostgresStorage, а обработчики запросов не меняются вовсе. Если в проекте 2 интерфейс не выделен, начните с него: иначе перенос превратится в переписывание.
Требования
Обязательные
| № | Требование | Проверяется |
|---|---|---|
| 1 | Пять сервисов; роль каждого явно ограничена | Checklist 1 |
| 2 | Healthcheck у каждого сервиса, кроме одноразового | Checklist 2 |
| 3 | Зависимости по готовности, а не по запуску | Checklist 2 |
| 4 | Миграции применяются до старта api и worker | Checklist 3 |
| 5 | Повторный запуск миграций ничего не делает | Checklist 3 |
| 6 | Две сети; база и кэш недоступны снаружи | Checklist 4 |
| 7 | Именованные тома; данные переживают down | Checklist 5 |
| 8 | Ограничения ресурсов у всех сервисов | Checklist 6 |
| 9 | Интеграционные тесты с настоящей базой | Checklist 7 |
| 10 | Отдельные конфигурации для разработки и тестов | Checklist 8 |
| 11 | Секрет базы не лежит в compose.yaml | Checklist 9 |
Требования, невыполнение которых снимает работу
depends_onбез условия готовности — приложение стартует раньше базы;- пароль базы записан в
compose.yamlоткрытым текстом; - тесты «проходят» при отсутствующей базе, потому что молча пропускаются;
- данные теряются при
docker compose downбез-v.
Архитектура
Порядок старта
db (healthy) ─┐
├──→ migrate (completed) ──→ api
cache (healthy)┘ └─→ worker
Три разных условия, и каждое нужно:
| Условие | Где | Почему именно оно |
|---|---|---|
service_healthy | db, cache | Запущен ≠ принимает соединения |
service_completed_successfully | migrate | Схема должна существовать до первого запроса |
| — | db, cache | Ни от кого не зависят |
Ошибка, которую требование 3 предотвращает: depends_on в форме списка ждёт запуска container'а. PostgreSQL после старта container'а несколько секунд инициализирует кластер. Симптом плавающий: на прогретой машине успевает, на холодной нет.
Миграции как одноразовая задача
migrate — не сервис. У него restart: "no", нет healthcheck, и он обязан завершиться с кодом 0.
Три свойства, которые нужно обеспечить:
| Свойство | Зачем |
|---|---|
| Порядок задаётся именем файла | Обход каталога не гарантирует порядок |
| Повторный запуск ничего не делает | docker compose up вызывается многократно |
| Две копии не мешают друг другу | В оркестраторе задача может запуститься дважды |
Третье решается консультативной блокировкой (pg_advisory_lock). Без неё две копии применят одну миграцию дважды.
Границы сетей
хост ──→ [frontend] ──→ api
│
▼
[backend, internal: true]
db cache worker migrate
internal: true у backend означает, что container'ы этой сети не имеют выхода наружу. Сервис api состоит в обеих: он единственный, кто принимает запросы снаружи.
Очередь: два варианта, и выбор надо обосновать
Задача «обработать накопленные события» решается двумя способами:
| Способ | Плюс | Минус |
|---|---|---|
| Очередь в Redis | Отделена от базы, быстрая | Ещё одно место, где теряются задачи |
Очередь в самой базе (FOR UPDATE SKIP LOCKED) | Транзакция общая с данными | Нагрузка на базу |
Эталонное решение выбирает второй: SKIP LOCKED позволяет нескольким обработчикам брать разные строки, не ожидая друг друга и не обрабатывая строку дважды, а признак обработки лежит в той же транзакции, что и сами данные. Redis остаётся для кэша.
Выбор обратный тоже обоснован — но обосновать его нужно письменно.
Пошаговый план
Шаг 1. Миграции
Раннер и три файла схемы. Проверить: порядок, повторный запуск, изменение уже применённого файла.
Опора: урок 9.4.
Шаг 2. Хранилище в PostgreSQL
Реализовать протокол Storage поверх пула соединений. Имя колонки в GROUP BY подставляется в SQL — нужен белый список.
Опора: урок 7.2.
Шаг 3. Обработчик
Цикл с пачками, реакция на SIGTERM, дорабатывание текущей пачки.
Шаг 4. Compose
Пять сервисов, две сети, тома, ограничения, три вида зависимостей.
Опора: урок 9.7.
Шаг 5. Конфигурации разработки и тестов
Отдельные файлы, а не профили: набор отличий велик.
Опора: урок 9.6.
Шаг 6. Интеграционные тесты
С настоящей базой. Отдельно решить, что делает набор тестов, если базы нет.
Опора: урок 15.3.
Критерии завершения
docker compose up -dиз чистого состояния десять раз подряд — без единого паденияapi.docker compose run --rm migrateвторой раз печатает «новых миграций нет».docker compose exec api python -c "import socket; socket.create_connection(('db',5432),2)"работает, а с хоста подключение к базе не проходит.- Запись,
docker compose down,up, чтение — данные на месте. docker compose down -v,up— данных нет, схема создана заново.docker compose -f compose.yaml -f compose.test.yaml run --rm testsпроходит.- Тот же прогон без базы завершается с ошибкой, а не с «пропущено».
grep -ri password compose.yamlне находит значения.
Седьмой пункт — самый содержательный. Разберите его прежде, чем считать проект законченным.
Дополнительные задания
| № | Задание | Что добавляет |
|---|---|---|
| 1 | Блокировка при миграциях; проверить двумя копиями | Поведение в оркестраторе |
| 2 | Отказ при изменении уже применённой миграции | Расхождение схем между средами |
| 3 | Проверить, что два обработчика не берут одни строки | SKIP LOCKED на практике |
| 4 | Переключатель «база обязательна» для конвейера | «Не проверено» ≠ «проверено» |
| 5 | Резервное копирование тома и проверка восстановления | Урок 7.6 |
| 6 | Замерить время старта стека из чистого состояния | Цена условий готовности |
Задания 1–4 реализованы в эталонном решении; 5 и 6 — нет.
Навигация
Список проверок →
Эталонное решение → — открывать после собственной попытки
← Предыдущий проект: FastAPI service
Вернуться к проектам
Главное оглавление