Главная/Проекты/Проект

Проект 2. FastAPI service

Выполнять после: раздела 08
Опирается на разделы: 05, 06, 07, 08
Ориентировочное время: 8 часов
Оценивание: rubric, проходной результат 70


Задача

Написать и контейнеризовать HTTP-сервис eventapi: приём событий и статистика по ним.

Задача продолжает проект 1. Там события читались из файла инструментом командной строки; здесь они приходят по HTTP, а агрегация та же. Логика подсчёта переносится почти без изменений — меняется всё остальное: конфигурация, журналирование, пробы, завершение.

Сервис из этого проекта встраивается в стек проекта 3, поэтому одно решение стоит принять заранее: хранилище должно быть отделено интерфейсом. В проекте 3 хранение в памяти заменяется на PostgreSQL, и заменяться должен один класс, а не обработчики запросов.

Endpoint'ы

МетодПутьНазначение
POST/eventsПринять событие с проверкой полей
GET/eventsСписок с фильтром и страницами
GET/statsАгрегация по полю
GET/healthzЖив ли процесс
GET/readyzГотов ли обслуживать
GET/startupzЗавершена ли инициализация

Требования

Обязательные

ТребованиеПроверяется
1Валидация тела запроса; неверные данные дают 422 с указанием поляChecklist 1
2Три пробы с разной логикойChecklist 2
3Конфигурация из переменных окружения, проверяется при стартеChecklist 3
4Опечатка в имени переменной приводит к отказу, а не игнорируетсяChecklist 3
5Структурированные логи в stdout: каждая строка — объект JSONChecklist 4
6Идентификатор запроса проходит через все записи одного запросаChecklist 4
7Мягкое завершение: активные запросы дорабатываютсяChecklist 5
8Запуск не от root, идентификатор числовойChecklist 6
9Multi-stage; инструментов сборки в итоговом образе нетChecklist 7
10Тесты; покрытие не ниже 85 %; провал останавливает сборкуChecklist 8
11Сервис доступен снаружи container'аChecklist 9

Требования, невыполнение которых снимает работу

  • приложение слушает 127.0.0.1 — снаружи container'а недоступно;
  • SIGTERM не завершает процесс в пределах grace period;
  • конфигурация с ошибкой не останавливает старт, а проявляется при первом запросе;
  • запуск от root без обоснования.

Архитектура

Три пробы, а не одна

Это главное содержательное требование проекта. Три вопроса — три ответа:

ПробаВопросЧто проверяетЧто делает оркестратор при отказе
/startupzИнициализация завершена?Готовность внутренних структурЖдёт, не убивая
/healthzПроцесс жив?Только сам процессПерезапускает
/readyzГотов обслуживать?Зависимости, прогрев, завершениеУбирает из балансировки

Ошибка, которую требование призвано предотвратить: один и тот же путь в liveness и readiness. Тогда отказ зависимости приводит к перезапуску всех экземпляров разом — а после перезапуска зависимость по-прежнему недоступна (урок 11.3).

Отсюда правило: /healthz не должен обращаться к зависимостям.

Конфигурация проверяется при старте

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

Требование 4 отдельное и не сводится к требованию 3. Опечатка в имени переменной (EVENTAPI_LOG_LEVE) не даёт неверного значения — она даёт тихое умолчание. Библиотеки такое обычно не ловят, и проверку приходится делать явно.

Хранилище за интерфейсом

text
обработчики запросов
        │
        ▼
   Storage (протокол)
        │
   ┌────┴─────┐
   ▼          ▼
MemoryStorage  PostgresStorage   ← проект 3

Обязательное свойство хранилища в памяти — предел размера. Сервис, принимающий события без ограничения, исчерпает память, и отказ придёт от OOM killer, без записи в лог приложения (checkpoint 3).

Структура файлов

text
eventapi/
├── Dockerfile
├── .dockerignore
├── compose.yaml
├── requirements.txt
├── requirements-dev.txt
├── pytest.ini
├── README.md
├── app/
│   ├── __init__.py
│   ├── settings.py         # окружение и проверка при старте
│   ├── models.py           # схемы запросов и ответов
│   ├── storage.py          # протокол и реализация в памяти
│   ├── logging_config.py   # JSON-формат и идентификатор запроса
│   └── main.py             # приложение, пробы, endpoint'ы
└── tests/
    ├── conftest.py
    ├── test_settings.py
    ├── test_api.py
    ├── test_probes.py
    ├── test_logging.py
    ├── test_shutdown.py
    └── test_process.py     # проверки на запущенном процессе

Последний файл отличает проект от упражнения: TestClient не поднимает сервер, поэтому две вещи он показать не может — формат строк, которые печатает сам uvicorn, и реакцию на сигнал. Обе проверяются только запуском процесса.


Пошаговый план

Шаг 1. Модели и валидация

Схемы запроса и ответа. Решить сразу:

  • extra="forbid" или молчаливое игнорирование лишних полей;
  • что делать со временем без часового пояса.

Опора: урок 6.10.

Шаг 2. Конфигурация

Переменные окружения с префиксом, проверка значений и проверка имён.

Опора: урок 6.4, урок 11.5.

Шаг 3. Хранилище

Протокол и реализация в памяти с пределом размера. Порядок при равных счётчиках должен быть воспроизводим.

Шаг 4. Журналирование

JSON в stdout, идентификатор запроса из контекстной переменной, подчинение логгеров uvicorn общему формату.

Опора: урок 6.6, урок 13.1.

Шаг 5. Приложение и пробы

lifespan вместо устаревших обработчиков событий, middleware с учётом активных запросов, три пробы.

Опора: урок 6.5, урок 11.3.

Шаг 6. Тесты

Отдельно — тесты на запущенном процессе. Именно они ловят то, что не видит TestClient.

Шаг 7. Образ

Multi-stage, стадия тестов, HEALTHCHECK, числовой USER, exec-форма точки входа.

Опора: урок 5.7, урок 5.8.


Критерии завершения

  1. curl -s localhost:8000/healthz отвечает 200 при недоступных зависимостях.
  2. curl -s -o /dev/null -w '%{http_code}' localhost:8000/readyz даёт 503 во время завершения.
  3. Неверное тело запроса даёт 422 с именем поля.
  4. docker run -e EVENTAPI_PORT=0 … завершается с ошибкой при старте, а не при запросе.
  5. docker run -e EVENTAPI_LOG_LEVE=info … завершается с ошибкой: имя переменной неизвестно.
  6. Каждая строка docker logs разбирается как JSON — включая строки uvicorn.
  7. time docker stop укладывается менее чем в 3 секунды.
  8. docker build --target test падает при сломанном тесте.
  9. Сервис отвечает на запрос с хоста после docker run -p.

О размере образа: сначала договоритесь, что вы меряете. Под Docker 29 два очевидных способа дают числа, различающиеся вчетверо:

bash
docker images eventapi --format '{{.Size}}'                        # DISK USAGE
docker image inspect eventapi --format '{{.Size}}' | numfmt --to=si  # CONTENT SIZE
text
293MB
69M

Первое — сколько образ занимает на узле, второе — сколько качается из registry (урок 3.4).

Ориентир «меньше 350 MB по DISK USAGE». Он выбран измерением, а не на глаз: python:3.13-slim сам по себе занимает 178 MB, и требование «меньше 200 MB» на диске для FastAPI-приложения недостижимо — на всё приложение осталось бы 22 MB. Эталонное решение даёт 293 MB / 69 MB (Docker 29.7.1, измерено 2026-08-04).

Если не укладываетесь — обоснованный путь: перейти на fastapi без [standard] плюс uvicorn[standard]. Это измерено и даёт 247 MB / 57 MB — минус 55 MB на диске. Точка входа python -m uvicorn нужна независимо от размера (требование 5). Записать в README, что именно было сделано и сколько дало.


Дополнительные задания

ЗаданиеЧто добавляет
1Ограничить размер тела запроса и проверить поведение при превышенииЗащита от исчерпания памяти
2Очистить идентификатор запроса, приходящий из заголовкаЗначение от клиента попадает в журнал
3Проверить, что все строки лога — JSON, тестом на запущенном процессеЛовит смешение форматов
4Сравнить размер образа с fastapi[standard] и безИзмерение вместо предположения
5Добавить метрики в формате PrometheusРаздел 13
6Проверить поведение при --read-onlyУрок 11.2

Задания 1–3 реализованы в эталонном решении; 4–6 — нет.


Навигация

Список проверок →
Эталонное решение → — открывать после собственной попытки
← Предыдущий проект: Python CLI
Вернуться к проектам
Главное оглавление

Markdown на GitHub ↗