Production ML-сервис на FastAPI: архитектура, деплой и CI/CD для высоких нагрузок

Практический курс по проектированию, реализации и деплою ML-сервиса на FastAPI, оборачивающего LLM/CV модели. Фокус на production-ready коде, Docker/Kubernetes, наблюдаемости и полноценном CI/CD, чтобы сервис было легко масштабировать и изменять.

1. Архитектура ML-сервиса: слои, контракты API, паттерны и нефункциональные требования

Архитектура ML-сервиса: слои, контракты API, паттерны и нефункциональные требования

Production ML‑сервис (FastAPI + LLM/CV/классическая модель) — это не «обёртка вокруг predict», а система, где важны изоляция ответственности, стабильные контракты, управляемые зависимости и заранее продуманные нефункциональные требования (NFR).

1) Слои: как разделять ответственность

Цель слоёв — сделать сервис изменяемым (модель/хранилище/транспорт можно менять), тестируемым и предсказуемым.

Рекомендуемая структура (по смыслу, не по папкам):

  • Transport / API слой
  • - Принимает запрос (HTTP), валидирует формат, аутентифицирует/авторизует, ставит трейс/корреляцию. - Преобразует DTO (внешний контракт) в команды/запросы приложения. - Возвращает ответы и ошибки в стандартизированном виде.

  • Application (use cases) слой
  • - Оркестрация: «что сделать», но не «как устроены детали». - Транзакционные границы (если есть БД), ретраи на уровне бизнес‑операции, политика таймаутов. - Вызов доменных сервисов и портов (интерфейсов) к инфраструктуре.

  • Domain слой (если есть выраженная предметная область)
  • - Инварианты, правила, сущности/значения, доменные события. - Для чисто инференс‑API иногда домен минимален, но полезен для правил вроде лимитов, тарифов, статусов задач.

  • Infrastructure слой
  • - Реализации портов: загрузка модели, доступ к фичестору/БД, кэш, очередь, файловое хранилище, внешние API. - Здесь живут «грязные детали»: SDK, драйверы, форматы, конкретные клиенты.

  • Cross‑cutting (сквозные аспекты)
  • - Логи, метрики, трассировка, конфигурация, секреты, лимиты, политики ретраев.

    Визуализация потоков зависимостей:

    2) Контракты API: что фиксируем заранее

    Контракт — это то, за что вы «держите удар» при изменениях.

    Формат запросов/ответов

  • Явные схемы: отдельные DTO для входа/выхода; не «протаскивайте» внутренние структуры наружу.
  • Версионирование: минимум — префикс /v1. Важно при изменениях схем и семантики.
  • Совместимость:
  • 1) добавление новых необязательных полей обычно безопасно; 2) изменение смысла поля или удаление — почти всегда breaking.

    Единый формат ошибок

    Клиентам важно получать предсказуемые ошибки.

  • Поля ошибки (пример): code, message, details, trace_id.
  • Разделяйте валидационные ошибки (400/422), авторизация/аутентификация (401/403), лимиты (429), внутренние (500).
  • Идемпотентность

    Если запрос может быть повторён (ретраи клиента, сетевые сбои), нужен ключ идемпотентности.

  • Для операций, создающих сущности/задачи: Idempotency-Key + хранение результата/статуса.
  • Для «чистого инференса» без побочных эффектов это менее критично, но полезно, если вы биллите/логируете вызовы.
  • Синхронный vs асинхронный инференс

  • Sync (быстро): ответ сразу. Нужны жёсткие таймауты и ограничение размеров.
  • Async job (долго/тяжело): POST /jobsjob_id, затем GET /jobs/{id} или вебхук.
  • Для LLM возможно streaming (частичные токены): это отдельный контракт и отдельные ограничения по прокси/таймаутам.
  • 3) Паттерны, которые чаще всего окупаются

  • Ports & Adapters (Hexagonal)
  • - Определяете интерфейсы (порты) в Application слое: ModelPort, StoragePort, QueuePort. - Инфраструктура предоставляет адаптеры. Это снижает связанность и упрощает тесты.

  • Repository + Unit of Work (когда есть БД)
  • - Репозиторий скрывает запросы к БД. - Unit of Work управляет атомарностью изменений, полезно для задач/статусов/лимитов.

  • Bulkhead + Rate limiting
  • - Разделяйте ресурсы: например, отдельные пулы/очереди для «тяжёлых» и «лёгких» запросов. - Лимиты защищают от шторма запросов и непредсказуемого времени инференса.

  • Circuit Breaker + Timeouts
  • - Если внешний сервис деградирует (фичестор/векторная БД/модельный runtime), breaker ограничивает лавинообразные таймауты.

  • Cache (осторожно)
  • - Кэшируйте только при понятной ключевой стратегии (например, хэш нормализованного входа) и ясной политике инвалидирования.

    4) Нефункциональные требования (NFR): что определить до реализации

    | Область | Что зафиксировать | Почему важно | |---|---|---| | Производительность | SLO по p95/p99 latency, throughput, лимиты payload | ML‑инференс нестабилен по времени | | Надёжность | таймауты, ретраи, деградация функционала | зависимость от GPU/внешних систем | | Масштабирование | горизонтальное (реплики), конкурентность, очереди | иначе «упрётесь» в CPU/GPU/IO | | Наблюдаемость | метрики, логи, трассировка, trace_id | поиск причин деградаций | | Безопасность | authn/authz, защита PII, лимиты, аудит | модели часто обрабатывают чувствительные данные | | Воспроизводимость | версии модели, данных, конфигов, откат | без этого невозможно расследовать инциденты | | Стоимость | бюджет на токены/инференс, лимиты, квоты | LLM может быть дорогим |

    Практическая рекомендация: NFR оформляйте как «контракт команды с самой собой» — конкретные числа (таймауты, лимиты, SLO), а не общие слова.

    ---

    Задания для закрепления

    1) Опишите 4–5 слоёв вашего будущего сервиса и перечислите, что точно нельзя помещать в Application слой.

    2) Спроектируйте контракт endpoint’а инференса: какие поля входа/выхода, какие коды ошибок, как будет выглядеть trace_id.

    3) Решите, где вам нужен async‑режим. Опишите жизненный цикл job: статусы, получение результата, обработка ошибок.

    4) Выберите 3 паттерна из статьи и для каждого напишите: какую проблему он решает именно в ML‑сервисе.

    5) Сформулируйте 6 NFR для сервиса: 2 про производительность, 2 про надёжность, 2 про безопасность.

    <details> <summary> Ответы (примерные ориентиры) </summary>

    1) Слои: API → Application → Domain (опционально) → Infrastructure + cross‑cutting. В Application нельзя класть: SQL/ORM запросы, конкретные SDK клиентов, работу с файловой системой «в лоб», детали загрузки модели, HTTP‑клиенты внешних сервисов.

    2) Контракт инференса: вход — input (текст/изображение/путь/байты), params (опционально), request_id/idempotency_key (если нужно), выход — result, model_version, latency_ms, trace_id. Ошибки: 422 (валидация), 401/403 (доступ), 429 (лимиты), 503 (модель/зависимость недоступна), 500 (неожиданное). trace_id — строковый идентификатор корреляции, возвращается в ответе и логах.

    3) Async нужен, если инференс может превышать HTTP‑таймауты/непредсказуем по времени, или если требуется очередь на GPU. Жизненный цикл: queuedrunningsucceeded/failed (+ canceled). Результат доступен по job_id, ошибки содержат code/message/trace_id.

    4) Примеры:

  • Bulkhead: тяжёлые запросы не «убивают» лёгкие.
  • Circuit breaker: предотвращает лавину таймаутов при деградации внешней зависимости.
  • Ports & Adapters: позволяет заменить модельный runtime или хранилище без переписывания use case.
  • 5) Примеры NFR:

  • Производительность: p95 latency для sync ≤ X мс; max payload ≤ Y МБ.
  • Надёжность: таймаут на инференс ≤ T; breaker открывается после N ошибок за окно.
  • Безопасность: обязательная аутентификация; запрет логирования сырого текста/изображений или маскирование PII.
  • </details>

    2. Каркас FastAPI: типы, Pydantic-схемы, DI, ошибки, версии API и OpenAPI

    Каркас FastAPI: типы, Pydantic-схемы, DI, ошибки, версии API и OpenAPI

    В прошлой статье мы фиксировали архитектурные слои и контракты. Здесь — как собрать production‑каркас FastAPI, чтобы контракт был типобезопасным, ошибки — единообразными, зависимости — управляемыми, а документация — автоматически поддерживалась.

    1) Типы в Python как часть контракта

    В FastAPI типы — это не «для красоты»: по ним строится валидация, OpenAPI и автодокументация.

    Практики, которые быстро окупаются:

  • Явно типизируйте вход и выход эндпоинта: -> ResponseModel + response_model=....
  • Разделяйте DTO и внутренние типы: наружу — только Pydantic‑схемы, внутрь — свои dataclass/типы домена.
  • Строгие типы там, где важны инварианты:
  • - UUID для идентификаторов, - conint/ge/le (или аналоги) для лимитов, - Literal/Enum для режимов ("sync" | "async").

    Мини‑шаблон эндпоинта:

    2) Pydantic‑схемы: запрос, ответ, ошибки

    2.1. Отдельные модели для Request/Response

    Правило: Request и Response — разные модели, даже если поля похожи. Это защищает от случайного «протекания» внутренних полей наружу.

    Что обычно кладут в Response модели ML‑сервиса:

  • result (строго типизированный результат: текст, классы, боксы и т.д.)
  • model_version (для воспроизводимости)
  • trace_id (для корреляции в логах/трейсинге)
  • Опционально: timings/usage (но не смешивайте это с бизнес‑результатом)
  • 2.2. Валидация и нормализация входа

    Полезная практика — нормализация в схеме, а не в handler’е:

    Так вы избегаете «рассыпанной» валидации по коду и получаете единый источник правды.

    2.3. Единая схема ошибок

    Даже если FastAPI умеет возвращать 422 сам, в production обычно нужен единый формат ошибок (см. контракт из прошлой статьи), чтобы фронт/клиенты не парсили разные структуры.

    Пример модели ошибки:

    3) DI (Depends): управляемые зависимости вместо глобального состояния

    FastAPI DI решает две задачи:

  • Где хранить «долгоживущие» ресурсы (клиенты, пулы, модель в памяти)
  • Как передавать их в use case/эндпоинты без глобальных переменных
  • 3.1. Lifespan для инициализации

    Используйте lifespan (или события startup/shutdown) для прогрева модели и создания клиентов:

    3.2. Провайдеры зависимостей

    Дальше — тонкий слой адаптеров:

    Рекомендация: зависимости должны отдавать абстракции (порт/интерфейс), а не конкретные SDK‑клиенты — так проще тестировать.

    4) Ошибки: exception handlers, маппинг доменных исключений

    FastAPI даёт два механизма:

  • HTTPException — когда вы уже знаете HTTP‑статус
  • Глобальные exception_handler — когда хотите централизованный маппинг
  • Схема, которая хорошо работает:

    Идея: внутри use case вы бросаете свои исключения, а на границе API слоя превращаете их в стандартный ErrorResponse.

    5) Версии API: /v1 как управляемое изменение

    Минимально практичный вариант — версионирование через роутеры:

    Важно:

  • Не смешивайте версии в одном роутере: проще поддерживать.
  • Если меняется семантика или обязательность полей — это новый /v2.
  • В OpenAPI используйте deprecated=True для старых эндпоинтов, если нужно мягкое снятие.
  • 6) OpenAPI: как сделать документацию полезной

    FastAPI генерирует OpenAPI автоматически, но «production‑качество» появляется, когда вы добавляете:

  • summary/description у эндпоинтов
  • response_model и явные responses={...} для ошибок
  • Примеры (examples) для request/response
  • Теги (tags), чтобы документация была навигабельной
  • Пример описания ошибок в контракте эндпоинта:

    Если часть роутов служебная (health/metrics), часто их исключают из публичной схемы через include_in_schema=False.

    ---

    Задания для закрепления

    1) Спроектируйте PredictRequest и PredictResponse для LLM‑эндпоинта: какие поля обязательны, какие ограничения по длине/диапазонам?

    2) Опишите 3 доменных исключения (например, «модель не готова», «лимит превышен», «неподдерживаемый формат») и их маппинг в HTTP‑коды.

    3) Нарисуйте (текстом) схему зависимостей: какие ресурсы создаются в lifespan, а какие должны быть per‑request.

    4) Предложите структуру роутеров для /v1: inference, admin, health. Что включать в OpenAPI, а что скрывать?

    5) Добавьте в эндпоинт явные responses для ошибок и объясните, чем это лучше «молчаливого 500».

    <details> <summary> Ответы (примерные) </summary>

    1) PredictRequest: text: str (min_length=1, max_length=20_000), temperature: float (0..2), max_tokens: int (1..4096), stream: bool = False. PredictResponse: text: str, model_version: str, trace_id: str, опционально usage: {"prompt_tokens": int, "completion_tokens": int}.

    2) Примеры:

  • ModelUnavailable → 503 + code=MODEL_UNAVAILABLE.
  • RateLimitExceeded → 429 + code=RATE_LIMIT.
  • UnsupportedMedia/InvalidInputFormat → 415 или 422 + понятное details.
  • 3) Lifespan: загрузка модели, warmup, создание клиентов (Redis/DB/HTTP), создание пулов. Per-request: trace_id, контекст пользователя/прав, лёгкие адаптеры, scoped-сессия БД (если используется).

    4) Роутеры:

  • /v1/inference/* — публичные, в OpenAPI.
  • /v1/admin/* — только для внутренних операций, можно оставить в OpenAPI, но защитить auth и пометить.
  • /health, /ready — часто include_in_schema=False.
  • 5) Явные responses фиксируют контракт: клиенты видят, какие ошибки возможны и какой у них формат. Это снижает стоимость интеграции и помогает тестировать (контрактные тесты), а также дисциплинирует обработку исключений.

    </details>

    3. ML-инференс в проде: модельный слой, батчинг, кэш, очереди и контроль ресурсов

    ML-инференс в проде: модельный слой, батчинг, кэш, очереди и контроль ресурсов

    ML‑инференс в production отличается от «вызвали predict» тем, что система должна выдерживать всплески нагрузки, непредсказуемую латентность и дорогие ресурсы (CPU/GPU/память). Ниже — практический набор решений для модельного слоя и управления вычислениями. Архитектурные слои, контракты API и единый формат ошибок считаем уже заданными (см. предыдущие статьи).

    1) Модельный слой: изоляция рантайма и предсказуемость

    Цель модельного слоя — спрятать все детали рантайма (PyTorch/ONNX/TensorRT/внешний LLM‑провайдер) за стабильным интерфейсом приложения.

    Ключевые обязанности адаптера модели:

  • Инициализация и прогрев
  • 1) загрузка весов и токенизатора; 2) прогрев (1–N тестовых прогонов), чтобы убрать «холодный старт»; 3) проверка доступности (health/readiness).
  • Единый формат входа/выхода
  • 1) нормализация: кодировки, размеры изображений, trimming текста; 2) стабилизация типов: всегда одинаковая структура результата.
  • Версионирование
  • 1) выдавайте model_version в ответе; 2) логируйте версию и конфиг инференса (температура, max_tokens и т.п.) отдельно от бизнес‑результата.
  • Потокобезопасность и процессная модель
  • 1) многие пайплайны безопаснее держать «один процесс — одна модель»; 2) если модель не потокобезопасна, не пытайтесь лечить это async — используйте ограничения конкурентности.

    Практический критерий: если вы можете заменить реализацию модели без изменения use case и API‑слоя — модельный слой сделан правильно.

    2) Конкурентность инференса: не путать HTTP‑параллелизм и вычисления

    FastAPI легко принимает множество запросов, но вычисления модели часто упираются в:

  • ограниченную GPU‑память;
  • сериализацию внутри CUDA/BLAS;
  • накладные расходы токенизации/препроцессинга.
  • Поэтому в проде почти всегда вводят явную модель конкурентности:

  • лимит одновременных инференсов (например, семафор);
  • отдельные «пулы» для тяжёлых/лёгких запросов (bulkhead);
  • строгие таймауты на этапы: препроцессинг → инференс → постпроцессинг.
  • Если не контролировать конкурентность, типичный отказ выглядит так: очередь запросов растёт, память фрагментируется, p99 взлетает, затем начинаются OOM и перезапуски.

    3) Батчинг: как поднять throughput без потери управляемости

    Батчинг выгоден, когда стоимость инференса на один запрос падает при обработке нескольких запросов вместе (особенно на GPU).

    Два распространённых режима:

  • Статический батчинг
  • 1) клиент присылает массив входов; 2) вы делаете один прогон модели. Подходит, если клиентская интеграция контролируема.
  • Микробатчинг на сервере (динамический)
  • 1) сервер собирает запросы в «окно» по времени (например, 5–20 мс); 2) либо до достижения max_batch_size.

    Важные нюансы микробатчинга:

  • Окно батчинга — это компромисс: больше окно → выше throughput, но хуже latency.
  • Переменная длина входов (токены/размеры изображений) может убить эффективность из‑за padding. Часто выгодно группировать похожие по размеру запросы.
  • Backpressure: если батч не успевает обрабатываться, нужно либо снижать приём, либо уходить в очередь.
  • Визуально:

    4) Кэш: когда он действительно помогает (и когда опасен)

    Кэширование ускоряет инференс, если:

  • входы повторяются;
  • результат детерминирован или «достаточно стабилен»;
  • стоимость инференса велика по сравнению с стоимостью чтения кэша.
  • Практика кэширования:

  • Ключ: хэш нормализованного входа + версия модели + важные параметры инференса.
  • TTL: даже если модель не меняется, TTL помогает от переполнения и устаревших результатов.
  • Негативный кэш: кэшировать «вход невалиден/файл не найден» обычно нельзя вечно, но иногда полезно на короткое время при штормах.
  • Защита от stampede: если популярный ключ истёк, не допускайте сотни параллельных пересчётов; нужен single-flight подход (один пересчитывает, остальные ждут/получают старое).
  • PII и безопасность: не кэшируйте сырой чувствительный контент; по возможности кэшируйте только хэши и агрегированные результаты.
  • Антипаттерн: кэшировать ответы LLM при высокой температуре и без учёта параметров — получите «случайные» попадания и странное поведение.

    5) Очереди: когда синхронный HTTP перестаёт работать

    Очередь нужна, когда:

  • время инференса нестабильно и может превышать таймауты;
  • GPU — общий ресурс, и нужен планировщик;
  • требуется приоритизация (например, платные пользователи vs бесплатные);
  • вы хотите сгладить пики нагрузки.
  • Что важно в очередях именно для инференса:

  • At-least-once семантика: задачи могут выполняться повторно — делайте обработку идемпотентной.
  • Retry политика: отличайте «временные» ошибки (таймаут модели) от «постоянных» (неподдерживаемый формат).
  • Dead-letter: без него проблемные задачи будут бесконечно крутиться.
  • Контроль размера очереди: переполненная очередь — это скрытая деградация. Иногда правильнее вернуть 429/503, чем принять всё.
  • 6) Контроль ресурсов: admission control и деградация вместо аварий

    Чтобы сервис не «самоубивался», нужны механизмы управления ресурсами на входе и внутри обработки:

  • Ограничение входа (admission control)
  • 1) max размер payload; 2) max длина текста/max_tokens; 3) rate limit по пользователю/ключу.
  • Ограничение одновременных вычислений
  • 1) семафор на GPU‑инференсы; 2) отдельные лимиты на препроцессинг (CPU) и на инференс (GPU).
  • Ограничение времени
  • 1) таймаут на всю операцию; 2) таймаут на отдельные шаги; 3) корректная отмена (cancellation) — особенно важна при очередях.
  • Наблюдаемые бюджеты
  • 1) лимиты токенов/стоимости на запрос; 2) квоты на пользователя/организацию; 3) алерты по GPU/CPU/RAM и глубине очереди.

    Мини‑правило: лучше вернуть контролируемую ошибку «перегрузка» (с понятным кодом), чем довести систему до OOM и рестартов.

    ---

    Задания для закрепления

    1) Опишите интерфейс модельного адаптера (словами): какие методы и какие инварианты должны быть гарантированы (версия, таймауты, формат ошибок)?

    2) Выберите один endpoint (LLM или CV) и решите: нужен ли ему батчинг. Если да — какой (статический или микробатчинг), и какие два параметра вы бы вынесли в конфиг?

    3) Спроектируйте ключ кэша для инференса: какие поля обязательно включить, а какие нельзя (или нежелательно) хранить по соображениям безопасности?

    4) Придумайте политику очереди: какие ошибки ретраить, сколько раз, и что отправлять в dead-letter.

    5) Составьте список из 6 лимитов/бюджетов для контроля ресурсов (минимум 2 про вход, 2 про конкурентность/время, 2 про стоимость/квоты).

    <details> <summary> Ответы (примерные ориентиры) </summary>

    1) Интерфейс адаптера:

  • load()/warmup() или эквивалент в lifecycle;
  • predict(input, params) -> result с гарантией: валидированный формат результата, проставленный model_version, понятные исключения (например, ModelUnavailable, InferenceTimeout, InvalidInput).
  • Инварианты: предсказуемые таймауты, отсутствие утечек ресурсов, потокобезопасность либо документированное ограничение конкурентности.
  • 2) Батчинг:

  • Для CV (детекция/классификация) часто выгоден батчинг на GPU.
  • Для LLM батчинг возможен, но сложнее из-за разной длины последовательностей; иногда эффективнее микробатчинг с группировкой по длине.
  • Параметры конфига: max_batch_size, batch_window_ms.
  • 3) Ключ кэша:

  • Обязательно: хэш нормализованного входа, model_version, критичные параметры (например, temperature, top_p, max_tokens, режим препроцессинга).
  • Нежелательно хранить: сырой текст/изображение, пользовательские токены, PII. Лучше хранить только хэши и метаданные.
  • 4) Очередь:

  • Ретраить: временные ошибки инфраструктуры (временная недоступность модели/таймаут внешнего хранилища) с экспоненциальной задержкой.
  • Не ретраить: ошибки валидации и неподдерживаемый формат.
  • Dead-letter: задачи, исчерпавшие ретраи, или задачи с «постоянной» ошибкой, чтобы их можно было расследовать.
  • 5) Лимиты/бюджеты:

  • Вход: max payload (МБ), max длина текста/размер изображения.
  • Конкурентность/время: max одновременных инференсов на GPU, таймаут на инференс и общий таймаут.
  • Стоимость/квоты: max tokens на запрос, дневная/минутная квота токенов или запросов на пользователя/организацию.
  • </details>

    4. Высокая нагрузка: async, Uvicorn/Gunicorn, лимиты, rate limit, idempotency и backpressure

    Высокая нагрузка: async, Uvicorn/Gunicorn, лимиты, rate limit, idempotency и backpressure

    Высокая нагрузка в ML‑сервисе проявляется не только как «много RPS», но и как комбинация: длинные хвосты латентности, дорогие вычисления, всплески, неравномерность входов. Архитектуру слоёв, контракты и модельный контроль ресурсов мы уже фиксировали в предыдущих статьях; здесь — как собрать и настроить рантайм так, чтобы сервис не деградировал лавинообразно.

    1) Async в FastAPI: где выигрывает, а где создаёт иллюзию масштабирования

    Async даёт прирост, когда запрос большую часть времени ждёт I/O:

  • вызовы внешних API (LLM‑провайдер, feature store, object storage);
  • чтение/запись в БД/Redis;
  • стриминг ответа.
  • Async не ускоряет CPU/GPU‑вычисления. Если внутри обработчика вы запускаете тяжёлую синхронную работу (токенизация, препроцессинг, постпроцессинг, вычисления на CPU), вы блокируете event loop.

    Практические правила:

  • Разделяйте I/O и compute: I/O оставляйте в async, compute — либо в отдельном процессе/воркере, либо в ограниченном пуле.
  • Любой блокирующий SDK (например, синхронный клиент) — риск. Либо используйте async‑клиент, либо уносите вызов в threadpool, но контролируйте параллелизм.
  • Отмена (cancellation) важна под нагрузкой: если клиент ушёл, незачем продолжать дорогую работу. Проверьте, что ваши зависимости корректно прерываются (или хотя бы не накапливают хвосты).
  • Мини‑мысленная модель:

    2) Uvicorn и Gunicorn: как выбирать процессную модель

    Uvicorn standalone

    Подходит, когда вам нужен простой запуск и вы контролируете окружение.

    Ключевые параметры:

  • workers — сколько процессов будет принимать запросы;
  • loop/http — реализация event loop и HTTP парсера;
  • graceful shutdown — чтобы не рвать активные запросы.
  • Gunicorn + UvicornWorker

    Чаще используют в production, потому что Gunicorn даёт зрелую процессную модель:

  • контроль воркеров (перезапуск при утечках);
  • ограничения по запросам на воркер;
  • более предсказуемое graceful‑поведение.
  • Рекомендации по воркерам:

  • CPU‑bound: больше процессов, но следите за памятью.
  • GPU‑inference: часто разумно 1 процесс на 1 GPU (или даже на часть GPU), иначе получите конкуренцию за память/контекст.
  • Не пытайтесь лечить всё воркерами: если модель ограничена семафором/очередью, увеличение workers может лишь ускорить деградацию.
  • Что обычно настраивают в Gunicorn:

  • timeout и graceful_timeout (не путать: второй — на корректное завершение);
  • keepalive (особенно за прокси);
  • max_requests и max_requests_jitter (перезапуск воркера до накопления утечек);
  • preload_app — осторожно: удобно для скорости старта, но может ухудшить память при copy‑on‑write, если вы активно мутируете большие структуры.
  • 3) Лимиты: что ограничивать на входе и внутри сервиса

    Лимиты — это способ сделать отказ контролируемым.

    На входе (admission control):

  • максимальный размер тела запроса;
  • ограничения на параметры инференса (например, длина текста, max_tokens);
  • ограничения на число одновременных соединений/stream.
  • Внутри обработки:

  • лимит конкурентности на «дорогой участок» (GPU/CPU‑пулы);
  • таймауты на этапы (I/O и compute отдельно);
  • лимит очереди ожидания (если запросы ставятся в ожидание семафора/локальной очереди).
  • Критичный момент: лимиты должны возвращать понятную ошибку и (по возможности) Retry-After, чтобы клиенты ретраили корректно.

    4) Rate limit: защита от шторма и «нечестных» клиентов

    Rate limiting отвечает на вопрос: сколько запросов/стоимости мы готовы принять за единицу времени.

    Практика:

  • Ключ лимита: API‑ключ, user_id, org_id, IP (как fallback), плюс endpoint.
  • Алгоритм: token bucket (разрешает burst), fixed window (проще, но резче), sliding window (ровнее, сложнее).
  • Хранилище:
  • 1) локально в памяти — быстро, но не работает при нескольких репликах; 2) Redis — типичный выбор для распределённого лимита.

    Важно разделять:

  • лимит по RPS (защита транспорта);
  • лимит по «стоимости» (например, токены/изображения/разрешение) — ближе к биллингу и защите GPU.
  • 5) Idempotency: как переживать ретраи без дублей

    Идемпотентность нужна там, где повтор запроса не должен создавать повторный эффект:

  • создание job (async‑инференс);
  • запись результата/логирование биллинга;
  • любые операции, где есть внешний побочный эффект.
  • Паттерн:

  • клиент отправляет Idempotency-Key;
  • сервис хранит статус и результат по ключу (обычно в Redis/БД) с TTL;
  • повторный запрос возвращает тот же результат (или текущий статус), не запуская вычисление заново.
  • Тонкость под нагрузкой: нужен single-flight (один исполняет, остальные ждут/получают статус), иначе «ретраи» станут мультипликатором нагрузки.

    6) Backpressure: как говорить «нет» до того, как станет поздно

    Backpressure — это политика, которая не даёт очередям и памяти расти бесконечно.

    Типовые сигналы перегрузки:

  • очередь ожидания семафора растёт;
  • p95/p99 растут быстрее, чем среднее;
  • увеличивается число отмен/таймаутов;
  • растёт глубина внешней очереди (если она есть).
  • Стратегии:

  • Load shedding: вернуть 429/503 сразу, если достигнуты лимиты конкурентности/очереди.
  • Приоритизация: платные/внутренние запросы обслуживать раньше (отдельные очереди/bulkhead).
  • Деградация: упрощённый режим (меньше max_tokens, отключение дорогих опций), если это допускает контракт.
  • Явный Retry-After: чтобы клиентские ретраи не были «слепыми».
  • Визуализация решения «принимать или отклонять»:

    ---

    Задания для закрепления

    1) Опишите, какие части вашего пайплайна должны быть async, а какие — нет, и почему.

    2) Выберите схему запуска: Uvicorn или Gunicorn+UvicornWorker. Какие 4 параметра вы обязательно зафиксируете в конфиге и какие риски они закрывают?

    3) Сформулируйте три лимита admission control для LLM‑эндпоинта и три лимита внутри обработки.

    4) Спроектируйте rate limit: ключ, алгоритм, хранилище, и какие коды/заголовки возвращать при превышении.

    5) Придумайте политику idempotency для POST /jobs: где хранить состояние, какие статусы, что возвращать при повторе.

    <details> <summary> Ответы (примерные) </summary>

    1) Async: вызовы внешних API/БД/Redis, стриминг. Не async: тяжёлый CPU/GPU compute (его нужно лимитировать семафором/пулом/очередью). Причина: compute блокирует event loop и ухудшает латентность всех запросов.

    2) Gunicorn+UvicornWorker: выбираю для контроля воркеров и рестартов.

  • workers: ограничить параллельные процессы и память.
  • timeout/graceful_timeout: не держать зависшие запросы и корректно завершаться.
  • max_requests (+ jitter): лечить утечки/фрагментацию.
  • keepalive: стабильность соединений за прокси.
  • Риски: слишком много workers → конкуренция за GPU/RAM; слишком маленький timeout → обрывы длинных запросов.

    3) Admission control (LLM): max размер тела, max длина текста, max max_tokens/запрещённые параметры. Внутри: семафор на инференс, общий таймаут на операцию, ограничение очереди ожидания (bounded queue), чтобы не копить хвосты.

    4) Rate limit:

  • ключ: org_id + endpoint (fallback: IP).
  • алгоритм: token bucket (разрешает burst).
  • хранилище: Redis для распределённости.
  • при превышении: 429, заголовок Retry-After, тело ошибки в вашем стандартном формате.
  • 5) Idempotency для POST /jobs:

  • ключ: Idempotency-Key обязателен.
  • хранение: Redis/БД запись key -> job_id, status, result/error с TTL.
  • статусы: queued/running/succeeded/failed.
  • повторный запрос: вернуть тот же job_id и текущий статус, не создавая новую задачу; при одновременных запросах — single-flight (один создаёт запись, остальные читают).
  • </details>

    5. Тесты и качество: unit/integration, контрактные тесты, линтеры, mypy и pre-commit

    Тесты и качество: unit/integration, контрактные тесты, линтеры, mypy и pre-commit

    Production‑ML сервис ломается редко из‑за «сложной математики» и часто из‑за изменений в контрактах, зависимостях, конфигурации и обработке ошибок. Поэтому качество — это система из тестов разных уровней + статический анализ + автоматизация через хуки.

    1) Стратегия: что и зачем тестируем

    Полезно держать в голове пирамиду (с поправкой на ML):

    Критерий «production‑готовности»: изменения в коде либо проходят все проверки, либо блокируются до мержа.

    2) Unit‑тесты: быстрые, детерминированные, на уровне логики

    Unit‑тесты должны проверять поведение без реальных сетей, GPU и внешних сервисов.

    Что обычно окупается в ML‑сервисе:

  • Use case / application‑логика: оркестрация шагов, таймауты/ретраи (на уровне вашей логики), выбор режимов.
  • Маппинг ошибок: доменное исключение → правильный code/HTTP‑статус/структура (единый формат ошибок вы уже фиксировали ранее).
  • Валидация и нормализация входа: особенно границы (max_tokens, длина текста, размеры изображения) и дефолты.
  • Ключи кэша / идемпотентности: чтобы любое изменение не ломало совместимость и не приводило к “cache miss storm”.
  • Практики:

  • Моки через порты/адаптеры (а не патчинг внутренностей): подменяйте ModelPort, StoragePort и т.д.
  • Фиксация времени и случайности: если есть рандомизация (LLM), тестируйте на «контракте» результата (структура, поля, коды), а не на точном тексте.
  • Параметризация: граничные значения (минимум/максимум) дешевле проверять списком кейсов.
  • 3) Integration‑тесты: проверяем стыки и реальную сборку приложения

    Integration‑тесты отвечают на вопрос: «всё ли реально работает вместе?» — роутинг, DI‑инициализация, сериализация, внешние клиенты.

    Типовые уровни интеграции:

  • FastAPI app как целое: запрос → endpoint → обработка → ответ. Здесь удобно проверять:
  • 1) что response_model реально соответствует; 2) что возвращаются нужные заголовки (например, Retry-After при перегрузке); 3) что корреляционный идентификатор (trace_id) пробрасывается.
  • Инфраструктура: Redis/БД/очередь.
  • 1) Запускайте зависимости в контейнерах в тестовом окружении. 2) Делайте миграции/инициализацию до тестов.
  • Модельный рантайм:
  • 1) Для тяжёлых моделей — часто достаточно «заглушки‑рантайма» в виде отдельного процесса/контейнера с предсказуемым ответом. 2) Реальную GPU‑модель тестируйте отдельно (smoke), потому что она дорогая и нестабильная по времени.

    Антипаттерн: пытаться сделать интеграционные тесты такими же быстрыми, как unit. Лучше честно разделить пайплайны: быстрые проверки на каждый PR, тяжёлые — по расписанию/перед релизом.

    4) Контрактные тесты: защита клиентов от ваших изменений

    Контрактные тесты фиксируют внешний договор: схемы, статусы, формат ошибок, совместимость.

    Что важно проверять автоматически:

  • OpenAPI как артефакт:
  • 1) схема генерируется; 2) не пропадают поля и эндпоинты; 3) не меняются типы/обязательность без повышения версии API.
  • Ошибки:
  • 1) для ключевых сценариев есть задокументированные ответы; 2) структура ошибки едина (например, code/message/trace_id/details).
  • Backwards compatibility:
  • 1) добавление необязательных полей обычно ок; 2) удаление/переименование/смена типа — блокировать.

    Минимально практичный подход:

  • В репозитории храните “снимок” openapi.json (или другой формат).
  • В CI сравнивайте текущую схему со снимком.
  • Любое изменение схемы требует осознанного апдейта снимка в PR (как изменение контракта).
  • Это дешевле, чем ловить поломку у потребителей.

    5) Линтеры и форматирование: меньше вариативности — меньше дефектов

    Цель линтеров — убрать целый класс проблем до тестов.

    Рекомендуемый набор ролей (инструменты можете выбрать свои):

  • Форматирование: единый стиль кода, чтобы диффы были про смысл.
  • Lint: ошибки, неиспользуемые импорты, опасные конструкции, сложность.
  • Security lint (минимум): явные секреты, небезопасные вызовы.
  • Docstring/typing стиль (опционально): если команда это реально поддерживает.
  • Практика для ML‑сервиса: отдельно линтите «ядро» (application/domain), и отдельно — инфраструктуру, где больше SDK‑исключений и компромиссов.

    6) mypy: типы как страховка контрактов и интеграций

    mypy полезен там, где Python чаще всего подводит в проде:

  • DTO ↔ внутренние структуры: неверное поле/тип ловится до запуска.
  • Границы портов: если ModelPort.predict() возвращает одно, а use case ожидает другое — вы узнаете в PR.
  • Optional и None‑логика: типизация вынуждает явно обработать “поля может не быть”.
  • Рекомендации:

  • Начинайте с режима «строго в ядре, мягко на краях»:
  • 1) строгие настройки для application/domain; 2) послабления для инфраструктуры (внешние библиотеки часто плохо типизированы).
  • Не пытайтесь типизировать всё сразу. Типизация должна уменьшать риск, а не останавливать разработку.
  • 7) pre-commit: автоматизация до того, как код попадёт в CI

    pre-commit делает так, чтобы базовые проверки выполнялись локально одинаково у всех.

    Что обычно включают:

  • форматирование;
  • линт;
  • mypy (иногда отдельным, более быстрым профилем);
  • базовые проверки репозитория: trailing spaces, окончания строк, большие файлы.
  • Политика команды: хуки должны быть достаточно быстрыми, иначе их начнут обходить.

    8) Quality gates: что блокирует merge

    Удобно явно зафиксировать “гейты”:

    | Категория | Проверка | Блокирует merge | |---|---|---| | Статика | формат + lint | да | | Типы | mypy на ядро | да | | Unit | быстрый набор | да | | Contract | OpenAPI diff/совместимость | да | | Integration | сервис + зависимости | обычно да (если стабильны) | | Coverage | порог на ядро | по ситуации |

    Главное: гейт должен быть предсказуемым. Если интеграционные тесты флапают, вы получите “red CI fatigue” и начнёте игнорировать сигналы.

    ---

    Задания для закрепления

    1) Разбейте ваш сервис на тестируемые зоны: что покрываете unit‑тестами, а что — только integration?

    2) Придумайте 5 контрактных инвариантов для вашего публичного /v1/* API (схемы, ошибки, статусы), которые нельзя ломать без /v2.

    3) Опишите минимальный набор интеграционных тестов (3–5 штук), которые дают уверенность, что релиз не сломан.

    4) Выберите набор quality gates для PR: какие проверки обязательны, а какие — “best effort”. Обоснуйте.

    5) Составьте список хуков pre-commit (4–6), которые вы реально готовы запускать на каждом коммите.

    <details> <summary> Ответы (примерные ориентиры) </summary>

    1) Пример разбиения:

  • Unit: use case инференса (без реальной модели), маппинг исключений в коды ошибок, генерация ключа кэша, валидация параметров.
  • Integration: FastAPI app целиком (роутинг + сериализация), Redis‑rate limit, создание job в очереди, сохранение/чтение результата.
  • Smoke (опционально отдельно): один реальный прогон модели или запрос к внешнему LLM‑провайдеру в тестовом окружении.
  • 2) Примеры контрактных инвариантов:

  • Endpoint /v1/predict существует и принимает PredictRequest без удаления полей.
  • Ошибки всегда имеют поля code и message, trace_id опционален.
  • При превышении лимита возвращается 429 с вашим code=RATE_LIMIT.
  • model_version возвращается строкой и не исчезает из ответа.
  • Типы полей не меняются (например, usage.prompt_tokens остаётся int).
  • 3) Минимальные integration‑тесты:

  • “Happy path” запрос → 200 → структура ответа.
  • Валидация: слишком длинный текст → 422/400 с единым форматом ошибки.
  • Перегрузка/лимит: искусственно превысить лимит → 429 + Retry-After.
  • Readiness/health: корректные статусы при готовности и при «модель не загружена».
  • 4) Пример quality gates:

  • Обязательные: формат+lint, mypy на ядро, unit, contract.
  • Условно обязательные: integration (если не флапают и быстро идут).
  • Best effort: тяжёлые smoke с реальной моделью (по расписанию или перед релизом).
  • 5) Пример хуков pre-commit:

  • форматтер;
  • линтер;
  • сортировка импортов (если не входит в линтер);
  • базовые хуки репозитория (концы строк, пробелы);
  • быстрый mypy для core‑пакета.
  • </details>

    6. Контейнеризация и деплой: Dockerfile, Compose, Helm/Kubernetes, секреты и конфиги

    Контейнеризация и деплой: Dockerfile, Compose, Helm/Kubernetes, секреты и конфиги

    Контейнеризация в production‑ML сервисе — это про воспроизводимость (одинаковая сборка), управляемость (конфиг/секреты вне образа) и безопасную эксплуатацию (health/readiness, ресурсы, обновления). Архитектурные решения (слои, контракты, лимиты) считаем зафиксированными ранее; здесь — как упаковать и доставить сервис.

    1) Образ как контракт: что должно быть стабильным

    Хороший контейнерный образ — это «артефакт релиза». Стабильными должны быть:

  • Точка входа (команда запуска сервера) и набор переменных окружения.
  • Порты/health endpoints (например, /health, /ready), чтобы оркестратор мог проверять состояние.
  • Поведение при SIGTERM (graceful shutdown), иначе K8s будет убивать активные запросы при rollout.
  • Важная практика: в образе нет секретов и окруженческих значений — только код и зависимости.

    2) Dockerfile для production: быстро собирать, безопасно запускать

    2.1 Multi-stage сборка

    Multi-stage уменьшает размер образа и ускоряет кеширование:

  • Builder stage: компилируемые зависимости, сборка wheels.
  • Runtime stage: только то, что нужно для запуска.
  • Это особенно полезно для ML‑пакетов, где сборка может быть дорогой.

    2.2 Кеширование зависимостей

    Чтобы не пересобирать всё при каждом изменении кода:

  • Сначала копируйте файлы зависимостей (например, pyproject.toml/poetry.lock или requirements.txt).
  • Устанавливайте зависимости.
  • Только потом копируйте исходники.
  • Иначе любое изменение файла приложения инвалидирует слой с установкой зависимостей.

    2.3 Небезопасные дефолты, которые надо исправить

  • Запуск не от root: создайте пользователя и запускайте процесс от него.
  • Минимальные права на файловую систему: приложение должно писать только туда, где действительно нужно (часто это /tmp или примонтированный volume).
  • Явные версии: фиксируйте версии зависимостей и базового образа, иначе «вчера работало».
  • 2.4 Запуск сервера: процессная модель и таймауты

    Команда запуска должна соответствовать вашему режиму эксплуатации (кол-во воркеров, таймауты, keepalive). Детали выбора process model и лимитов мы уже разбирали в статье про высокую нагрузку — здесь важно лишь, что эти параметры должны быть настраиваемыми через env/args, а не захардкожены.

    Мини-визуализация слоёв запуска:

    2.5 Healthcheck внутри контейнера

    Даже если в Kubernetes вы используете probes, локальный HEALTHCHECK в Docker полезен для:

  • docker ps и локальной диагностики.
  • Compose‑зависимостей (ожидание готовности).
  • Health должен проверять готовность обслуживать запросы, а не просто «процесс жив». Для ML‑сервиса readiness обычно означает: модель загружена/прогрета и зависимости доступны.

    3) Docker Compose: воспроизводимая локальная среда

    Compose нужен не для production, а чтобы:

  • поднимать сервис + зависимости (Redis/БД/очередь/объектное хранилище);
  • тестировать конфиг и миграции;
  • воспроизводить интеграционные тесты локально.
  • Практики, которые окупаются:

  • Отдельные профили: например, dev (hot reload) и test (сервис + тестовые зависимости).
  • Явные volumes для данных зависимостей, чтобы перезапуск не «сбрасывал мир».
  • Единый .env только для локалки (без секретов production).
  • depends_on + healthcheck у зависимостей, чтобы приложение не стартовало «в пустоту».
  • 4) Kubernetes и Helm: базовые объекты и что важно именно для ML

    4.1 Какие объекты почти всегда нужны

  • Deployment: декларация желаемых реплик и стратегии обновления.
  • Service: стабильный адрес внутри кластера.
  • Ingress (или Gateway): входящий HTTP‑трафик.
  • ConfigMap/Secret: конфигурация и секреты.
  • Helm нужен, чтобы эти объекты были шаблонами с параметрами (values), а релиз — управляемым.

    Типовая структура chart:

    4.2 Probes: liveness vs readiness

  • Readiness probe: «можно ли слать трафик». Для ML — модель загружена, warmup завершён.
  • Liveness probe: «нужно ли перезапустить контейнер». Делайте её консервативной: частые рестарты под нагрузкой могут ухудшить ситуацию.
  • Важно: readiness должен быстро и предсказуемо отвечать, иначе вы создадите самодельный DoS на себя.

    4.3 Ресурсы и планирование

    ML‑сервис легко «съедает» память и CPU. Поэтому фиксируйте:

  • requests/limits по CPU/RAM (и GPU, если используется).
  • nodeSelector/taints/tolerations для посадки GPU‑подов на нужные ноды.
  • Ограничение параллелизма на уровне приложения и ресурсов: иначе Kubernetes даст много трафика, а вы уйдёте в деградацию (семафоры/очереди/лимиты обсуждали ранее).
  • 4.4 Обновления без простоя

  • RollingUpdate: постепенно заменяет поды.
  • PodDisruptionBudget: ограничивает одновременное «выбивание» подов (например, при обслуживании нод).
  • TerminationGracePeriodSeconds + корректный shutdown: чтобы завершить активные запросы.
  • 5) Секреты и конфиги: не путать и не смешивать

    Разделяйте:

  • Config (не секрет): лимиты, таймауты, фичи, имена очередей, URL сервисов.
  • Secrets: токены, пароли, ключи, сертификаты.
  • Практики управления конфигом:

  • Один источник правды: переменные окружения или конфиг‑файл, но с понятным приоритетом.
  • Конфиг валидируется при старте: лучше упасть сразу, чем работать «в полусломанном режиме».
  • Иммутабельность образа: конфиг и секреты подставляются при деплое.
  • Практики управления секретами:

  • Не хранить секреты в репозитории и в Docker image слоях.
  • В Kubernetes использовать Secret (минимум) или интеграцию с внешним хранилищем секретов (часто через CSI/операторы), чтобы поддерживать ротацию.
  • Не логировать секреты и не включать их в details ошибок.
  • Задания для закрепления

    1) Составьте чек‑лист из 8 пунктов для Dockerfile production‑класса именно для ML‑сервиса (размер образа, кеширование, безопасность, health).

    2) Опишите Compose‑окружение для локальной разработки: какие сервисы нужны вашему приложению, какие volumes и какие healthchecks вы добавите.

    3) Спроектируйте минимальный Helm chart: какие шаблоны нужны и какие 10 параметров вы вынесете в values.yaml.

    4) Предложите реалистичную readiness‑проверку для ML‑сервиса (что именно проверять, чтобы не было ложноположительных/ложноотрицательных).

    5) Разделите 12 параметров на config vs secrets (придумайте сами) и объясните спорные случаи.

    <details> <summary> Ответы (примерные ориентиры) </summary>

    1) Dockerfile чек‑лист (пример):

  • Multi-stage (builder/runtime).
  • Кеширование слоёв: зависимости отдельно от кода.
  • Фиксация версий (base image + зависимости).
  • Минимальный runtime (без компиляторов и dev‑утилит).
  • Запуск не от root.
  • Явные переменные/аргументы для порта и режима запуска.
  • HEALTHCHECK или документированный endpoint для probes.
  • Корректная обработка SIGTERM (через выбранный ASGI server) и достаточный graceful timeout.
  • 2) Compose‑окружение (пример):

  • app (FastAPI), redis (rate limit/кэш/idempotency), postgres (если есть БД), опционально minio (объектное хранилище), опционально worker (если есть очередь).
  • Volumes: для Postgres/MinIO, чтобы сохранять данные.
  • Healthchecks: redis-cli ping, pg_isready, для app — запрос к /ready.
  • 3) Helm chart (пример): Шаблоны: deployment, service, ingress, configmap, secret (опционально hpa, pdb). Параметры в values: image repo/tag, replicas, resources requests/limits, env config, секреты (через reference), probes пути/таймауты, ingress host/path, service port, nodeSelector/tolerations (GPU), autoscaling thresholds.

    4) Readiness (пример):

  • Проверить, что модель загружена и прогрета (флаг в памяти/статус), а также что критичные зависимости доступны (например, Redis для rate limit, если без него сервис не должен принимать трафик).
  • Не делать внутри readiness тяжёлый инференс; максимум — очень лёгкая проверка или проверка состояния модели.
  • 5) Config vs secrets (пример):

  • Config: MAX_TOKENS, REQUEST_TIMEOUT_MS, RATE_LIMIT_RPS, LOG_LEVEL, MODEL_VERSION, REDIS_HOST.
  • Secrets: REDIS_PASSWORD, DB_PASSWORD, API_KEY, JWT_SIGNING_KEY.
  • Спорное: DB_HOST и DB_USER обычно не секреты, но иногда их тоже скрывают по политике. Практика: секретом считается то, что даёт доступ; адрес обычно остаётся конфигом.
  • </details>

    7. CI/CD и наблюдаемость: GitHub Actions/GitLab CI, blue-green/canary, метрики, логи, трассировка

    CI/CD и наблюдаемость: GitHub Actions/GitLab CI, blue-green/canary, метрики, логи, трассировка

    Production ML‑сервис живёт в режиме постоянных изменений: код, конфиг, зависимости, версия модели. CI/CD и наблюдаемость — это «страховка», которая делает изменения управляемыми: вы быстро выкатываете и так же быстро понимаете, что пошло не так.

    1) CI/CD как контракт поставки

    CI/CD отвечает на три вопроса:

  • Можно ли доверять артефакту? (тесты, типы, линт, контрактные проверки).
  • Можно ли воспроизвести релиз? (сборка контейнера, тегирование, SBOM/сканирование).
  • Можно ли безопасно выкатить? (стратегия деплоя, проверки, откат).
  • Подробности про тестовые уровни, quality gates и контрактные проверки уже были в статье про тесты; здесь важно, как превратить это в последовательный пайплайн.

    2) Типовой пайплайн: stages и артефакты

    Практичная структура (одинаково применима в GitHub Actions и GitLab CI):

  • Validate
  • 1) формат/линт/типы; 2) unit‑тесты; 3) контрактный дифф OpenAPI (как артефакт).
  • Build
  • 1) сборка Docker‑образа; 2) тегирование (commit sha, semver tag); 3) публикация в registry.
  • Scan (часто параллельно)
  • 1) уязвимости образа; 2) секрет‑скан; 3) проверка лицензий (по политике).
  • Integration
  • 1) поднять зависимости (Compose или ephemeral env); 2) интеграционные тесты.
  • Deploy
  • 1) деплой в staging; 2) smoke‑проверки; 3) деплой в prod (с ручным approve или по правилам).

    Артефакты, которые стоит сохранять:

  • docker image digest (чтобы деплоить именно то, что тестировали);
  • openapi snapshot/дифф;
  • отчёты тестов (junit), покрытие, результаты сканирования.
  • 3) GitHub Actions vs GitLab CI: что важно

    GitHub Actions

    Ключевые практики:

  • Concurrency: отменять предыдущие запуски на ветке, чтобы не тратить ресурсы на устаревшие коммиты.
  • Environments: отдельные окружения staging/prod с required reviewers для деплоя.
  • Reusable workflows: вынесите сборку/тесты в переиспользуемый workflow, чтобы не копировать YAML.
  • GitLab CI

    Ключевые практики:

  • Stages + rules: строгие условия запуска (например, деплой в prod только по тегам).
  • Artifacts/Cache: кешируйте зависимости, но артефакт релиза фиксируйте образом.
  • Protected environments: защита production‑деплоя и аудит.
  • Разница не в «какой лучше», а в том, насколько дисциплинированно вы фиксируете артефакт (digest), условия деплоя и гейты.

    4) Стратегии выката: blue-green и canary

    Blue-green

    Смысл: есть два набора инстансов — blue (текущий) и green (новый). Трафик переключается целиком.

    Плюсы:

  • быстрый откат: вернуть трафик на blue;
  • простая ментальная модель.
  • Риски:

  • «скрытая несовместимость» с внешним состоянием (кэш/очередь/схемы);
  • если миграции/данные не совместимы, откат усложняется.
  • Canary

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

    Что обязательно определить заранее:

  • шаги увеличения (например, 1% → 10% → 50% → 100%);
  • метрики‑сигналы для стопа/отката (см. ниже);
  • окно наблюдения на каждом шаге.
  • Частая реализация в Kubernetes:

  • отдельные deployment для stable/canary;
  • управление весами на ingress/service mesh;
  • автоматизация прогрессивного деплоя (если используете).
  • Визуализация логики решения:

    5) Наблюдаемость: что собирать и как связать

    Вы уже закладывали trace_id и единый формат ошибок в контракте API. Наблюдаемость — это довести это до системы, где по одному идентификатору можно пройти путь запроса.

    5.1 Метрики: минимум, который даёт контроль

    Для HTTP‑сервиса удобны «золотые сигналы»:

  • Latency: p95/p99 по endpoint и по коду ответа.
  • Traffic: RPS/throughput.
  • Errors: доля 5xx, 4xx по причинам (валидатор, лимит, зависимость).
  • Saturation: очередь/семафор (сколько ждут), CPU/RAM/GPU, длина очереди задач.
  • ML‑специфика:

  • тайминги этапов (preprocess/inference/postprocess);
  • метрики лимитов/деградации (сколько запросов было отсечено);
  • cost/usage (токены, размер батча) — если это часть экономики.
  • 5.2 Логи: структурированные и пригодные для корреляции

    Правила, которые окупаются:

  • структурированные поля: trace_id, request_id, user/org, endpoint, status_code, latency_ms, model_version;
  • разделяйте события (start/end) и ошибки (с кодом и краткими details);
  • не логируйте PII/сырой контент (текст, изображения), если нет явной политики.
  • 5.3 Трассировка: единая «нить» через сервис и зависимости

    Трассировка нужна, когда есть хвосты латентности и внешние зависимости.

    Практический минимум:

  • один root span на запрос;
  • span на внешние вызовы (Redis/БД/внешний LLM);
  • span на инференс (и отдельные шаги, если это важно для диагностики);
  • обязательная привязка trace_id к логам.
  • 6) Авто‑гейты на выкаты: как связать CI/CD и наблюдаемость

    Идея: деплой не должен «верить» только тестам. Он должен проверять production‑сигналы.

    Пример гейтов для canary шага:

  • рост p95/p99 latency не более чем на X% относительно stable;
  • error rate 5xx не выше порога;
  • отсутствие всплеска 429/503 (признак перегруза/неверных лимитов);
  • saturation (очередь/семафор) не ухудшилась.
  • Если гейт не проходит — автоматический rollback трафика и остановка выката.

    ---

    Задания для закрепления

    1) Нарисуйте (текстом) пайплайн из 5–6 стадий для вашего сервиса и укажите, какие артефакты сохраняются на каждой стадии.

    2) Выберите стратегию деплоя для prod: blue-green или canary. Укажите 3 преимущества именно для ML‑инференса и 2 риска.

    3) Определите 8 метрик: 4 «золотых сигнала» и 4 ML‑специфичных. Для каждой напишите, какой инцидент она помогает диагностировать.

    4) Опишите обязательные поля структурированного лога (минимум 10), чтобы по одному запросу можно было восстановить цепочку событий.

    5) Придумайте 4 автоматических гейта для canary шага (условия «стоп/откат») и какие действия выполняются при провале.

    <details> <summary> Ответы (примерные ориентиры) </summary>

    1) Пайплайн (пример):

  • Validate: lint/format, mypy, unit → артефакты: отчёты тестов/покрытия.
  • Contract: сгенерировать OpenAPI и сравнить со snapshot → артефакт: openapi diff.
  • Build: собрать docker image → артефакт: image digest.
  • Scan: CVE/secret scan → артефакт: отчёт сканера.
  • Integration: поднять зависимости и прогнать интеграционные → артефакты: junit отчёты.
  • Deploy: staging автодеплой, prod по approve/тегу → артефакт: параметры релиза (chart version, values, digest).
  • 2) Стратегии:

  • Canary выгоден для ML, потому что:
  • 1) можно поймать деградацию p99 из‑за новой версии модели на малой доле; 2) можно оценить рост стоимости (токены/время GPU) до полного выката; 3) меньше риск массовых 5xx при неожиданном OOM/утечке.
  • Риски:
  • 1) нужно чётко определить метрики и окна наблюдения, иначе «тихий» регресс пройдёт; 2) сложнее эксплуатация (веса трафика, два набора подов).

    3) Метрики (пример):

    Золотые:

  • RPS по endpoint — видит всплески.
  • p95/p99 latency — ловит хвосты.
  • 5xx rate — регресс релиза/зависимости.
  • saturation (CPU/RAM, очередь ожидания семафора) — приближение к перегрузу.
  • ML‑специфичные:

  • inference_time_ms — деградация модели/рантайма.
  • preprocess_time_ms — рост входных данных или регресс препроцесса.
  • rejected_requests_total (лимиты/429/503) — неправильные лимиты или DDoS.
  • usage_tokens_total (или cost_units_total) — экономическая деградация.
  • 4) Поля лога (пример):

  • timestamp
  • level
  • service
  • env
  • trace_id
  • request_id
  • user_id/org_id (если есть)
  • endpoint/method
  • status_code
  • latency_ms
  • model_version
  • error_code (если ошибка)
  • 5) Гейты canary (пример):

  • p95 latency canary не хуже stable более чем на 20%.
  • 5xx rate canary < 1% и не выше stable более чем на 0.3%.
  • доля 429/503 не растёт (признак перегруза/неверных лимитов).
  • saturation: средняя длина очереди ожидания семафора < порога.
  • Действия при провале: остановить увеличение доли, вернуть вес трафика на stable, пометить релиз как failed, создать инцидент/уведомление, сохранить диагностические артефакты (логи/трейсы за окно).

    </details>