Production ML Serving: FastAPI + High Load + CI/CD (LLM/CV) end-to-end

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

1. Цели курса, стек, требования к production ML-сервису

Цели курса, стек, требования к production ML‑сервису

Зачем нужен production ML‑serving (а не просто «запустить модель»)

ML‑модель в ноутбуке решает задачу качества предсказания. Production‑сервис решает задачу стабильной поставки предсказаний в реальных условиях: высокие нагрузки, ограниченные ресурсы, требования к безопасности, воспроизводимость, наблюдаемость и быстрое обновление.

В рамках production ML‑serving модель становится частью системы, где критичны:

  • предсказуемая задержка (latency) и пропускная способность (throughput)
  • отказоустойчивость и деградация без падения
  • контроль версий, совместимость и повторяемость развёртывания
  • безопасная эксплуатация (секреты, доступы, изоляция)
  • наблюдаемость (логи/метрики/трейсы) и диагностика инцидентов
  • Цели курса (что вы должны уметь по итогам)

    Цель курса — научиться проектировать и реализовывать production‑ready ML‑сервис на FastAPI, который выдерживает нагрузку и корректно живёт в CI/CD и контейнерной среде.

    В терминах практических навыков:

  • Проектировать API для инференса (схемы запросов/ответов, контракты, версии).
  • Упаковывать ML‑инференс в сервис с управляемыми ресурсами (CPU/GPU, память, параллелизм).
  • Обеспечивать производительность: очередь/батчинг/кеширование, правильная модель конкурентности.
  • Делать сервис наблюдаемым: структурированные логи, метрики, трассировка, health/readiness.
  • Делать сервис безопасным: секреты, ограничения, rate limiting (на уровне архитектуры), защита от небезопасных входов.
  • Настраивать CI/CD и воспроизводимый деплой: тесты, сборка образов, сканирование, откаты, конфигурация.
  • Типовая архитектура production ML‑serving

    Ниже — упрощённая карта того, из чего обычно состоит «боевой» ML‑сервис.

    Важно: «runtime модели» — не только файл весов. Это ещё и зависимости, формат входа, лимиты, потоковая модель исполнения, управление памятью и политика деградации.

    Стек курса (что считается «production‑минимумом»)

    Ниже — типовой стек, который покрывает полный путь от кода до продакшна. Конкретные инструменты могут отличаться, но классы решений — одинаковы.

    API и приложение

  • FastAPI: контрактный API, валидация входа/выхода, высокая скорость разработки.
  • ASGI‑сервер (например, uvicorn/gunicorn‑worker‑модель): запуск приложения и управление воркерами.
  • Pydantic‑схемы: строгие модели данных для запросов/ответов.
  • ML‑инференс

  • PyTorch / ONNX Runtime / TensorRT (опционально): движок инференса (выбор зависит от модели и требований по latency).
  • Токенизация/препроцессинг для LLM или пайплайн изображений для CV.
  • Батчинг: объединение запросов для повышения throughput (особенно актуально для GPU).
  • Контейнеризация и деплой

  • Docker: воспроизводимая упаковка сервиса.
  • Kubernetes: масштабирование, rolling‑обновления, управление ресурсами, self‑healing.
  • CI/CD

  • пайплайн сборки и тестов
  • публикация образа
  • деплой по окружениям (dev/stage/prod)
  • политика версий и откатов
  • Наблюдаемость и эксплуатация

  • структурированные логи
  • метрики (latency, RPS, error rate, ресурсы)
  • трейсинг (где именно тратится время)
  • health/readiness probes
  • Требования к production ML‑сервису: чек‑лист по категориям

    1) Контракт API и совместимость

    Что нужно:

  • однозначные схемы запросов/ответов
  • версионирование API (чтобы не ломать клиентов)
  • строгая валидация входных данных
  • Почему это важно:

  • в ML особенно часто «прилетает» неожиданный формат данных (пустые строки, не тот тип, слишком большой payload), что может приводить к падениям или деградации качества.
  • 2) Производительность: latency, throughput, конкурентность

    Что нужно контролировать:

  • среднюю и хвостовую задержку (p95/p99)
  • максимальную нагрузку (RPS/QPS) при заданной задержке
  • модель конкурентности: сколько запросов обрабатывается параллельно
  • Типовые приёмы:

  • Батчинг: собирать несколько запросов в один прогон модели.
  • Кеширование: для повторяющихся запросов (актуально для некоторых LLM‑сценариев и эмбеддингов).
  • Ограничение входов: лимиты на размер текста/изображения, время выполнения.
  • Правильная изоляция тяжёлых частей: инференс отделять от лёгкой валидации и маршрутизации.
  • 3) Надёжность и деградация

    Production‑сервис должен уметь «не умирать красиво».

    Что нужно:

  • таймауты на уровне запросов и внутренних операций
  • контроль очереди (чтобы не съесть память при всплеске нагрузки)
  • лимиты ресурсов (CPU/GPU/RAM)
  • политики деградации: вернуть упрощённый ответ, сократить контекст, отключить дорогую постобработку, вернуть понятную ошибку
  • 4) Наблюдаемость (observability)

    Без наблюдаемости сервис невозможно поддерживать.

    Минимальный набор сигналов:

  • Логи: структурированные, с request_id/trace_id, уровни severity.
  • Метрики:
  • - RPS - latency (p50/p95/p99) - error rate по кодам - длина очереди/количество активных запросов - CPU/RAM/GPU‑utilization
  • Трейсы: разложить запрос на этапы (валидация → препроцессинг → инференс → постпроцессинг).
  • 5) Безопасность

    ML‑сервис часто принимает «сырые» данные от внешних систем — это поверхность атаки.

    Нужно учитывать:

  • аутентификация/авторизация (хотя бы на уровне окружения)
  • хранение секретов вне кода и образа
  • защита от чрезмерных входов (payload limits)
  • rate limiting и квоты (архитектурно)
  • безопасная работа с файлами/изображениями (валидация формата, ограничение размеров)
  • 6) Воспроизводимость и управление версиями

    Обязательные артефакты, которые должны версионироваться и быть воспроизводимыми:

  • код сервиса
  • зависимости
  • модель (веса) и её конфигурация
  • параметры препроцессинга/постпроцессинга
  • Зачем:

  • чтобы можно было точно повторить поведение сервиса при расследовании инцидента или откате.
  • 7) Тестируемость

    Для ML‑serving полезно разделять тесты:

  • контрактные (схемы, коды ответов, обработка ошибок)
  • интеграционные (сервис + модель + зависимости)
  • нагрузочные (проверка latency/throughput)
  • регрессионные по качеству (минимальный sanity‑набор примеров)
  • 8) Специфика LLM и CV

    LLM:

  • стоимость инференса растёт с длиной контекста и генерации → нужны лимиты и политики усечения
  • полезны кеши (prompt/embedding), потоковая отдача (если используется)
  • контроль токсичности/политик (зависит от продукта)
  • CV:

  • входы крупные → важны лимиты размеров, форматов, сжатия
  • препроцессинг может стать бутылочным горлышком (декодирование, ресайз)
  • батчинг часто даёт сильный выигрыш на GPU
  • Практический критерий «production‑ready»

    Сервис можно считать близким к production‑готовности, если вы можете:

  • объяснить, что будет при 10× росте нагрузки (масштабирование, очередь, деградация)
  • назвать как измеряется latency/ошибки и где это видно
  • быстро откатиться на предыдущую версию без ручного шаманства
  • воспроизвести сборку и запуск по одному источнику правды (репозиторий + CI)
  • ---

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

    1) Сформулируйте 5 требований к production ML‑сервису, которые не связаны напрямую с качеством модели (accuracy/F1).

    2) Опишите, какие метрики вы бы вывели для сервиса инференса и зачем каждая из них нужна (минимум 6 метрик).

    3) Придумайте политику деградации для LLM‑сервиса при перегрузке (что делать с длинными запросами, очередью и таймаутами).

    4) Составьте список артефактов, которые вы будете версионировать, чтобы обеспечить воспроизводимость.

    5) Для CV‑сервиса перечислите 4 риска безопасности/устойчивости, связанные с обработкой входных файлов, и меры снижения.

    <details> <summary> Ответы </summary>

    1) Пример 5 требований (не про качество модели):

  • ограничение размера входа и времени обработки (защита от перегруза)
  • наблюдаемость: метрики latency/error rate + корреляция запросов (request_id)
  • воспроизводимая сборка: контейнер, фиксированные зависимости
  • безопасное хранение секретов и контроль доступа
  • корректная деградация и таймауты вместо зависаний
  • 2) Пример метрик:

  • RPS/QPS: текущая нагрузка
  • latency p50/p95/p99: средняя и «хвосты», важны для SLA
  • error rate (4xx/5xx отдельно): разделить ошибки клиента и сервера
  • количество активных запросов/длина очереди: ранний сигнал перегруза
  • CPU/RAM usage: контроль ресурсов и утечек
  • GPU utilization + GPU memory: понять, упираемся ли в GPU и хватает ли памяти
  • 3) Пример деградации для LLM:

  • лимит на длину входного текста (контекст), жёсткий или адаптивный
  • при росте очереди: снижать max_tokens генерации, отключать «дорогие» опции (например, дополнительные ранжирования)
  • вводить таймаут инференса; при превышении — возвращать контролируемую ошибку и рекомендации клиенту
  • при перегрузке: отдавать 429/503 с Retry-After (если инфраструктура поддерживает), вместо того чтобы копить запросы в памяти
  • 4) Артефакты для версионирования:

  • код сервиса (включая схемы API)
  • зависимости (lockfile/constraints)
  • модельные веса и их идентификатор (hash/версия)
  • конфиги инференса (dtype, device, max_tokens, image_size и т.д.)
  • препроцессинг/постпроцессинг (словари, токенизатор, нормализация)
  • Dockerfile и манифесты деплоя
  • 5) Риски для CV и меры:

  • слишком большие файлы → лимит размера payload, ограничение разрешения после декодирования
  • «битые»/вредоносные файлы → строгая валидация формата, отказ при ошибках декодирования
  • zip-bomb/подобные эффекты (если принимаются архивы) → не принимать архивы или жёстко ограничивать распаковку
  • перегруз препроцессинга (много CPU на декодирование) → лимиты, батчинг, вынос препроцессинга, контроль параллелизма
  • </details>

    10. Pydantic v2 схемы: request/response, validation, examples

    Pydantic v2 схемы: request/response, validation, examples

    Pydantic v2 — это “контрактный слой” между внешним миром (HTTP) и вашим application core. В предыдущих материалах мы уже зафиксировали важность контракта API, формата ошибок и тонких handlers. Здесь разберём как именно на Pydantic v2 описывать request/response схемы, где проводить валидацию, как задавать ограничения и как добавлять примеры так, чтобы OpenAPI был полезным в эксплуатации.

    1) Базовый принцип: DTO ≠ доменные модели

    Pydantic‑модели в inbound HTTP слое — это DTO (Data Transfer Objects). Их задача:

  • Проверить типы/ограничения (быстро и предсказуемо).
  • Нормализовать вход (например, trim строк) на уровне транспорта.
  • Дать читаемый OpenAPI.
  • Не кладите сюда:

  • Декодирование больших изображений.
  • Токенизацию.
  • Вызовы внешних систем.
  • Эта тяжёлая логика должна быть в use case/runtime (см. архитектурные границы из прошлых статей).

    2) Request схемы: типы, ограничения, профили

    2.1. Типизация в v2: Annotated + Field

    В Pydantic v2 удобный паттерн — описывать ограничения через typing.Annotated:

    Ключевые решения:

  • extra="forbid" — запрещает неожиданные поля (частая причина “тихих” ошибок интеграции).
  • Ограничения (max_length, le, ge) — это часть политики входов и стабилизации latency.
  • Literal фиксирует допустимые режимы (профили) и упрощает маршрутизацию и SLO по профилям.
  • 2.2. Нормализация без сюрпризов: StringConstraints и str_strip_whitespace

    Если вы хотите “подчистить” вход (например, убрать пробелы по краям), делайте это явно через конфиг:

    Так вы избегаете ручных “.strip()” по всему коду и получаете единое поведение.

    2.3. Batch запросы: не «list где попало», а явная модель

    Для batch endpoint’ов хорошо работает отдельная DTO‑модель, где вы фиксируете лимиты батча:

    Почему так лучше:

  • Контракт явно говорит: “батч — это объект с полем inputs”.
  • Лимит max_length на список защищает от обхода ограничений.
  • Альтернатива v2 — RootModel[list[str]], но для HTTP контрактов чаще удобнее явный объект, чтобы позже добавлять options без breaking change.

    3) Валидация: field_validator и model_validator

    3.1. field_validator: проверка одного поля

    Пример: запретить “пустой текст после нормализации”:

    Практика:

  • Держите validator’ы лёгкими.
  • Ошибки делайте стабильными (в HTTP‑адаптере вы всё равно приведёте их к вашему error_code).
  • 3.2. model_validator: проверка согласованности полей

    Пример: разные лимиты для разных профилей:

    Это полезно, когда простых le/ge недостаточно.

    3.3. Не прячьте “бизнес‑правила” в DTO

    DTO‑валидация должна отвечать на вопрос: “запрос обрабатываемый и безопасный по ресурсам?”.

    Политики более высокого уровня (деградация, динамические лимиты, class‑of‑service) обычно живут в use case.

    4) Response схемы: стабильность, сериализация, “конверт”

    4.1. Стабильный ответ‑конверт

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

    Плюсы:

  • У ответа появляется единый “скелет”.
  • Метаданные (request_id, model) доступны без разборов вложенных структур.
  • 4.2. Управление выдачей: exclude_none, by_alias

    В Pydantic v2 сериализация делается через model_dump(...).

    Рекомендации:

  • Убирайте None поля, если они не несут смысла в контракте:
  • - resp.model_dump(exclude_none=True)
  • Если используете алиасы (например, requestId для внешних клиентов), делайте это централизованно:
  • - поля с alias=... - сериализация by_alias=True

    Важно: не меняйте style/alias хаотично — это быстро превращается в breaking change.

    5) Примеры (examples): чтобы OpenAPI был операционно полезным

    Примеры нужны не “для красоты”, а чтобы:

  • клиент быстро понял, как вызвать endpoint;
  • вы закрепили типовые ошибки/edge‑cases;
  • тесты могли использовать эти примеры как golden‑входы.
  • 5.1. Примеры на уровне полей

    5.2. Примеры на уровне модели (json_schema_extra)

    Практика: держите 1–2 “эталонных” примера на модель. Слишком много примеров делают схему шумной.

    6) Производительность и предсказуемость валидации

    В high load важен принцип: валидация должна быть дешёвой.

  • Предпочитайте встроенные ограничения Field(...) вместо тяжёлых validator’ов.
  • Если вы валидируете “большие” поля (например, base64), делайте это в два слоя:
  • 1. на DTO: ограничить размер строки (защита от огромного payload); 2. дальше: более дорогая проверка/декодирование уже в сервисном слое, под контролем очереди/лимитов.
  • extra="forbid" часто экономит время на отладке и снижает риск неожиданных входов.
  • 7) Ошибки валидации: как сделать их пригодными для клиентов

    Pydantic отдаёт структурированные ошибки, но их формат не должен стать вашим публичным контрактом.

    Практика для production:

  • В HTTP‑адаптере перехватывайте RequestValidationError.
  • Маппите в ваш единый error‑формат с стабильным error_code.
  • В details можно положить “сжатое” описание: список полей и причин.
  • Так вы сможете менять внутреннюю структуру DTO/валидации без поломки клиентов.

    ---

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

    1) Спроектируйте Pydantic v2 DTO EmbedRequest для /v1/embeddings:

  • поле inputs: список строк
  • лимиты: 1..64 элементов, каждая строка 1..8000 символов
  • запрет неизвестных полей
  • 2) Добавьте model_validator, который запрещает одновременно:

  • profile="fast"
  • normalize=true
  • Объясните, почему такое правило лучше держать в DTO, а не в runtime.

    3) Добавьте примеры (2 штуки) на уровень модели для GenerateRequest: один короткий запрос, один “тяжёлый” на границе лимита.

    4) Опишите, какие 3 проверки вы бы сделали “дешёвыми” (в DTO), а какие 2 проверки — “дорогими” (в use case), для CV endpoint, который принимает изображение как base64.

    <details> <summary> Ответы </summary>

    1) Пример DTO:

  • inputs: Annotated[list[Annotated[str, Field(min_length=1, max_length=8000)]], Field(min_length=1, max_length=64)]
  • model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
  • Так вы одновременно:

  • ограничиваете размер батча;
  • ограничиваете размер каждого элемента;
  • не принимаете неожиданные поля.
  • 2) Пример model_validator:

  • Если profile == "fast" and normalize is True: ошибка.
  • Почему это можно держать в DTO:

  • правило относится к совместимости параметров запроса (контракт и политика режима), а не к механике исполнения;
  • клиент получает быстрый и понятный отказ ещё до очереди/рантайма;
  • это снижает нагрузку: “плохие” комбинации не доходят до дорогих стадий.
  • 3) Примеры на модель:

  • короткий: text="Hi", max_tokens=32, temperature=0.7.
  • тяжёлый: text длиной близко к max_length (в примере можно показать “...”), profile="quality", max_tokens близко к верхней границе.
  • Важно: в OpenAPI примере “тяжёлый” запрос демонстрирует границы лимитов, но не обязан содержать реально огромную строку — достаточно показать намерение и прокомментировать ограничение в description.

    4) Для CV base64.

    Дешёвые (DTO):

  • ограничение длины base64‑строки (защита от огромного payload);
  • проверка, что строка не пустая;
  • проверка enum/профиля (например, fast/quality) и простых численных параметров (размеры/thresholds).
  • Дорогие (use case):

  • декодирование base64 в bytes (может быть CPU‑дорогим и выделяет память);
  • декодирование изображения (JPEG/PNG) и проверка реального размера/разрешения после декодирования (именно это влияет на RAM/CPU и иногда на безопасность).
  • </details>

    11. Обработка ошибок: единый error model, exception handlers

    Обработка ошибок: единый error model, exception handlers

    В production ML‑serving ошибки — это часть публичного контракта и одновременно инструмент эксплуатации. Если в разных местах сервиса ошибки оформлены «как получится», то:

  • клиентам сложно корректно ретраить и диагностировать проблемы;
  • поддержке трудно связывать инциденты с конкретными причинами;
  • наблюдаемость распадается: метрики и алерты не группируются по стабильным признакам.
  • В предыдущих статьях мы уже фиксировали необходимость единого формата ошибок и стабильных error_code, а также разделение домена и HTTP‑слоя. Здесь фокус: как спроектировать единый error model и реализовать его через exception handlers в FastAPI, не ломая архитектурные границы.

    ---

    1) Единый error model: что именно стабилизируем

    1.1. Минимальный «конверт» ошибки

    Рекомендуемый минимальный формат (поля и смысл):

  • error_code: стабильный машинно‑читаемый код (на него опираются клиенты, метрики, алерты)
  • message: человеко‑читаемое сообщение (не должно быть контрактом)
  • details: структурированные детали (опционально), чтобы клиент мог понять что именно не так
  • request_id: корреляция с логами/трейсами
  • Практическое правило: контракт — это error_code + структура details, но не текст message.

    1.2. Границы ответственности

    Чтобы не размазать логику, держим роли так:

  • Домен / use case: формирует причину как доменную ошибку (например, InputTooLarge, Overloaded). Никаких HTTP‑кодов.
  • Inbound HTTP‑адаптер: превращает доменную ошибку в HTTP‑ответ (код + error model).
  • Exception handlers: обеспечивают единообразие для всех «нештатных» путей (валидация, неожиданные исключения, таймауты), чтобы не было разных форматов в разных endpoint’ах.
  • ---

    2) Дизайн error_code: как сделать коды полезными

    2.1. Свойства хорошего error_code

    Хороший error_code:

  • стабилен (не меняется из‑за рефакторинга)
  • группирует причины (например, input_too_large, overloaded, runtime_error)
  • не «переобъясняет» HTTP‑код (не нужно bad_request_400)
  • пригоден для метрик (низкая кардинальность, то есть не содержит динамики)
  • 2.2. Нормализация details

    details стоит делать предсказуемым, иначе клиенты и аналитика начнут «парсить текст». Типовые структуры:

  • для лимитов: { "limit": 8192, "got": 12000, "unit": "chars", "field": "text" }
  • для перегруза: { "queue": "gpu", "retry_after_ms": 200 }
  • для валидации: { "errors": [{"field": "inputs[3]", "reason": "min_length"}] }
  • Важно: не класть в details чувствительные данные (промпты, изображения, токены).

    ---

    3) Ошибки в домене: типы вместо «строк»

    Ниже — типовой паттерн для ядра (домен/приложение):

    Ключевые моменты:

  • домен возвращает причину (code, details), а не транспортную форму;
  • одна ошибка = один error_code (стабильно для метрик и клиентов);
  • детали ограничены и без PII.
  • ---

    4) HTTP‑маппинг: как превратить доменную ошибку в ответ

    4.1. ErrorResponse DTO

    4.2. Единый маппинг кодов

    Сведите все решения «какой HTTP‑код» в одно место:

    Почему это важно:

  • вы не размазываете решения по handlers;
  • изменение политики не требует поиска по всему проекту;
  • проще писать контрактные тесты.
  • ---

    5) Exception handlers в FastAPI: как обеспечить единообразие

    Цель: любой неуспех должен превратиться в ваш ErrorResponse.

    5.1. Request ID как зависимость error model

    Обязательное условие: у вас должен быть доступный request_id (обычно через middleware). В handler’ах мы будем его читать из request.state.

    5.2. Регистрация обработчиков

    Практическое правило: handler для Exception — последняя линия обороны. Он должен:

  • возвращать безопасное сообщение;
  • не ломать формат ответа;
  • не мешать логированию/алертингу.
  • ---

    6) Логи и метрики ошибок: что делать внутри handler’ов

    Не превращайте error handler в «лог‑помойку», но базовый минимум полезен:

  • логировать error_code, request_id, path, method;
  • для internal_error логировать stacktrace (внутрь логов, не клиенту);
  • метрика счётчика ошибок по error_code и endpoint.
  • Критично: не включать в логи сырые промпты/изображения по умолчанию.

    ---

    7) Async/job‑based: как не потерять единый error model

    В job‑based режиме ошибка «живет» в результате job. Важно сохранить те же принципы:

  • внутри worker/use case формируется доменная ошибка с code/details;
  • при сохранении результата job вы сохраняете структурированную ошибку (в том же error model или очень близком);
  • GET /jobs/{id} возвращает:
  • - status=failed - error={error_code, message, details, request_id}

    Так клиенты видят одинаковые причины ошибок независимо от sync/async режима.

    ---

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

    1) Придумайте 6 error_code для ML‑serving сервиса (LLM или CV): 1. 2 кода для ошибок входа 2. 2 кода для перегруза/ресурсов 3. 2 кода для runtime/внутренних ошибок

    2) Для каждого из двух доменных исключений (InputTooLarge, Overloaded) определите: 1. HTTP‑код 2. минимальный details

    3) Опишите правила логирования в exception handler’ах: 1. какие 5 полей логировать всегда 2. что категорически нельзя логировать по умолчанию

    4) У вас есть и sync endpoint, и async job‑based. Опишите, как вы обеспечите «одинаковость» ошибок для клиента: что будет общим, а что может отличаться.

    <details> <summary> Ответы </summary>

    1) Пример error_code:

  • Ошибки входа:
  • - validation_error (DTO/контракт не прошёл) - input_too_large (лимиты размера)
  • Перегруз/ресурсы:
  • - overloaded (backpressure отказал) - timeout (внутренний таймаут вычислений/очереди)
  • Runtime/внутреннее:
  • - runtime_error (ошибка движка инференса, без деталей наружу) - internal_error (неожиданное исключение)

    2) Пример маппинга:

  • InputTooLarge:
  • - HTTP: 413 - details: { "field": "text", "limit": 8192, "got": 12000, "unit": "chars" }
  • Overloaded:
  • - HTTP: 429 (или 503 по вашей политике, но выберите один вариант и закрепите) - details: { "retry_after_ms": 200 } (или пусто, если не можете оценить)

    3) Логирование:

  • Всегда:
  • - request_id - error_code - path - method - status_code

  • Нельзя по умолчанию:
  • - полный prompt/текст пользователя - base64 изображений или бинарные данные - секреты (API keys, токены) - любые PII/чувствительные поля без явного режима отладки и маскирования

    4) Одинаковость sync vs async:

  • Общее:
  • - одинаковые error_code - одинаковая структура details (по тем же кодам) - наличие request_id для корреляции

  • Отличия:
  • - в sync ошибка приходит как HTTP‑ответ сразу; - в async ошибка — часть результата job (status=failed) и возвращается при чтении статуса (обычно HTTP 200 для самого запроса статуса, а ошибка — внутри payload).

    </details>

    12. Логирование: structlog, JSON-логи, correlation id

    Логирование: structlog, JSON-логи, correlation id

    Production ML‑serving сервис под нагрузкой живёт в логах: по ним ищут причины p99, разбирают инциденты, подтверждают деградацию, связывают ошибки клиента с конкретным воркером/версией модели. В прошлых статьях мы уже фиксировали требования к observability, единому формату ошибок и необходимости request_id. Здесь — практическая дисциплина структурированного логирования: structlog, JSON‑логи, correlation id (и как не утонуть в объёме/PII).

    1) Почему “просто print()” и текстовые логи ломают эксплуатацию

    Текстовый лог удобен человеку, но плохо работает как сигнал:

  • По нему сложно строить агрегации: “все ошибки overloaded по endpoint’у за 5 минут”.
  • Его тяжело коррелировать: один запрос порождает 10 строк в разных модулях, а собрать их без общего ключа невозможно.
  • Под high load «красивое сообщение» часто превращается в шум, а полезные признаки (версия модели, профиль, размер входа, тайминги стадий) отсутствуют.
  • Итог: в production лог должен быть событием с полями, а не строкой.

    2) JSON‑логи: контракт для вашей эксплуатации

    2.1. Базовая идея

    Одна запись = один JSON‑объект. Любая система сбора логов (stdout контейнера, агент, централизованное хранилище) почти всегда умеет:

  • парсить JSON,
  • индексировать ключи,
  • фильтровать и агрегировать.
  • 2.2. Минимальная схема (что должно быть почти всегда)

    Сделайте “скелет” полей, который будет в каждом событии:

  • timestamp — время события
  • level — уровень (info, warning, error)
  • event — короткое имя события (не длинный текст)
  • service — имя сервиса
  • env — окружение (dev/stage/prod)
  • request_id — correlation id запроса
  • path, method, status_code — для HTTP событий
  • model / model_version — если событие относится к инференсу
  • Полезное правило: event должен быть стабильным, как error_code (из статьи про ошибки). Менять текст можно, менять “имя события” — нежелательно.

    2.3. Пример записи (как её должен “видеть” лог‑сборщик)

    Важно: вы не обязаны логировать все поля всегда. Но базовые — должны быть доступны, иначе инциденты будут “слепыми”.

    3) structlog: зачем он нужен поверх стандартного logging

    logging в Python умеет уровни и хендлеры, но структурированность и контекст “на каждый запрос” становится неудобной.

    structlog добавляет:

  • удобный API log.info("event", key=value, ...) вместо ручной сборки JSON;
  • pipeline “процессоров” (добавить timestamp, level, request_id, сериализацию);
  • нормальную работу с контекстом (contextvars) в async‑коде.
  • Практический принцип: используйте стандартный logging как транспорт (хендлеры/уровни), а structlog — как способ формировать события.

    4) Correlation ID: request_id как обязательный контекст

    4.1. Что именно коррелируем

    В serving минимум нужен request_id, который:

  • приходит от клиента (заголовок) или генерируется на входе,
  • кладётся в контекст запроса,
  • автоматически добавляется в каждый лог в рамках этого запроса,
  • возвращается клиенту в ответе.
  • Это связывает:

  • HTTP‑логи,
  • логи use case/runtime,
  • error‑events,
  • (опционально) метрики/трейсы.
  • Сама идея request_id и его роль уже обсуждалась в статьях про observability и ошибки — здесь важна реализация без ручного прокидывания параметра.

    4.2. Реализация без “протечки” по функциям

    Под high load вы не хотите писать везде logger.info(..., request_id=request_id) вручную. Делайте так:

  • На входе (middleware) определяете request_id.
  • Кладёте его в request.state (для FastAPI/Starlette) — чтобы endpoint’ы могли его достать.
  • Параллельно кладёте его в structlog contextvars — чтобы любой лог внутри обработки запроса автоматически получил request_id.
  • Схема потока:

    4.3. request_id vs trace_id

    Если у вас есть распределённый трейсинг, там появляется trace_id. Практика:

  • request_id — ваш сервисный correlation id (понятный клиентам).
  • trace_id — сквозной id распределённого трейса.
  • Они могут совпадать, но чаще это разные сущности. Главное — чтобы в логах были оба (если трейсинг включён).

    5) Middleware для логов: два разных типа событий

    Не превращайте middleware в «генератор всего подряд». Обычно достаточно двух событий:

  • http_request_started (редко нужен, если есть завершение)
  • http_request_finished (почти всегда нужен)
  • Что логировать на завершении:

  • status_code
  • latency_ms (server-side)
  • path, method
  • request_id
  • признаки нагрузки, которые не содержат PII: payload_bytes, profile, batch_size (если известно)
  • Ошибки (исключения) лучше логировать в exception handler’ах/обработчиках (см. статью про единый error model), но HTTP middleware может быть “страховкой”, чтобы фиксировать 5xx даже если что-то пошло не так.

    6) Политика полей: низкая кардинальность и запрет PII

    6.1. Кардинальность: что нельзя превращать в метку

    В логах можно хранить много, но для агрегаций опасны поля с высокой кардинальностью:

  • полный URL с параметрами,
  • сырой текст prompt,
  • user_id (если пользователей миллионы),
  • случайные строки ошибок.
  • Их можно логировать только:

  • в маскированном виде,
  • или в отдельном debug‑канале,
  • или по семплингу.
  • 6.2. PII/секреты

    Правило по умолчанию: не логировать входы модели.

    Вместо этого логируйте безопасные производственные признаки:

  • размер текста (символы/оценка токенов),
  • размер изображения (байты, ширина/высота после декодирования — если уже известно),
  • выбранный профиль,
  • включённые опции.
  • Если вам нужен “кусок входа” для расследований:

  • делайте это через feature‑flag,
  • маскируйте,
  • пишите в отдельный лог‑поток/хранилище с ограниченным доступом.
  • 7) Уровни логирования и семплинг

    Под high load логи легко становятся DoS‑ом для вашей инфраструктуры.

    Практичная политика:

  • info — только важные бизнес‑события и завершение HTTP запроса (с метаданными).
  • warning — отказ по политике (лимиты, деградация, overloaded).
  • error — неожиданные ошибки runtime/инфраструктуры.
  • debug — выключен в production или включается точечно.
  • Семплинг:

  • логировать http_request_finished для 100% запросов можно, но иногда достаточно 10–20% при очень большом RPS;
  • ошибки обычно логируют всегда;
  • для “успешных инференсов” можно делать sampling по request_id (стабильно), чтобы сохранять репрезентативность.
  • 8) Практический wiring structlog (что должно быть в конфигурации)

    В конфиге structlog обычно есть:

  • процессор timestamp
  • процессор уровня
  • процессор добавления “статических” полей (service, env, version)
  • интеграция с contextvars (чтобы request_id подмешивался автоматически)
  • JSON renderer (финальная сериализация)
  • Важно: конфиг должен быть единым для сервиса и вызываться один раз на старте процесса (обычно в bootstrap).

    ---

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

    1) Спроектируйте минимальную JSON‑схему для события inference_failed (минимум 10 полей). Разделите поля на:

  • обязательные всегда
  • опциональные
  • 2) Придумайте правило генерации/приёма request_id на входе:

  • какие заголовки поддерживаем
  • как валидируем формат
  • что делаем, если заголовок “кривой”
  • 3) Назовите 6 примеров полей, которые почти всегда приводят к утечке PII/секретов, если их логировать бездумно.

    4) У вас 2000 RPS на /v1/generate. Какие 3 меры вы введёте, чтобы логирование не стало бутылочным горлышком?

    <details> <summary> Ответы </summary>

    1) Пример inference_failed:

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

  • timestamp
  • level = error
  • event = inference_failed
  • service
  • env
  • request_id
  • path
  • method
  • status_code (например, 500/503/429)
  • error_code (стабильный код, согласованный с error model)
  • Опциональные:

  • model, model_version
  • profile
  • latency_ms
  • queue_wait_ms
  • batch_size
  • retry_after_ms (если overload)
  • runtime (например, onnxruntime/pytorch) — без внутренних деталей
  • 2) Правило request_id:

  • Поддерживаем входящие: X-Request-ID (основной) и, если нужно, X-Correlation-ID.
  • Валидируем: длина (например, 8–128), допустимые символы (base32/hex/UUID), запрет пробелов.
  • Если “кривой”: игнорируем и генерируем новый; в логах можно добавить поле request_id_source=generated.
  • 3) Поля‑риски:

  • полный текст prompt/ввод пользователя
  • base64 изображений/файлов
  • заголовки Authorization, API keys, токены
  • cookies
  • email/телефон/адрес (любые PII поля)
  • “сырой” exception message от внешних библиотек, если он включает куски входа
  • 4) Меры для 2000 RPS:

  • Семплинг успешных запросов (например, 10–20%), ошибки — 100%.
  • Минимизировать объём события: не логировать большие структуры, только метаданные и числа.
  • Не делать тяжёлых вычислений ради логов (например, не считать хэши огромных payload’ов в горячем пути, не форматировать строки заранее; использовать структурированные поля).
  • </details>

    13. Конфигурация: pydantic-settings, секреты, 12-factor

    Конфигурация: pydantic-settings, секреты, 12-factor

    Конфигурация в production ML-serving — это не «набор констант», а управляемый механизм, который обеспечивает воспроизводимость, безопасность и предсказуемое поведение под нагрузкой. Архитектурный смысл: всё, что влияет на поведение сервиса, должно быть явно настроено и валидировано, а не «прочитано из env где-то по месту».

    В предыдущих статьях мы уже закрепили идею app factory, DI и typed settings, а также воспроизводимость сборок и pinned deps. Здесь сфокусируемся на том, как именно устроить конфигурацию, как безопасно обращаться с секретами и как это соотносится с принципами 12-factor.

    ---

    1) 12-factor применительно к ML-serving: какие пункты реально важны

    Из 12-factor для serving-сервиса (особенно LLM/CV) практически критичны следующие идеи:

  • Config хранится в окружении, а не в коде и не в образе.
  • 1. «Окружение» в Kubernetes — это обычно ConfigMap/Secret + переменные окружения/смонтированные файлы.
  • Чёткое разделение окружений (dev/stage/prod) без ветвлений логики в коде.
  • 1. Один и тот же артефакт (образ) должен запускаться в разных окружениях за счёт конфигурации.
  • Fail fast при неверной конфигурации.
  • 1. Если забыли переменную, секрет или указали некорректный лимит — сервис должен не «работать как-нибудь», а падать при старте.
  • Строгое отделение секретов от несекретных настроек.
  • 1. Лимиты, профили, хосты — это config. 2. API keys, пароли, токены — это secrets.

    Важно: 12-factor не запрещает config-файлы как таковые. Он требует, чтобы источник правды был внешним по отношению к коду и одинаково работал во всех средах.

    ---

    2) Зачем pydantic-settings в production

    pydantic-settings (Pydantic v2) решает три задачи, которые в ML-serving особенно полезны:

  • Типизация и валидация
  • 1. Лимиты (max_tokens, max_image_side, timeout_ms) валидируются при старте. 2. Ошибки не «всплывают» под нагрузкой в runtime.
  • Единая точка входа к конфигурации
  • 1. Вы читаете env один раз и раздаёте значения через DI (см. подход app factory/контейнер из предыдущих статей).
  • Согласованность между компонентами
  • 1. Например: если включён batching, должны быть заданы его параметры; если включён Redis — должны быть заданы host/port и секрет.

    Практическая цель: конфигурация — это часть контракта эксплуатации, почти как OpenAPI для клиентов.

    ---

    3) Структура Settings: как не превратить её в «помойку»

    В production ошибка — делать один гигантский Settings на 200 полей без структуры. Лучше — группировать по смысловым контурам сервиса.

    Рекомендуемая декомпозиция:

  • ServiceSettings: имя сервиса, окружение, версия, режим (debug/strict).
  • ApiSettings: лимиты HTTP (payload size), CORS, timeouts на уровне API (если есть).
  • InferenceSettings:
  • 1. профиль инференса по умолчанию, 2. лимиты LLM/CV, 3. таймауты стадий (например, инференс).
  • RuntimeSettings: device, dtype, количество параллельных инференсов, batch window/max batch.
  • IntegrationsSettings: Redis/брокер/S3 и т.п.
  • ObservabilitySettings: включённость метрик/трейсинга, уровни логов (детали логирования были в отдельной статье).
  • Ключевой принцип: Settings отражает архитектуру (контуры API/очередь/runtime/observability), а не набор случайных переменных.

    ---

    4) pydantic-settings: практические паттерны

    Ниже — паттерны, которые обычно нужны в serving.

    4.1. Явный префикс env-переменных

    Вместо TIMEOUT_MS лучше APP_TIMEOUT_MS или ML_SERVING_TIMEOUT_MS.

    Зачем:

  • меньше конфликтов в окружении,
  • проще работать с несколькими сервисами,
  • легче искать переменные.
  • В pydantic-settings это делается через конфиг (env_prefix).

    4.2. Строгая валидация и запрет «магии»

    Практика:

  • дефолты только там, где они безопасны,
  • критичные вещи (адреса интеграций, секреты, идентификатор активной модели) — обязательны,
  • логическая валидация через model validators:
  • 1. «если включён Redis cache → нужны host/port + secret», 2. «если streaming включён → ограничения времени стрима обязательны».

    4.3. Источники конфигурации и их приоритет

    Чаще всего вы хотите такой приоритет (от самого сильного к слабому):

  • env переменные (оператор/деплой решает),
  • secrets directory (смонтированные секреты файлами),
  • .env файл (только локальная разработка),
  • значения по умолчанию в коде.
  • Важно: .env — это удобство, но в production его обычно не используют, чтобы не было «скрытого второго источника правды».

    4.4. Кэширование Settings

    Создание Settings() — недорогая операция, но читать env «в каждом запросе» — плохая дисциплина. Практика:

  • создать settings один раз при старте процесса,
  • положить в контейнер/app.state,
  • использовать через DI.
  • ---

    5) Секреты: модель угроз и практическая дисциплина

    5.1. Что считаем секретом

    Типовые секреты в ML-serving:

  • токены доступа к model registry / artifact storage,
  • креды Redis/брокера,
  • API keys для внешних провайдеров (LLM, moderation, feature-store),
  • private keys для webhook подписи.
  • Не путайте с «просто конфигом»:

  • MAX_TOKENS, BATCH_WINDOW_MS, MODEL_ID — не секреты, но критичны для поведения.
  • 5.2. Где хранить секреты

    Здоровые варианты:

  • Kubernetes Secret:
  • 1. как env var, 2. или как смонтированный файл (часто лучше для крупных токенов/сертификатов и для ротации).
  • Docker secrets (если не Kubernetes).
  • Secret manager (Vault и аналоги) — когда нужна централизованная ротация и аудит.
  • Нездоровые варианты:

  • секрет в репозитории,
  • секрет в Docker image,
  • секрет в логах.
  • 5.3. pydantic-settings и secrets directory

    У pydantic-settings есть удобная концепция чтения секретов из директории (когда секреты смонтированы файлами). Практический смысл:

  • вы не тащите секреты через env (где их проще случайно утечь в диагностику окружения),
  • ротация может происходить через обновление файлов и рестарт/rolling update.
  • 5.4. Ротация секретов и «срок жизни процесса»

    Serving-сервис почти всегда читает секреты при старте и держит клиентов (Redis/S3) живыми. Отсюда правило:

  • если секреты ротируются — чаще всего нужен rolling restart,
  • либо нужно специально проектировать переподключение клиентов.
  • Для начала курса достаточно принять: ротация = обновили Secret + rolling restart.

    ---

    6) Конфигурация и безопасность: что запрещено делать

    Минимальные запреты (как часть инженерной политики):

  • не логировать значения секретов (и любые структуры, которые могут их содержать),
  • не возвращать секреты в ошибках/диагностике,
  • не делать «debug endpoint», который печатает env,
  • не хранить секреты в настройках, которые сериализуются целиком (например, settings.model_dump() в логи).
  • Рекомендация: если нужно логировать конфигурацию при старте, логируйте только whitelist полей (например, env, имя сервиса, активная модель, лимиты), и никогда — «всё подряд».

    ---

    7) Конфигурация как контракт эксплуатации: что обязано быть зафиксировано

    Чтобы конфигурация не превращалась в хаос, зафиксируйте «обязательный минимум»:

  • Схема переменных:
  • 1. имена, 2. типы, 3. дефолты, 4. какие обязательны.
  • Политика окружений:
  • 1. какие значения меняются между dev/stage/prod, 2. что запрещено менять без релиза (например, некоторые параметры runtime могут требовать прогрева).
  • Связь с SLO/NFR:
  • 1. лимиты входа и timeouts — это часть выполнения latency/SLO, 2. batching/параллелизм — часть throughput.

    Идеально, когда изменения «опасных» параметров (batch window, max_tokens, concurrency) проходят через review так же строго, как изменения кода.

    ---

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

    1) Спроектируйте набор переменных окружения (12–16 штук) для ML-serving сервиса, разделив их на группы:

  • service/api
  • inference limits
  • runtime/batching
  • integrations
  • observability
  • 2) Для 6 переменных укажите:

  • тип,
  • допустимый диапазон/ограничение,
  • дефолт (если нужен),
  • является ли переменная секретом.
  • 3) Опишите приоритет источников конфигурации в вашем сервисе (env, .env, secrets dir, defaults) и объясните, почему именно такой порядок.

    4) Придумайте 5 правил «fail fast» для старта сервиса (какие ошибки конфигурации должны приводить к немедленному падению).

    <details> <summary> Ответы </summary>

    1) Пример набора переменных:

    Service/API:

  • APP_ENV (dev/stage/prod)
  • APP_SERVICE_NAME
  • APP_API_VERSION
  • APP_HTTP_MAX_PAYLOAD_BYTES
  • Inference limits:

  • APP_LLM_MAX_INPUT_CHARS
  • APP_LLM_MAX_OUTPUT_TOKENS
  • APP_CV_MAX_IMAGE_SIDE
  • APP_INFER_TIMEOUT_MS
  • Runtime/batching:

  • APP_RUNTIME_DEVICE (cpu/cuda)
  • APP_RUNTIME_DTYPE (fp16/fp32)
  • APP_MAX_CONCURRENCY
  • APP_BATCH_WINDOW_MS
  • APP_MAX_BATCH_SIZE
  • Integrations:

  • APP_REDIS_HOST
  • APP_REDIS_PORT
  • APP_REDIS_PASSWORD (секрет)
  • Observability:

  • APP_LOG_LEVEL
  • APP_METRICS_ENABLED
  • 2) Пример детализации 6 переменных:

  • APP_HTTP_MAX_PAYLOAD_BYTES: int, диапазон 1e5..5e7, дефолт зависит от продукта (например 5_000_000), не секрет.
  • APP_LLM_MAX_OUTPUT_TOKENS: int, 1..4096, дефолт 256, не секрет.
  • APP_INFER_TIMEOUT_MS: int, 50..60000, дефолт 5000, не секрет.
  • APP_BATCH_WINDOW_MS: int, 0..50 (для micro-batch), дефолт 0 или 5, не секрет.
  • APP_REDIS_PORT: int, 1..65535, дефолт 6379, не секрет.
  • APP_REDIS_PASSWORD: str, ограничение длины (например 8..256), дефолта нет, секрет.
  • 3) Пример приоритета:

    1) env vars (K8s ConfigMap/Secret как env) — главный операционный канал 2) secrets directory (смонтированные файлы) — секреты/сертификаты, удобнее для безопасности 3) .env — только локальная разработка 4) defaults — только безопасные значения

    Смысл: production управляется деплоем, локальная разработка не должна становиться «скрытым источником правды».

    4) Пример 5 правил fail fast:

  • Если включён Redis cache, но нет APP_REDIS_HOST — падать.
  • Если включён Redis cache, но нет APP_REDIS_PASSWORD (когда требуем auth) — падать.
  • Если APP_MAX_BATCH_SIZE > 1, но APP_BATCH_WINDOW_MS не задан/некорректен — падать.
  • Если APP_LLM_MAX_OUTPUT_TOKENS превышает допустимое для профиля runtime — падать (иначе получите нестабильность и OOM).
  • Если APP_ENV=prod и APP_LOG_LEVEL=debug — падать или принудительно понижать (по вашей политике), чтобы не допустить утечек/перегруза логами.
  • </details>

    14. Безопасность API: OAuth2/JWT, mTLS, CORS, rate limits

    Безопасность API: OAuth2/JWT, mTLS, CORS, rate limits

    Безопасность production ML‑serving — это не «прикрутить авторизацию». Это контроль того, кто и на каких условиях тратит ваши вычислительные ресурсы, и как сервис ведёт себя под атакой/ошибочной интеграцией. Формат ошибок, request_id/correlation и лимиты входа мы уже фиксировали в прошлых статьях — здесь фокус на механизмах доступа и защиты периметра: OAuth2/JWT, mTLS, CORS и rate limiting.

    1) Модель угроз для ML‑serving (коротко и практично)

    В serving наиболее частые классы проблем:

  • Неавторизованный доступ: сервис случайно доступен «всем».
  • Кража/утечка токенов: ключ попал в логи, фронтенд, репозиторий.
  • Abuse по стоимости: легитимный клиент выжигает GPU длинными запросами (denial‑of‑wallet).
  • DoS и «шумовые» ретраи: всплеск запросов ломает p99 и приводит к каскадным ошибкам.
  • Browser‑риски: неправильно настроенный CORS позволяет чужому сайту дергать ваш API из браузера пользователя.
  • Из этого следует принцип: авторизация и лимитирование — часть SLO и эксплуатации, а не только security‑чеклист.

    2) OAuth2 и JWT: где заканчивается протокол и начинается эксплуатация

    2.1. Что обычно нужно в ML‑serving

    В production чаще всего встречаются два сценария:

  • Machine‑to‑machine (M2M): сервис‑клиент получает токен по OAuth2 Client Credentials и ходит в ваш inference API.
  • User‑delegated (реже для serving напрямую): фронтенд/бекенд действует от лица пользователя.
  • Для high load inference критично, чтобы проверка токена:

  • была локальной (JWT) без сетевого вызова на каждый запрос,
  • имела кеширование ключей (JWKS) и понятную ротацию,
  • давала стабильные признаки для метрик/квот (tenant/client_id).
  • 2.2. JWT валидация: что именно проверять

    JWT «просто декодировать» недостаточно. Минимальный набор проверок:

  • Подпись: проверка по публичному ключу провайдера.
  • issuer (iss): токен действительно от вашего IdP.
  • audience (aud): токен предназначен этому API (иначе возможен token confusion).
  • сроки (exp, опционально nbf): токен не просрочен и уже действителен.
  • алгоритм: запрещайте неожиданные/неподдерживаемые алгоритмы.
  • Поля из токена, которые полезно извлекать для serving:

  • sub или client_id — идентификатор субъекта (для квот/логов),
  • scope/permissions — права (например, доступ к /generate vs /embeddings),
  • tenant/organization claim (если multi‑tenant).
  • Важно: не логируйте сам JWT. В логах используйте только безопасные идентификаторы (например, client_id, tenant_id) и request_id.

    2.3. Scopes/permissions как часть контракта

    Для ML‑serving удобно мыслить доступом по «возможностям», например:

  • inference:embeddings
  • inference:generate
  • admin:metrics (если метрики/статусы закрыты)
  • Практика: разные endpoint’ы имеют разную стоимость и риск. Разделяйте права, чтобы не выдавать всем «генерацию на GPU» только потому что нужен эмбеддинг.

    2.4. Где выполнять auth: gateway vs приложение

    Рекомендуемая модель в production:

  • API Gateway / Ingress выполняет:
  • 1. базовую аутентификацию (JWT/mTLS), 2. первичное rate limiting, 3. WAF/блокировки по IP (если применимо).
  • FastAPI повторно (или дополнительно) выполняет:
  • 1. проверку claims/scopes, 2. доменные политики доступа (например, какие профили инференса разрешены этому клиенту).

    Это снижает нагрузку на приложение и упрощает инциденты: часть трафика отсекается до входа в ваш runtime.

    2.5. Ротация ключей и JWKS

    JWT‑провайдеры ротируют ключи подписи. Чтобы не сломать сервис:

  • кешируйте JWKS с разумным TTL,
  • поддерживайте несколько ключей одновременно (по kid),
  • при ошибке подписи не делайте «шторм» запросов за ключами (нужен backoff).
  • 3) mTLS: когда токена недостаточно

    mTLS (mutual TLS) — это TLS, где сервер и клиент предъявляют сертификаты. Он решает другую задачу, чем JWT:

  • JWT отвечает на вопрос: «кто клиент и что ему можно».
  • mTLS отвечает на вопрос: «это действительно наш сервис/клиент в нашем доверенном контуре, и канал защищён».
  • 3.1. Где mTLS особенно полезен в serving

  • Service‑to‑service внутри кластера: worker ↔ API, API ↔ model runtime sidecar, API ↔ internal cache/broker.
  • Доступ к admin‑эндпойнтам (если они не отделены): проще закрыть сетевым контуром + mTLS.
  • Партнёрские интеграции: когда у партнёра нет вашего IdP, но есть возможность держать клиентские сертификаты.
  • 3.2. Что учитывать при mTLS в Kubernetes

    Частые ошибки эксплуатации:

  • Ротация сертификатов: сервис должен переживать обновление секретов и перезапуски.
  • Проверка SAN/subject: одного факта «сертификат валиден» мало; нужно понимать, какой именно клиент пришёл.
  • Границы доверия: mTLS внутри mesh/ingress и mTLS «до клиента» — это разные режимы.
  • Полезный паттерн: делайте mTLS «сетевым уровнем допуска», а fine‑grained авторизацию (scopes/тенанты) оставляйте JWT.

    4) CORS: не безопасность сервера, а безопасность браузера

    CORS нужен только когда ваш API вызывают из браузера (frontend). Для server‑to‑server клиентов CORS не имеет смысла.

    4.1. Типовые ошибки CORS

  • Access-Control-Allow-Origin: * вместе с Allow-Credentials: true — опасная комбинация (браузеры это ограничивают, но полагаться на это нельзя).
  • Разрешать «всё подряд» в Allow-Headers и Allow-Methods без необходимости.
  • Забыть про preflight (OPTIONS) и получить ложные ошибки у клиентов.
  • 4.2. Практическая политика CORS

  • Явно перечисляйте допустимые origins (по окружениям: dev/stage/prod).
  • Разрешайте только нужные методы (обычно POST для inference).
  • Ограничьте заголовки: например, Authorization, Content-Type, X-Request-ID.
  • Если используете cookies/credentials — делайте это осознанно, чаще для inference это не нужно.
  • CORS не заменяет auth: даже при идеальном CORS ваш API обязан быть защищён токеном/mTLS.

    5) Rate limits и quotas: защита мощности и денег

    Rate limiting решает две задачи:

  • защита от перегруза и DoS,
  • справедливое распределение мощности между клиентами (multi‑tenant).
  • Важно не смешивать:

  • rate limit (RPS/запросы в минуту),
  • concurrency limit (сколько запросов одновременно),
  • quota (суточный/месячный бюджет),
  • cost limit (лимит по «стоимости» запроса).
  • 5.1. Почему «RPS лимита» недостаточно для LLM/CV

    Одинаковый RPS может иметь радикально разную цену:

  • LLM: короткий prompt vs длинный prompt + большая генерация.
  • CV: маленькое изображение vs огромная картинка после декодирования.
  • Поэтому для ML‑serving полезны взвешенные лимиты:

  • считаете «стоимость» запроса по простым прокси‑метрикам (например, длина текста, лимит max_tokens, размер изображения),
  • ограничиваете не только количество запросов, но и суммарную стоимость.
  • 5.2. Где ставить rate limiting

    Рекомендуется минимум два слоя:

  • На входе (gateway/ingress): защита от грубого DoS, ограничение по IP/ключу.
  • Внутри приложения/runtime: защита GPU/CPU очереди, справедливость по тенантам, учёт «стоимости».
  • Внутренний лимитер должен быть согласован с вашим backpressure‑поведением (например, возвращать 429 с единым error_code, как мы уже обсуждали в модели ошибок).

    5.3. Алгоритмы лимитирования (когда какой выбирать)

  • Token Bucket: хорош для API, допускает короткие всплески (burst), затем ограничивает.
  • Leaky Bucket: сглаживает поток, полезен когда важна стабильность очереди.
  • Fixed Window: прост, но даёт «пилу» на границах окна.
  • Для inference под нагрузкой чаще выбирают Token Bucket (на периметре) + отдельный concurrency limiter (внутри).

    5.4. Ключи лимитирования и кардинальность

    Правильные ключи (обычно низкая кардинальность и бизнес‑смысл):

  • client_id / tenant_id из JWT,
  • класс обслуживания (premium/free),
  • endpoint + профиль (/generate + quality).
  • Неправильные ключи:

  • полный URL с параметрами,
  • request_id,
  • сырой текст/хэш текста без необходимости.
  • 5.5. Что возвращать при лимите

    При срабатывании лимита:

  • статус обычно 429,
  • единый error_code (например, overloaded или отдельный rate_limited — выберите и закрепите),
  • опционально Retry-After или поле в details (если умеете оценить).
  • Не делайте «молчаливые» задержки вместо ответа: под high load это ухудшает хвостовые задержки и провоцирует ретраи.

    6) Сводная схема: как слои безопасности складываются вместе

    Ключевой принцип: чем раньше вы отсекаете нежелательный трафик, тем меньше шанс потерять SLO и стабильность.

    ---

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

    1) Составьте минимальную матрицу прав (scopes) для сервиса с endpoint’ами /v1/embeddings, /v1/generate, /v1/jobs, /metrics. Какие scopes нужны и какие endpoint’ы ими защищаете?

    2) Придумайте политику «стоимостного лимита» для LLM генерации: какие 3 параметра запроса будут входить в cost‑оценку, и где вы примените лимит (gateway или приложение)?

    3) Опишите 5 обязательных проверок JWT, которые вы сделаете в сервисе, и что будет, если одну из них не делать.

    4) Придумайте безопасную CORS‑политику для production: какие origins, методы и заголовки разрешите для фронтенда, который вызывает только /v1/embeddings?

    5) Выберите: где именно вы включите mTLS — до ingress или только внутри кластера? Обоснуйте в 3 пунктах.

    <details> <summary> Ответы </summary>

    1) Пример матрицы scopes:

  • inference:embeddings → доступ к POST /v1/embeddings
  • inference:generate → доступ к POST /v1/generate (и, если нужно, к streaming‑варианту)
  • inference:jobs:write → доступ к POST /v1/jobs
  • inference:jobs:read → доступ к GET /v1/jobs/{id}
  • ops:metrics → доступ к /metrics (часто лучше вообще не экспонировать наружу, а закрыть сетью)
  • 2) Пример cost‑политики для LLM:

  • Параметры для cost:
  • 1) оценка размера входа (символы/оценка токенов) 2) max_tokens на выход 3) profile (fast/quality) как мультипликатор стоимости

  • Где применять:
  • - грубый лимит по RPS/токенам — на gateway, - точный cost‑лимит и справедливость по tenant’ам — в приложении/use case (потому что там доступен контекст профиля, правила деградации и бизнес‑класс обслуживания).

    3) 5 обязательных проверок JWT и риск:

  • подпись: без неё токен можно подделать;
  • iss: без неё можно принять токен от чужого провайдера;
  • aud: без неё возможен «токен для другого сервиса»;
  • exp/nbf: без них токены могут быть вечными или «ещё не действующими»;
  • разрешённый алгоритм: защита от неожиданных/неподдерживаемых режимов и ошибок конфигурации.
  • 4) Пример CORS для фронтенда на /v1/embeddings:

  • Origins: только домены вашего фронтенда (например, https://app.example.com), отдельно для stage.
  • Methods: POSTOPTIONS для preflight).
  • Headers: Authorization, Content-Type, X-Request-ID.
  • Credentials: выключить, если нет необходимости в cookies.
  • 5) Выбор mTLS (пример): только внутри кластера.

    Обоснование:

  • Проще эксплуатация: клиентам не нужно управлять сертификатами, внешний доступ решается JWT.
  • mTLS внутри кластера защищает сервис‑to‑сервис трафик и снижает риск lateral movement.
  • Внешний mTLS имеет смысл, если у вас строго controlled B2B‑интеграции или требования комплаенса; иначе стоимость поддержки обычно выше пользы.
  • </details>

    15. ML runtime: загрузка модели, warmup, thread safety

    ML runtime: загрузка модели, warmup, thread safety

    ML runtime в serving‑сервисе — это слой, который детерминированно превращает валидированный вход (текст/картинку) в ответ модели при заданных лимитах, конкурентности и ресурсах. В референс‑архитектуре и статьях про FastAPI lifecycle/DI и NFR мы уже закрепили, что:

  • модель нельзя «лениво» грузить в обработчике;
  • параллелизм нужно контролировать рядом с runtime;
  • readiness должен означать «модель реально готова выполнять прогон», а не просто «процесс поднялся».
  • Ниже — практические детали именно про runtime: как грузить, как греть (warmup) и как не убиться об thread safety.

    ---

    1) Загрузка модели: что именно считается “загрузкой”

    Загрузка — это не только weights.

    1.1. Состав runtime‑комплекта (LLM/CV)

  • Артефакты модели:
  • 1. веса/граф (PyTorch, ONNX, TensorRT engine); 2. конфиги (например, max_position_embeddings, image size); 3. сопутствующие файлы (vocab/merges, label maps, preprocessing config).
  • Препроцессинг:
  • 1. LLM: токенизатор, правила truncation/padding; 2. CV: декодер (JPEG/PNG), нормализация, resize.
  • Параметры исполнения:
  • 1. device (cpu/cuda), dtype (fp16/bf16/fp32); 2. лимиты (max tokens/image side), таймауты; 3. параметры батчинга/конкурентности.

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

    1.2. Где выполнять загрузку в FastAPI

    Правильная точка — lifespan/startup (см. статью про app factory и lifespan). Важно разделять:

  • init процесса (импорты, создание контейнера);
  • инициализацию runtime (чтение артефактов, создание session/model);
  • готовность к обслуживанию (warmup + проверка “модель отвечает”).
  • Практический смысл: если runtime не поднялся — сервис должен fail fast и не становиться ready.

    1.3. Модель конкурентности при загрузке: процессы важнее потоков

    Под high load обычно используется несколько процессов‑воркеров (gunicorn/uvicorn workers). Это означает:

  • каждый воркер имеет свою память;
  • каждый воркер загрузит модель отдельно, если вы не используете специальные техники shared memory;
  • на GPU это часто означает: «одна GPU → один воркер», иначе легко получить OOM/деградацию.
  • Практическое решение выбирается от обратного:

  • для CPU inference допускается несколько воркеров и внутренняя многопоточность библиотек;
  • для GPU обычно держат 1 воркер на GPU и управляют конкурентностью/батчингом внутри runtime.
  • 1.4. Антипаттерны загрузки

  • Лениво грузить “на первый запрос”:
  • 1. ломает p99; 2. создаёт гонки инициализации; 3. усложняет диагностику и readiness.
  • Грузить модель на уровне модуля (глобальный singleton):
  • 1. плохо контролируется жизненный цикл; 2. сложно подменять в тестах; 3. легко «протекает» между слоями.

    ---

    2) Warmup: зачем он нужен и что именно прогреваем

    Warmup — это управляемые “первые прогоны”, которые переводят runtime из “теоретически загружен” в “практически стабилен по задержкам”.

    2.1. Что устраняет warmup

  • Ленивая инициализация внутри библиотек:
  • 1. аллокации буферов; 2. выбор/компиляция kernel’ов; 3. построение внутренних кешей.
  • “Холодные” пути препроцессинга:
  • 1. первый запуск токенизатора; 2. первый декод изображения и преобразования.
  • JIT/компиляция (если применяете):
  • 1. torch.compile/graph capture; 2. оптимизации ORT/TensorRT.

    2.2. Какой warmup считать достаточным

    Делайте warmup репрезентативным, но не дорогим.

  • Подберите 2–4 типовых “формата” входа:
  • 1. LLM: короткий prompt, средний prompt, верхняя граница по длине (в рамках лимита); 2. CV: типичный размер после decode/resize, и “тяжёлый” на границе max side.
  • Прогоните несколько повторов каждого формата:
  • 1. первый прогон “съест” cold‑start; 2. последующие дадут представление о стабильной задержке.
  • Если поддерживаете батчинг — прогрейте и батчи:
  • 1. batch=1; 2. batch около ожидаемого среднего; 3. batch близкий к max batch size.

    2.3. Warmup и readiness: “готов” означает “может отвечать”

    Readiness‑проверка должна подтверждать, что:

  • модель загружена;
  • минимальный прогон прошёл успешно;
  • runtime способен обрабатывать запросы без немедленного OOM/исключений.
  • Иначе вы получите ситуацию: Kubernetes считает pod готовым, трафик пошёл, и первые же запросы ловят холодные задержки или падение.

    2.4. Частые ошибки warmup

  • Прогревать только batch=1 при реальной работе батчером → неожиданная деградация при first real batch.
  • Прогревать “идеальные” входы, которые не совпадают с продом (длины/размеры другие).
  • Делать warmup с включённым градиентным режимом или train‑режимом (для PyTorch) → лишняя память и нестабильность.
  • ---

    3) Thread safety: где реально возникают гонки в ML runtime

    Thread safety — это не абстракция. В serving гонки обычно появляются из-за смешения:

  • нескольких запросов одновременно;
  • общей модели/токенизатора на процесс;
  • внутренних потоков библиотек (BLAS, OpenMP, ORT);
  • async‑обвязки FastAPI + вызовов в threadpool.
  • 3.1. Быстрый ориентир по конкурентности

    У вас почти всегда есть три уровня:

    Если вы не ограничили второй/третий уровень, thread safety “съедается” перегрузом: даже потокобезопасная библиотека может деградировать по p99 или уйти в OOM при чрезмерной параллельности.

    3.2. Типовые источники проблем

  • Общий mutable state в runtime:
  • 1. переиспользуемые буферы без синхронизации; 2. глобальные переменные “последний запрос/последний батч”; 3. переиспользование одного и того же объекта результата.
  • Токенизатор/препроцессор:
  • 1. некоторые реализации используют внутренние кеши/пулы; 2. параллельное обновление кеша может приводить к редким ошибкам.
  • GPU‑контекст и память:
  • 1. одновременные прогоны без ограничения часто приводят к фрагментации/пикам памяти; 2. “пилообразный” OOM на p99 — симптом неконтролируемой конкурентности.
  • Нечёткое разделение sync/async:
  • 1. тяжёлый CPU‑препроцессинг в event loop блокирует обработку других запросов; 2. попытка “ускорить” через threadpool без лимитов → много потоков → рост задержек.

    3.3. Практические правила, которые реально предотвращают гонки

  • Делайте runtime максимально иммутабельным:
  • 1. модель в eval режиме; 2. конфиг runtime не меняется “на лету” без контролируемого механизма.
  • Вводите явный concurrency limiter на входе в runtime:
  • 1. семафор/очередь ограничивает число одновременных прогонов; 2. для GPU это часто важнее, чем количество HTTP‑соединений.
  • Разделяйте объекты:
  • 1. входы/выходы — новые объекты на запрос; 2. переиспользуемые буферы — только если есть строгая синхронизация.
  • Если используете кеши (например, pretokenize/embeddings cache), убедитесь, что:
  • 1. операции записи атомарны; 2. ключи и значения не содержат PII по умолчанию (см. статью про логи/безопасность).

    3.4. Thread safety vs process safety: важное следствие для деплоя

    Даже идеально потокобезопасный runtime не спасёт, если:

  • вы запустили несколько процессов на одну GPU без расчёта памяти;
  • каждый процесс прогревается и аллоцирует “пиковую” память одновременно;
  • нагрузка приводит к синхронным пикам.
  • Поэтому планирование воркеров — часть runtime‑дизайна, а не “настройка сервера”.

    ---

    4) Мини-чеклист production‑готовности runtime

  • Загрузка в lifespan и fail fast при ошибках.
  • Readiness зависит от успешного warmup.
  • Есть ограничение конкурентности на уровне runtime.
  • Нет глобального mutable state, влияющего на инференс.
  • Warmup покрывает типовые размеры (и батчи, если батчинг есть).
  • Настройки device/dtype/лимитов конфигурируемы и валидируются при старте.
  • ---

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

    1) Вы обслуживаете LLM генерацию на GPU. Опишите (текстом) политику запуска воркеров и конкурентности:

  • сколько процессов на одну GPU;
  • где ограничиваете одновременные прогоны;
  • что будет сигналом перегруза.
  • 2) Придумайте warmup‑набор из 4 входов для:

  • LLM генерации;
  • CV классификации.
  • Для каждого входа укажите, какую проблему warmup он “закрывает”.

    3) У вас “редкие” ошибки под нагрузкой (примерно 1 на 10 000) в токенизации. Назовите 3 гипотезы, связанные с thread safety, и по одной проверке/доказательству для каждой.

    4) Перечислите 5 антипаттернов runtime‑слоя (не HTTP‑слоя), которые приводят к росту p99 или OOM.

    <details> <summary> Ответы </summary>

    1) Пример политики (GPU, LLM):

  • Процессов: обычно 1 процесс на одну GPU (чтобы проще контролировать память и избежать межпроцессной конкуренции за VRAM).
  • Ограничение прогонов: перед входом в runtime — семафор/очередь на фиксированное число одновременных инференсов (часто 1–2), плюс micro‑batcher рядом с runtime.
  • Сигнал перегруза:
  • 1) очередь ожидания слота превысила лимит; 2) время ожидания слота превысило порог; 3) частота OOM/таймаутов выросла.

    2) Пример warmup‑набора:

    LLM:

  • Короткий prompt + маленький max_tokens: прогревает базовый путь токенизации и первый kernel.
  • Средний prompt + типичный max_tokens: прогревает “реальный” режим работы.
  • Prompt на границе лимита входа: выявляет проблемы с памятью/паддингом/pack.
  • Батч из нескольких запросов (если micro‑batch): прогревает батчевый путь и аллокации под батч.
  • CV:

  • Маленькое изображение (после decode): прогревает декодер и базовый инференс.
  • Типичное изображение вашего продукта: прогревает нормальный путь resize/normalize.
  • “Тяжёлое” на границе max side: проверяет лимиты памяти и худшие случаи препроцессинга.
  • Батч из N картинок (если батчинг): прогревает векторизованный препроцессинг и batched inference.
  • 3) Гипотезы по tokenization:

  • Общий mutable state в токенизаторе/обёртке: проверка — запустить токенизацию в одном потоке (concurrency=1) и сравнить частоту ошибки.
  • Непотокобезопасный кеш (например, LRU без блокировки): проверка — временно отключить кеш или заменить на реализацию с блокировкой и сравнить частоту.
  • Гонки из-за смешения async/threadpool (например, объект запроса мутируется): проверка — добавить защиту “copy входа” перед передачей в worker и/или включить строгую иммутабельность DTO/доменных объектов.
  • 4) Антипаттерны runtime:

  • Неконтролируемая параллельность прогонов на GPU (нет семафора/лимита).
  • Препроцессинг с тяжёлыми операциями в горячем пути без лимитов (например, decode больших изображений без предохранителей по размеру).
  • Переиспользование общих буферов без синхронизации.
  • Warmup только “для галочки” (не репрезентативные входы), из-за чего первые реальные запросы дают p99‑шип.
  • Динамическое изменение конфигурации runtime (dtype/device/параметры) без контролируемого механизма и без перезагрузки/прогрева.
  • </details>

    16. Оптимизация инференса: batching, caching, quantization, ONNX

    Оптимизация инференса: batching, caching, quantization, ONNX

    Оптимизация инференса в production — это не «ускорить модель любой ценой», а добиться устойчивого выигрыша по latency/throughput внутри ваших NFR/SLO и без деградации надёжности. Сами требования, backpressure, профили и базовая модель конкурентности уже разобраны ранее — здесь сфокусируемся на четырёх рычагах, которые чаще всего дают максимальный эффект: batching, caching, quantization, ONNX Runtime.

    ---

    1) Batching: как поднять throughput, не убив хвосты

    Batching бывает двух типов:

  • Client-side batch: клиент присылает массив входов (вы управляете контрактом и частичными ошибками).
  • Server-side micro-batching: сервис агрегирует одиночные запросы в микробатчи рядом с runtime.
  • Ниже — то, что обычно «решает судьбу» батчинга в production.

    1.1. Ключевой компромисс: throughput vs p95/p99

    Micro-batching добавляет ожидание «сборки батча», поэтому важно фиксировать в политике исполнения (а не в endpoint):

  • batch window: сколько времени ждать сборки батча.
  • max batch size: верхняя граница батча.
  • класс обслуживания: какие запросы можно смешивать вместе.
  • Практика: делайте батчинг профильным (например, fast — минимальное окно, throughput — окно побольше). Иначе вы случайно ухудшите p95 для всех.

    1.2. LLM: проблема длины последовательностей

    Для LLM батчинг часто упирается не в количество запросов, а в разнородность длины:

  • при padding «короткие» запросы платят за «длинные»;
  • в батче может резко вырасти VRAM и время шага;
  • хвосты усиливаются: один «тяжёлый» элемент портит задержку всему батчу.
  • Рабочие тактики:

  • Бакетизация по длине: отдельные очереди/батчи для диапазонов длины (например, 0–512, 513–2048 токенов).
  • Ограничение совместимости: не смешивать разные профили (quality/fast) в одном батче.
  • Отдельная политика для генерации: батчинг префилла и батчинг декодинга могут вести себя по-разному (декодинг чувствительнее к вариативности).
  • 1.3. CV: бутылочное горлышко может быть до модели

    Для CV батчинг проще после приведения к фиксированному размеру, но частая ловушка — декодирование/resize съедают CPU так, что GPU простаивает.

    Практика:

  • измерять отдельно время препроцессинга и время инференса;
  • при высоком RPS ограничивать параллельность декодера так же строго, как GPU-инференса;
  • рассмотреть батчинг/векторизацию препроцессинга, если это реально даёт выигрыш.
  • 1.4. Наблюдаемость батчинга (минимум)

    Чтобы понимать, помогает ли батчинг, нужны метрики уровня runtime:

  • batch_size (распределение)
  • batch_wait_ms (сколько запросы ждут сборки)
  • infer_ms (чистое время инференса батча)
  • доля «одиночных» батчей (batch=1) под нагрузкой
  • Если вы видите, что batch_wait_ms растёт, а batch_size не растёт — это признак неправильных параметров окна/лимитов или перекоса трафика.

    ---

    2) Caching: где реально окупается и как не сломать семантику

    Кеширование в serving окупается только там, где есть повторяемость и вы можете сформировать корректный ключ. На практике встречаются 4 полезных класса кеша.

    2.1. Кеш результата (response cache)

    Подходит для:

  • эмбеддингов
  • классификации/детекции, если входы повторяются
  • LLM-ответов — только если у вас детерминированный режим или продукт готов к «примерно такому же» ответу
  • Риски:

  • недетерминизм (temperature/top_p) ломает ожидания клиента;
  • любой «скрытый» параметр, влияющий на результат, делает кеш некорректным.
  • Правило: в ключ должны входить все параметры, влияющие на результат (модель/версия/профиль/опции), иначе вы получите «магические» ответы.

    2.2. Кеш промежуточных артефактов

    Часто выгоднее кешировать не финальный ответ, а «дорогую часть»:

  • токенизацию (LLM) для часто повторяющихся prompt’ов
  • декодирование/resize (CV) для повторяющихся изображений (обычно реже, но бывает)
  • Плюс: меньше рисков несовместимости с режимами генерации.

    2.3. Дедупликация in-flight

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

    Важно:

  • дедупликация должна жить рядом с очередью/батчером (иначе вы всё равно перегрузите runtime);
  • обязателен таймаут ожидания, чтобы не «висели навсегда»;
  • ключ дедупликации должен совпадать с ключом семантики результата.
  • 2.4. Политики TTL и инвалидации

    Минимум, который нужно решить заранее:

  • TTL результата
  • что происходит при смене версии модели
  • нужно ли разделять кеши по окружениям/тенантам
  • Наиболее безопасная схема — включать в ключ идентификатор версии модели, тогда инвалидация при обновлении происходит естественно.

    ---

    3) Quantization: ускорение и экономия памяти ценой компромиссов

    Квантизация — это снижение точности представления весов/активаций ради выигрыша в:

  • latency
  • throughput
  • потреблении памяти (особенно важно для больших LLM)
  • 3.1. Что реально меняется при квантизации

  • Память: веса меньше → больше моделей помещается или выше batch/concurrency.
  • Скорость: зависит от железа и ядра (CPU/GPU) и от того, есть ли оптимизированные int8/int4 kernels.
  • Качество: возможна деградация (иногда заметная), особенно на «тонких» задачах.
  • 3.2. Варианты, которые встречаются чаще всего

  • FP16/BF16 (на GPU): часто «бесплатный» выигрыш в памяти и скорости при минимальной деградации.
  • INT8:
  • 1. на CPU часто даёт хороший прирост, 2. на GPU выигрыш зависит от стека и реализации.
  • Weight-only quantization (INT8/INT4):
  • 1. сильно экономит память, 2. особенно популярна для LLM, 3. скорость может вырасти, но не гарантированно — всё решают kernels.

    3.3. Production-риски квантизации

  • Дрейф качества: нужен минимальный регрессионный набор (sanity) именно для serving.
  • Непредсказуемость latency: разные длины входа/батчи могут вести себя иначе.
  • Ограничения операторов: не все операции/слои поддерживаются выбранной схемой квантизации.
  • Практика: оформляйте квантизованный runtime как отдельный «профиль» (fast/cheap) и измеряйте его отдельно по SLO.

    ---

    4) ONNX и ONNX Runtime: когда и как это даёт выигрыш

    ONNX — формат вычислительного графа, а ONNX Runtime (ORT) — движок, который исполняет этот граф с оптимизациями и разными execution providers.

    4.1. Когда ONNX особенно полезен

  • CPU-serving: часто помогает стабилизировать latency и упростить оптимизации.
  • Стандартизированный runtime: легче контролировать граф и оптимизации, чем «живой» Python-стек.
  • Ограничение вариативности: меньше «питоновских сюрпризов» в горячем пути.
  • 4.2. Типовые причины, почему ONNX “не взлетел”

  • Динамические формы (dynamic shapes) без дисциплины: движку сложнее оптимизировать, а кэш планов исполнения хуже.
  • Несовместимые/неоптимальные операторы: экспорт прошёл, но скорость не выросла.
  • Неправильная граница графа: значимая часть времени остаётся в препроцессинге на Python.
  • 4.3. Практические рычаги оптимизации в ORT

    Без привязки к коду, что важно контролировать как инженерные параметры:

  • уровень граф-оптимизаций (fusions, constant folding)
  • выбор execution provider под вашу платформу
  • настройка потоков/параллелизма на CPU (чтобы не получить “шторм потоков”)
  • фиксация (или аккуратная политика) dynamic axes
  • Для GPU-сценариев полезно думать про минимизацию копирований (вход/выход), иначе выигрыш от ускоренного ядра будет съеден передачей данных.

    ---

    5) Как комбинировать техники (практическая матрица)

    | Техника | Где чаще всего даёт максимум | Главный риск | Минимальный «предохранитель» | |---|---|---|---| | Micro-batching | GPU inference (LLM/CV) | рост p99 из-за ожидания окна | метрики batch_wait_ms, лимиты очереди | | Response cache | embeddings, классификация, детерминированные ответы | некорректный ключ/семантика | ключ включает модель+параметры, TTL | | In-flight dedupe | пики одинаковых запросов | ожидание «чужого» запроса навсегда | таймаут ожидания + cancellation | | Quantization | LLM память, CPU throughput | деградация качества/нестабильность | sanity-набор + профильный rollout | | ONNX Runtime | CPU latency/стабильность | «экспорт есть, ускорения нет» | измерение стадий + контроль shapes |

    ---

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

    1) Вы обслуживаете LLM генерацию на GPU. Предложите политику micro-batching: какие 3 параметра вы зафиксируете (и почему), чтобы не ухудшить p95.

    2) Спроектируйте ключ кеша для эмбеддингов. Укажите минимум 6 составляющих ключа, чтобы избежать ложных совпадений.

    3) Выберите один сценарий, где квантизация почти наверняка окупится, и один — где она рискованна. Объясните причину (по 2–3 пункта на сценарий).

    4) У вас есть модель CV, вы перенесли инференс в ONNX Runtime, но ускорения нет. Назовите 5 гипотез, которые нужно проверить в первую очередь (по стадиям пайплайна).

    5) Опишите, какие 4 метрики вы обязаны добавить, чтобы доказать, что оптимизация (любая из четырёх) реально улучшила production-поведение, а не только «среднее в бенчмарке».

    <details> <summary> Ответы </summary>

    1) Пример micro-batching политики для LLM:

  • batch_window_ms: маленькое (например, единицы миллисекунд) для профиля fast, чтобы не раздувать p95 ожиданием батча.
  • max_batch_size: ограничение по памяти и по худшему случаю длины входа, чтобы тяжёлые запросы не приводили к OOM.
  • Правила совместимости в батче: не смешивать разные профили (fast/quality) и/или разные диапазоны длины контекста, чтобы не было «один длинный портит всем».
  • 2) Пример ключа кеша эмбеддингов (минимум 6 частей):

  • хэш нормализованного текста (например, после trim и нормализации пробелов)
  • идентификатор/версия модели
  • версия токенизатора/правил препроцессинга
  • профиль инференса (fast/quality)
  • параметры, влияющие на эмбеддинг (например, pooling/normalize, если есть)
  • схема постобработки/формат выхода (например, dtype/округление, если это влияет на байтовое представление)
  • 3) Пример:

    Сценарий, где квантизация окупится:

  • большая LLM упирается в VRAM, и вы вынуждены держать маленький batch/concurrency;
  • weight-only INT8/INT4 часто позволяет поднять допустимый batch и снизить OOM.
  • Сценарий, где рискованно:

  • задача чувствительна к качеству (например, ранжирование/классификация с тонкими границами), где даже небольшая деградация ухудшит продукт;
  • у вас нет регрессионного набора и контроля качества в релизном процессе — вы не поймёте, что стало хуже.
  • 4) 5 гипотез “почему ONNX не ускорил”:

  • время уходит в препроцессинг (decode/resize/токенизация), а не в модельный шаг.
  • dynamic shapes слишком широкие: оптимизации слабее, кэш планов исполнения не помогает.
  • execution provider выбран неудачно для платформы (или фактически используется CPU, хотя ожидали ускорение).
  • копирования вход/выход доминируют над временем инференса (особенно при больших тензорах).
  • модель экспортировалась, но граф не оптимизирован (нет фьюзингов/оптимизаций, или часть операторов работает медленно).
  • 5) Минимальные 4 метрики для доказательства улучшения:

  • p95/p99 server-side latency по endpoint и по профилю (чтобы видеть хвосты, а не среднее).
  • throughput в “рабочем режиме” (RPS при выполнении SLO, а не «максимум пока не упало»).
  • ошибка/отказы по причинам (error_code) — особенно overloaded, timeout, runtime_error.
  • метрики уровня runtime: для batching — batch_size и batch_wait_ms; для кеша — hit ratio; для квантизации/ONNX — infer_ms и, при необходимости, использование памяти (RAM/VRAM).
  • </details>

    17. Очереди и фоновые задачи: Redis/RQ или Celery, idempotency

    Очереди и фоновые задачи: Redis/RQ или Celery, idempotency

    Async job‑based режим (очередь + воркеры) нужен, когда синхронный инференс становится хрупким: переменная длительность, риск таймаутов на ingress/gateway, необходимость контролируемой очереди/приоритизации/TTL результата. Семантику job‑эндпойнтов и модель статусов мы уже фиксировали в статье про sync/async/job/streaming — здесь разберём инженерную реализацию очереди, выбор RQ vs Celery и обязательную для production вещь: idempotency.

    1) Что именно даёт очередь в ML‑serving (помимо “сделать async”)

    Очередь — это не просто «фон». Это механизм, который:

  • Отделяет приём HTTP от выполнения тяжёлой работы (инференс/препроцессинг/постпроцессинг).
  • Даёт явную точку контроля: лимит длины очереди, приоритеты, ретраи, dead‑letter сценарии.
  • Делает поведение предсказуемым при перегрузке: лучше контролируемо отказать/отложить, чем повесить API воркеры.
  • Минимальная схема:

    Важно: брокер (очередь) и хранилище результата — разные роли. В простом варианте оба на Redis, но логически это две подсистемы.

    2) Выбор: Redis + RQ или Redis + Celery

    2.1. RQ (Redis Queue)

    RQ — минималистичный вариант, когда хочется:

  • Быстро поднять job‑воркеры поверх Redis.
  • Простую модель: очередь в Redis, воркеры читают, выполняют, пишут результат.
  • Меньше «магии» и конфигурации.
  • Обычно RQ хорошо подходит для:

  • Небольшого числа типов задач (например, один тип инференса и один тип препроцессинга).
  • Простого retry‑поведения.
  • Команды, которая хочет низкую операционную сложность.
  • Ограничения (инженерно важные):

  • Меньше встроенных продвинутых примитивов оркестрации (цепочки/группы/хорды).
  • Меньше готовых механизмов маршрутизации по нескольким очередям и сложных политик ретраев.
  • 2.2. Celery

    Celery — более тяжёлый, но мощный вариант. Типично выбирают, когда нужны:

  • Несколько очередей и строгая маршрутизация задач (по типу, приоритету, классу обслуживания).
  • Богатые политики ретраев (backoff, ограничения, отдельные “failed queues” в виде практики).
  • Сложные пайплайны задач (цепочки, группы) — например, CV: decode → normalize → infer → postprocess, если это реально вынесено в фон.
  • Операционные издержки:

  • Больше конфигурации (workers, prefetch, ack‑политики, time limits).
  • Больше места для “неочевидных” эффектов под нагрузкой.
  • 2.3. Практическая таблица выбора

    | Критерий | RQ + Redis | Celery + Redis | |---|---|---| | Сложность внедрения | ниже | выше | | Контроль маршрутизации/много очередей | базовый | сильный | | Оркестрация задач | ограниченно | богато | | Ретраи и политики | проще | гибче | | Вероятность “неожиданных” эффектов | ниже | выше |

    Если вы не уверены — в serving‑сервисах часто начинают с RQ, а к Celery приходят, когда появляются реальные требования к маршрутизации/пайплайнам.

    3) Семантика доставки: почему idempotency обязательна

    Почти любые очереди в production дают at‑least‑once доставку: задача может выполниться дважды. Причины:

  • Воркер упал после выполнения, но до фиксации результата.
  • Сбой сети/Redis во время ack/записи результата.
  • Ретраи по таймауту: вы не уверены, завершилась ли задача, и запускаете снова.
  • В ML‑inference “чистая функция” (input → output) часто делает дубликаты не страшными, но есть два практических риска:

  • Денежный/ресурсный: дважды сожгли GPU.
  • Побочные эффекты: запись результата/аудита/логов/сохранение артефактов может удвоиться.
  • Отсюда правило: каждая job должна быть идемпотентной.

    4) Idempotency: что это означает на практике

    Идемпотентность в job‑контексте: повторная отправка того же запроса не должна приводить к созданию второй независимой задачи (или должна приводить к безопасному “дедупу”).

    4.1. Что считается “тем же” запросом

    Нужно определить ключ идемпотентности. Обычно это:

  • Явный Idempotency-Key от клиента (заголовок) — лучший вариант.
  • Или вычисляемый ключ на стороне сервиса: хэш нормализованного входа + параметры, влияющие на результат (см. дисциплину “всё влияющее — явно в запросе” из материалов про контракт).
  • Важно: если модель недетерминирована (например, LLM с temperature), то “тот же запрос” должен включать и параметры генерации.

    4.2. Где хранить состояние идемпотентности

    Типовой минимум — хранить маппинг:

  • idempotency_key -> job_id
  • и отдельно состояние job:

  • job_id -> status/result/error (+ TTL)
  • Это можно хранить в Redis.

    4.3. Атомарность (иначе будут гонки)

    Критический момент под high load: два одинаковых запроса могут прийти одновременно. Значит, запись idempotency_key -> job_id должна быть атомарной.

    Практический паттерн на Redis:

  • Попытка SET key value NX EX ttl (установить, только если ключа ещё нет, с TTL).
  • Если установка успешна — создаём job и возвращаем 202 + job_id.
  • Если ключ уже существует — возвращаем тот же job_idне создаём новую задачу).
  • TTL для idempotency‑ключа выбирают так, чтобы:

  • покрыть период, когда клиент может ретраить создание job;
  • не хранить бесконечно.
  • Часто TTL делают чуть больше TTL результата.

    4.4. Повторный ответ для клиента

    При повторном POST /jobs с тем же ключом возможны два корректных поведения:

  • Вернуть тот же job_id и 202 (задача ещё выполняется).
  • Вернуть тот же job_id и сразу 200 с результатом (если результат уже готов и вы это поддерживаете).
  • Главное — стабильность контракта и отсутствие дубликатов задач.

    5) Ретраи: как не превратить их в “шторм”

    Ретраи полезны для временных проблем (внешняя зависимость, краткий OOM, сетевой сбой), но опасны для CPU/GPU, если включены без политики.

    Рекомендуемая дисциплина:

  • Разделяйте ошибки на retryable и non‑retryable (в терминах ваших error_code, см. материал про единый error model).
  • Делайте ограничение по числу попыток и backoff (пауза растёт).
  • Для “ядовитых” задач (всегда падают) — фиксируйте failed и не ретрайте бесконечно.
  • Лимитируйте параллелизм воркеров так же строго, как runtime‑конкурентность (иначе очередь просто ускорит путь к перегрузу).
  • 6) Result store и TTL: что именно вы обязаны решить заранее

    Async без TTL обычно превращается в мусор.

    Нужно явно определить:

  • TTL результата (succeeded/failed) — сколько клиент может получить результат.
  • TTL “idempotency mapping” — сколько вы помните связь ключа с job.
  • Максимальный размер результата (особенно важно для больших ответов LLM).
  • Если результат хранится в Redis:

  • следите за memory policy (иначе eviction может “случайно” удалить результаты);
  • не храните “тяжёлые” артефакты без необходимости (большие тексты/бинарники лучше хранить в отдельном хранилище, а в результате держать ссылку/идентификатор).
  • 7) Отмена (cancellation): что реально возможно

    В job‑модели DELETE /jobs/{id} часто означает:

  • Если задача ещё в очереди — удалить/пометить как отменённую.
  • Если уже выполняется — пометить как canceled_requested и воркер должен периодически проверять флаг.
  • В ML‑инференсе “жёстко убить” вычисление не всегда возможно безопасно. Поэтому cancellation — это контракт “best effort”. Важно, чтобы клиент понимал это по статусам.

    8) Мини‑чеклист production‑готовности очереди

  • Есть чёткая idempotency стратегия (ключ, TTL, атомарность).
  • Определены retry‑политики и классификация ошибок.
  • Есть result store с TTL и ограничениями размера.
  • Метрики/логи различают: queued, running, succeeded, failed, canceled.
  • Описано поведение при недоступности Redis (что вернём клиенту и как быстро).
  • ---

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

    1) Вы выбираете между RQ и Celery для ML‑serving. Опишите 4 критерия принятия решения и выберите инструмент для случая:

  • 2 типа задач (generate и embeddings)
  • нужна одна очередь, без цепочек
  • важна минимальная операционная сложность
  • 2) Спроектируйте правило формирования idempotency_key (если клиент его не передал): перечислите минимум 6 компонентов, которые должны влиять на ключ для LLM‑генерации.

    3) Опишите алгоритм обработки POST /jobs с idempotency:

  • какие записи делаем в Redis
  • какие TTL
  • что возвращаем при повторном запросе
  • 4) Придумайте retry‑политику для ошибок timeout, overloaded, validation_error, runtime_error: какие ретраим, какие нет, и почему.

    <details> <summary> Ответы </summary>

    1) Пример критериев:

  • Нужны ли сложные пайплайны/оркестрация задач (цепочки, группы).
  • Сколько очередей и насколько сложна маршрутизация (по типу задачи, приоритету, tenant).
  • Насколько критична простота эксплуатации (минимум конфигов и “магии”).
  • Насколько сложные retry‑политики нужны и кто будет их поддерживать.
  • Выбор для указанного случая: RQ, потому что задач мало, оркестрация не нужна, одна очередь, и вы хотите минимальную операционную сложность.

    2) Пример компонентов ключа (LLM):

  • Нормализованный prompt (например, trim) — хэш.
  • System prompt / инструкции (если есть) — хэш.
  • Идентификатор и версия модели.
  • Профиль (fast/quality) или эквивалент.
  • Параметры генерации: temperature, top_p, max_tokens, stop‑последовательности.
  • Политика усечения/контекста (если это параметризуемо).
  • Смысл: любое изменение, влияющее на результат, должно менять ключ — иначе получите “ложные совпадения”.

    3) Пример алгоритма:

  • Получаем Idempotency-Key или вычисляем.
  • Пытаемся атомарно записать idem:{key} -> {job_id} с TTL (например, 1 час) через SET ... NX EX.
  • Если запись успешна:
  • - создаём job в брокере (RQ/Celery), - создаём job:{job_id} -> {status=queued, created_at, ...} с TTL результата (например, 30 минут), - возвращаем 202 {job_id}.
  • Если idem:{key} уже существует:
  • - читаем job_id и возвращаем его (обычно 202), - опционально: если job уже succeeded, можно вернуть 200 с результатом.

    4) Пример retry‑политики:

  • validation_error: не ретраить (ошибка входа, повтор даст то же).
  • overloaded: ретраить ограниченно с backoff (это временная перегрузка). Иногда лучше ретраить на стороне клиента по Retry-After.
  • timeout: зависит от смысла таймаута.
  • - Если таймаут — “внешняя зависимость/временный сбой” → ретраить ограниченно. - Если таймаут — “вычисление не укладывается в лимит профиля” → не ретраить, а возвращать управляемую ошибку.
  • runtime_error: обычно не ретраить автоматически (часто детерминированная ошибка модели/рантайма). Ретрай допустим только если вы явно выделили подтип “transient runtime error” (например, редкий OOM) и у вас есть лимит попыток.
  • </details>

    18. Хранилища: Postgres для метаданных, S3 для артефактов

    Хранилища: Postgres для метаданных, S3 для артефактов

    В production ML‑serving почти всегда выгодно разделять метаданные и тяжёлые артефакты:

  • Postgres — транзакционное хранилище для структурированных фактов (версии моделей, активный релиз, конфиги, статусы job’ов, аудит выкладок).
  • S3‑совместимое object storage — для больших неизменяемых объектов (веса, токенизаторы, ONNX/TensorRT engine, словари, label maps, примеры для warmup).
  • Референс‑идея хранилищ уже встречалась в архитектурных материалах курса; здесь — практические решения: что хранить где, как связать Postgres↔S3, как не убить latency/надежность и как организовать жизненный цикл артефактов.

    ---

    1) Почему разделение Postgres / S3 — стандарт для serving

    Postgres лучше подходит для

  • Согласованных обновлений: переключить активную модель, записать аудит, обновить политики лимитов — всё в одной транзакции.
  • Запросов по фильтрам: “какая модель активна для tenant X?”, “какие версии откатили за неделю?”, “сколько job’ов в failed за час?”.
  • Гарантий целостности: внешние ключи, уникальные ограничения, нормализованные справочники.
  • S3 лучше подходит для

  • Больших объектов: веса модели, токенизаторы, энжины, кэши словарей.
  • Дешёвого хранения и доставки: object storage эффективно хранит гигабайты и обслуживает скачивания.
  • Иммутабельности и версионирования на уровне объекта: артефакт — это файл(ы), которые не редактируются «на месте».
  • Критично: не храните бинарные веса в Postgres (включая большие BYTEA), если вы строите высоконагруженный сервис. Это усложняет бэкапы, увеличивает нагрузку на БД и делает масштабирование дороже.

    ---

    2) Что такое «артефакты» и как их упаковывать в S3

    2.1. Состав артефактного “пакета”

    Обычно пакет версии модели включает:

  • weights/engine (PyTorch state dict / ONNX / TensorRT engine)
  • pre/post config (например, image_size, нормализации, truncation policy)
  • tokenizer / vocab / merges (LLM)
  • label map / thresholds (CV)
  • warmup inputs (необязательно, но удобно для стабильного прогрева)
  • manifest — маленький файл с метаданными пакета: список файлов, их размеры, хэши, совместимость
  • 2.2. Имя объекта и “адресация по версии”

    Хорошая практика — строить ключи объектов так, чтобы:

  • объект однозначно относился к версии (по model_id и version),
  • ключ был стабильным,
  • было удобно чистить старые версии.
  • Пример структуры ключей:

    Важно: версия должна быть иммутабельной. Если нужно поменять артефакт — это новая версия, а не перезапись файла по старому ключу.

    2.3. Целостность: хэш и размер как минимальные гарантии

    Postgres хранит ссылку на объект, а целостность — проверяется через:

  • size_bytes
  • checksum (например, sha256)
  • (опционально) etag/версию объекта в хранилище
  • Это позволяет сервису понять: «я скачал ровно то, что ожидается», и быстро отдиагностировать ошибки доставки/несовместимости.

    ---

    3) Модель данных в Postgres: минимально полезные таблицы

    Ниже — практичный минимум, который покрывает продакшен‑эксплуатацию model serving.

    3.1. models и model_versions

    | Таблица | Назначение | Ключевые поля | |---|---|---| | models | сущность модели (логическое имя) | id, name, task_type | | model_versions | конкретная версия артефактов | model_id, version, s3_prefix, manifest_checksum, created_at |

    Рекомендации:

  • model_versions(model_id, version)уникальный индекс.
  • Полезно хранить runtime_kind (pytorch/onnx/tensorrt) и runtime_compat (например, cpu/cuda, dtype), чтобы не переключить на версию, которую текущий деплой не способен исполнить.
  • 3.2. deployments (или active_model): переключение активной версии

    Вместо “захардкоженной активной версии в конфиге” часто удобнее иметь таблицу активных назначений:

    | Поле | Смысл | |---|---| | scope | где действует назначение (global/tenant/env) | | scope_key | идентификатор tenant/окружения (если нужно) | | model_id | какая модель | | active_version | какая версия активна | | rollout | опционально: проценты трафика/канареечный режим |

    Переключение версии тогда становится транзакционной операцией в Postgres (с аудитом), а воркеры/реплики читают назначение через кэш/периодический refresh.

    3.3. Job‑based режим: jobs и job_results

    Если у вас async/job‑based (см. статью про очереди и идемпотентность), Postgres можно использовать для метаданных job’ов, а результат хранить отдельно (Redis/S3). Минимум в Postgres:

  • jobs: job_id, status, created_at, updated_at, request_meta (без PII), model_id, model_version
  • job_results: ссылка на результат (например, result_s3_key или result_ref), TTL/expire_at
  • Практика: не кладите тяжёлый результат LLM‑генерации в Postgres, если он большой и частый. Лучше хранить результат в object storage (или в Redis для коротких TTL), а в Postgres — только ссылку и статус.

    ---

    4) Производительность и надежность Postgres в high load serving

    4.1. Соединения и пул

    В high load главная ошибка — создать слишком много соединений к Postgres (особенно при нескольких воркерах/репликах).

    Рекомендации:

  • Используйте пул соединений на уровне приложения.
  • Снаружи часто применяют PgBouncer как “connection concentrator”.
  • Разделяйте нагрузку чтения/записи, если метаданных много (read‑replica для чтений, primary для записей).
  • 4.2. Какие запросы должны быть быстрыми

    Метаданные в serving должны читаться короткими запросами:

  • «дай активную версию»
  • «дай s3_prefix и manifest checksum по version»
  • «обнови status job»
  • Не превращайте Postgres в аналитическое хранилище для всех запросов/ответов — это обычно конфликтует с latency.

    4.3. Транзакции и блокировки

    Самый частый конфликт — конкурирующие обновления активной версии/rollout.

    Практика:

  • Делайте обновление “активной версии” редким административным событием, а чтение — быстрым.
  • Для критичных операций используйте один явный “источник правды” (одна строка назначения), чтобы не было несогласованности.
  • ---

    5) Загрузка артефактов из S3: стратегии, которые не ломают SLO

    5.1. “Bake into image” vs “Pull on startup”

    Два рабочих подхода:

  • Запекать артефакты в Docker‑образ
  • - плюсы: минимальная зависимость от S3 в рантайме, быстрый старт при наличии образа - минусы: образ становится тяжёлым; каждое обновление модели требует пересборки/публикации

  • Тянуть артефакты из S3 на старте (startup/lifespan)
  • - плюсы: модель обновляется без пересборки образа; артефакты централизованы - минусы: зависимость от доступности S3 при старте; нужен локальный кеш, контроль целостности

    В production часто выбирают pull on startup + локальный диск‑кеш (на pod/VM), чтобы не скачивать гигабайты при каждом рестарте.

    5.2. Локальный кеш артефактов

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

  • Кешируйте по (model_id, version, checksum). Если checksum меняется — это новая версия.
  • Держите верхний лимит диска и политику очистки старых версий.
  • Никогда не считайте наличие файла достаточным: проверяйте checksum/размер.
  • 5.3. Атомарность “переключения версии”

    Опасный сценарий: Postgres уже переключили на новую версию, а часть реплик ещё не скачала артефакты.

    Практика:

  • Сначала публикуется версия артефактов (S3) + запись model_versions.
  • Затем сервисы скачивают/прогревают (readiness зависит от готовности runtime — см. статью про runtime lifecycle).
  • Только после этого переключают active_version.
  • Если вы делаете rollout процентами, убедитесь, что реплики, принимающие новый трафик, действительно имеют нужные файлы.

    ---

    6) Безопасность и доступы (минимум для production)

  • Разделяйте права: сервису serving нужны, как правило, read‑only на бакет артефактов.
  • Включайте шифрование на storage и ограничивайте доступ по принципу least privilege.
  • Не храните креды к S3/Postgres в коде; используйте секреты (см. статью про конфигурацию и секреты).
  • Логи/метрики не должны содержать S3‑токены, presigned URL и чувствительные части путей.
  • ---

    7) Мини‑чеклист: “готово к эксплуатации”

  • В Postgres есть таблицы версий и назначений (active version), уникальные ключи и индексы на критичных чтениях.
  • В S3 артефакты иммутабельны, есть manifest с checksum/размерами.
  • Runtime проверяет целостность скачанного набора и не становится ready без успешного прогрева.
  • Есть локальный кеш и лимит его роста.
  • Доступы к S3 минимальны (read‑only), к Postgres — строго по нужным операциям.
  • ---

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

  • Опишите, какие данные вы положите в Postgres, а какие — в S3 для LLM‑сервиса генерации. Приведите по 6 примеров для каждого.
  • Спроектируйте таблицу model_versions: перечислите минимум 8 полей (с назначением), которые помогут эксплуатации (совместимость runtime, контроль целостности, аудит).
  • Придумайте стратегию ключей S3 для артефактов CV‑модели, которая поддерживает: несколько моделей, несколько версий, разные форматы (onnx/engine), и хранение manifest.
  • Опишите сценарий «частичный rollout новой версии модели», где реплики могут оказаться в несогласованном состоянии. Какие 3 технических “предохранителя” вы добавите?
  • У вас высокая нагрузка, и Postgres начал быть источником latency. Назовите 5 гипотез (не “просто мало железа”) и по одной проверке для каждой.
  • <details> <summary> Ответы </summary>

    1) Пример разнесения.

    Postgres (метаданные):

  • список моделей (models)
  • версии моделей и их статус (model_versions)
  • активная версия по окружению/тенанту (deployments)
  • параметры rollout/канареек
  • аудит: кто/когда переключил версию
  • статусы async job’ов (если есть)
  • S3 (артефакты):

  • веса/граф модели (onnx/pt/engine)
  • токенизатор/vocab/merges
  • конфиги препроцессинга/постпроцессинга
  • label maps/списки классов
  • warmup‑примеры
  • manifest с checksum/размерами
  • 2) Пример полей model_versions:

  • id — первичный ключ
  • model_id — ссылка на models
  • version — строковая/семвер версия, уникальна в рамках модели
  • s3_prefix — базовый префикс папки версии
  • manifest_checksum — контроль целостности набора
  • size_bytes_total — суммарный размер, полезно для планирования диска/скачиваний
  • runtime_kind — pytorch/onnx/tensorrt
  • runtime_compat — например cpu/cuda + dtype (минимально)
  • created_at — когда опубликовано
  • created_by — кто опубликовал (если нужна трассируемость изменений)
  • 3) Пример ключей S3:

    Если нужно разделить по средам — добавьте env/ уровнем выше, но чаще лучше разделять бакетами/политиками доступа.

    4) Пример несогласованности: вы переключили active_version в Postgres, ingress начинает слать трафик на все реплики, но часть реплик ещё не скачала/не прогрела новую версию.

    Предохранители:

  • Readiness зависит от наличия и успешного warmup нужной версии (реплика не получает трафик, пока не готова).
  • Двухфазная операция: сначала загрузка/прогрев, затем переключение активной версии (или rollout только на готовые реплики).
  • Локальный кеш + проверка checksum: реплика не “думает”, что готова, если файлы частично скачались.
  • 5) Гипотезы по Postgres latency:

  • Слишком много соединений → проверить число активных коннектов и очереди на пул.
  • Нет нужных индексов на “горячие” чтения активной версии → проверить планы запросов и частоту seq scan.
  • Слишком частые записи (например, вы пишете событие на каждый запрос) → проверить write QPS и блокировки.
  • Долгие транзакции держат блокировки → проверить “долго живущие” транзакции.
  • Сетевая проблема между сервисом и БД/плохой DNS → проверить RTT/таймауты соединений и ретраи.
  • </details>

    19. Миграции БД: Alembic, транзакции, схемы таблиц

    Миграции БД: Alembic, транзакции, схемы таблиц

    В ML‑serving сервисе Postgres чаще используется для метаданных (версии моделей, назначения активной версии, статусы job’ов и т.п.; мотивацию см. статью про Postgres/S3). Тогда миграции — это не «формальность», а механизм, который гарантирует:

  • воспроизводимость деплоя (один и тот же код ↔ одна и та же схема),
  • безопасное изменение схемы под нагрузкой,
  • контроль совместимости между версиями приложения и БД.
  • Ниже — практическая дисциплина для Alembic + Postgres: как устроены ревизии, как использовать транзакции, как проектировать схемы таблиц и делать изменения без простоя.

    ---

    1) Alembic как «журнал изменений схемы»

    1.1. Базовые сущности

  • Revision — файл миграции с уникальным revision id и ссылкой на предыдущую (down_revision).
  • Upgrade — функция, которая переводит схему вперёд.
  • Downgrade — функция отката (не всегда реально безопасна для data‑миграций, но должна быть осознанной).
  • alembic_version — таблица, где хранится текущая применённая ревизия.
  • Практический смысл: код приложения и миграции живут в одном репозитории, а база “знает”, на какой ревизии она находится.

    1.2. Почему автогенерация — не «автопилот»

    Alembic умеет --autogenerate, сравнивая SQLAlchemy metadata и текущую БД. Но в production нужно считать, что автогенерация:

  • не понимает намерения (например, “сначала nullable, потом backfill, потом NOT NULL”),
  • может не увидеть изменения, если metadata неполная,
  • может предложить опасные операции (drop/rename) без контекста.
  • Правило: автогенерация — это стартовый черновик, финальная миграция всегда проходит ручной аудит.

    ---

    2) Транзакции в миграциях: что гарантируется, а что нет

    2.1. Транзакционная миграция — что это даёт

    Если миграция выполняется в транзакции, то при ошибке:

  • изменения не “наполовину применены”,
  • база возвращается в исходное состояние,
  • повторный запуск чаще всего безопасен.
  • Для Postgres большинство DDL действительно транзакционны.

    2.2. Операции, которые ломают «одну транзакцию на всё»

    В Postgres есть команды, которые не могут выполняться внутри транзакции, типичный пример — CREATE INDEX CONCURRENTLY.

    Следствие для Alembic:

  • иногда миграцию приходится делать в режиме “autocommit” (или дробить на несколько миграций),
  • нужно заранее определить, где допустимы “частично применённые” изменения и как их детектировать.
  • Практическая дисциплина: любые “нетранзакционные” операции выделяйте в отдельные ревизии с явным комментарием и проверками.

    2.3. Миграции данных (data migrations) и транзакции

    Миграция данных — это изменение содержимого таблиц, а не схемы (backfill, пересчёт полей, перенос колонок). В production под нагрузкой это опаснее DDL, потому что:

  • может занять минуты/часы,
  • держит блокировки,
  • конкурирует с сервисом за ресурсы.
  • Правило: тяжёлые backfill‑операции не делайте “внутри alembic upgrade” одним большим UPDATE. Лучше:

  • схема‑изменение через Alembic,
  • backfill отдельным джобом/скриптом порционно,
  • финальное ужесточение ограничений (NOT NULL/UNIQUE) отдельной миграцией.
  • ---

    3) Схемы таблиц: проектирование под миграции и high load

    3.1. Нейминг и явные constraints

    Чтобы миграции были стабильными, важно:

  • давать имена индексам и ограничениям (а не полагаться на авто‑имена),
  • фиксировать уникальности там, где они определяют инварианты (например, (model_id, version)),
  • не использовать “магические” implicit‑поведения.
  • Почему: при rename или переносе между схемами авто‑имена часто меняются, и миграции становятся хрупкими.

    3.2. Несколько схем (Postgres schema) vs один public

    Postgres schema (например, serving, ops) — это способ логически разделить объекты. Полезно, когда:

  • вы хотите отделить операционные таблицы от бизнес‑метаданных,
  • есть разные роли/права доступа,
  • нужно изолировать служебные объекты (например, для фоновых job’ов).
  • Но цена:

  • миграции должны явно указывать schema,
  • нужно следить за search_path (иначе “таблица не там”).
  • Практический минимум:

  • определите 1–2 схемы и закрепите в коде/миграциях,
  • не смешивайте “как получится”: это усложняет обслуживание.
  • 3.3. Эволюция схемы без простоя: паттерн expand/contract

    Под нагрузкой опасно делать изменения, которые требуют долгих блокировок или моментальной миграции данных. Рабочий паттерн:

  • Expand: добавили новую колонку/таблицу, не ломая старый код.
  • Dual‑write/compat: приложение пишет и читает в обе структуры (или читает с fallback).
  • Backfill: заполнили новые поля порционно.
  • Contract: удалили старое (колонку/таблицу/код) отдельной миграцией.
  • Визуально:

    Этот подход особенно важен для таблиц со статусами job’ов и аудитом, которые могут быстро расти.

    ---

    4) Типовые миграции и их риски (Postgres)

    4.1. Добавить колонку

  • Добавить nullable колонку без DEFAULT — обычно быстро.
  • Добавить колонку с DEFAULT и NOT NULL — может привести к переписыванию таблицы/долгой операции (зависит от версии Postgres и типа DEFAULT).
  • Безопасный шаблон:

  • добавить колонку NULL,
  • backfill,
  • поставить NOT NULL (после проверки).
  • 4.2. Индексы

    Индексы — классическая причина блокировок.

    Практика для больших таблиц:

  • создавать индекс “concurrently” (значит, вне транзакции),
  • заранее планировать, что миграция может пережить рестарт и быть повторяемой,
  • следить за тем, чтобы индекс реально использовался (иначе вы платите за запись).
  • 4.3. Уникальные ограничения

    UNIQUE — часто нужен для инвариантов (например, версия модели). Но вводить UNIQUE на существующие данные рискованно:

  • миграция упадёт, если есть дубликаты,
  • исправление “вручную” в проде болезненно.
  • Правило: перед добавлением UNIQUE — отдельная проверка/очистка (и лучше заранее на стейдже с копией данных).

    ---

    5) Миграции в CI/CD и при деплое

    (Детали пайплайнов и репозитория уже обсуждались ранее; здесь — только специфичное для миграций.)

    5.1. Где запускать миграции

    Два рабочих варианта:

  • Отдельный job/step в деплое (например, Kubernetes Job):
  • 1. понятно, кто и когда применил миграции, 2. легко повторить и логировать.
  • Init‑контейнер перед стартом приложения:
  • 1. гарантирует, что pod не стартует без миграций, 2. но требует аккуратности при параллельном старте многих реплик.

    Ключевое правило: миграции должен применять один исполнитель (либо за счёт оркестратора, либо через блокировку).

    5.2. Конкурентный запуск и блокировки

    Если два процесса одновременно применяют миграции, вы рискуете:

  • гонками,
  • частично созданными объектами,
  • нестабильным состоянием.
  • Практика:

  • используйте advisory lock (или инфраструктурную гарантию “одна миграция за раз”),
  • отделяйте миграции от горизонтального масштабирования приложения.
  • 5.3. Совместимость приложения и схемы

    Нельзя выкатывать схему и код “в вакууме”. Нужно обеспечить период, когда:

  • старая версия приложения работает на новой схеме,
  • новая версия приложения работает на старой схеме.
  • Это и есть смысл expand/contract и отказа от “ломающих” миграций одним шагом.

    ---

    6) Откаты (downgrade) и реальность

    Технически Alembic поддерживает downgrade, но в production откат часто означает:

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

  • downgrade пишем для простых DDL (создали таблицу — удалили),
  • для data migrations downgrade либо запрещён, либо делает только безопасную часть (например, не удаляет данные).
  • Важно: это должно быть явно описано в ревизии, чтобы “откат по кнопке” не стал катастрофой.

    ---

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

    1) Опишите 3 операции миграций, которые вы считаете “безопасными по умолчанию” для production под нагрузкой, и 3 операции, которые требуют отдельного плана выполнения.

    2) Для таблицы jobs (статусы async‑задач) вам нужно добавить поле finished_at и сделать его NOT NULL для статусов succeeded/failed. Спроектируйте пошагово изменения в стиле expand/contract.

    3) Придумайте набор из 6 правил ревью миграций (что reviewer проверяет обязательно), ориентируясь на риск блокировок и совместимость.

    4) Почему запуск миграций как init‑контейнер при 10 репликах приложения может быть опасен? Предложите 2 способа убрать риск.

    <details> <summary> Ответы </summary>

    1) Пример.

    Безопасные “по умолчанию” (часто быстро и без долгих блокировок):

  • Добавить nullable колонку без DEFAULT.
  • Создать новую таблицу (если она не используется горячим путём немедленно).
  • Добавить индекс на маленькой/новой таблице (или если проверено, что операция быстрая).
  • Опасные (нужен план):

  • Создание индекса на большой таблице (часто требуется CONCURRENTLY и контроль времени).
  • Добавление NOT NULL/UNIQUE на существующие данные без подготовки.
  • Большой backfill одним UPDATE внутри миграции (риск блокировок и долгого выполнения).
  • 2) Пример пошагово (expand/contract).

  • Expand: добавить finished_at NULL.
  • Обновить приложение: при переходе job в succeeded/failed писать finished_at=now(); чтение — с fallback.
  • Backfill: отдельной джобой заполнить finished_at для старых succeeded/failed.
  • Contract: добавить constraint/проверку на уровне БД (вариант: CHECK, что finished_at IS NOT NULL для status IN (...)).
  • После периода совместимости — ужесточить логику/убрать fallback в коде.
  • 3) Пример 6 правил ревью миграций.

  • Есть ли риск долгой блокировки (ALTER TABLE на большой таблице, индексы)?
  • Требует ли операция “вне транзакции” (CONCURRENTLY) и что будет при рестарте на середине?
  • Совместима ли новая схема со старым кодом (минимум на период rolling update)?
  • Есть ли явные имена для индексов/constraints (стабильность)?
  • Есть ли план для data backfill (не внутри миграции одним запросом), если нужен?
  • Понятна ли политика downgrade и описана ли она (что безопасно откатывать, что нет)?
  • 4) Почему init‑контейнер опасен при 10 репликах.

    Потому что 10 pod’ов могут одновременно попытаться применить миграции: гонки, конфликты DDL, нестабильное состояние.

    Способы убрать риск:

  • Делать миграции отдельным Job/step, который гарантированно запускается один раз, до масштабирования приложения.
  • Ввести блокировку на уровне БД (например, advisory lock) так, чтобы только один исполнитель мог выполнять миграции, а остальные ждали/завершались.
  • </details>

    2. Референс-архитектура: API, model runtime, storage, queue, observability

    Референс-архитектура: API, model runtime, storage, queue, observability

    Ниже — практичная референс‑архитектура ML‑serving сервиса на FastAPI, рассчитанная на high load и эксплуатацию в production. Она не привязана к одной модели (LLM/CV), но учитывает их типичные особенности. Базовые требования (latency/throughput, деградация, безопасность, воспроизводимость, метрики) уже разобраны в статье «Цели курса, стек, требования к production ML‑сервису» — здесь фокус на компонентах и границах ответственности.

    1) Карта компонентов и поток запроса

    Референс‑схема (минимально достаточная, но расширяемая):

    Ключевая идея: API слой не должен “знать” детали исполнения модели, а runtime — не должен принимать “сырые” входы из внешнего мира без политики ограничений и контроля.

    2) API: контракт, границы и “тонкая” логика

    2.1. Что относится к API слою

    API слой (FastAPI) отвечает за то, чтобы запрос был:

  • Легитимен: правильные типы/схемы, допустимые значения, лимиты размеров.
  • Безопасен для обработки: ограничения на payload, контент‑тайпы, быстрый отказ на некорректных входах.
  • Маршрутизируем: выбор версии модели/профиля инференса/параметров деградации (но не сам инференс).
  • Важно удерживать API слой “тонким”: тяжёлый препроцессинг (например, декодирование больших изображений или сложная нормализация текста) лучше вынести в сервисный слой или отдельный воркер, чтобы API‑воркеры не “залипали”.

    2.2. Дизайн endpoint’ов: sync, async, streaming

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

  • Синхронный инференс: клиент ждёт ответ. Подходит для коротких запросов и предсказуемой задержки.
  • Асинхронный инференс (job‑based): клиент получает job_id и опрашивает результат или получает callback/webhook.
  • Для LLM часто нужен третий режим:

  • Streaming‑ответ: сервер отдаёт токены/чанки по мере генерации. Это снижает perceived latency, но усложняет таймауты, ретраи, проксирование и наблюдаемость.
  • Правило: если модель может работать дольше типичного таймаута API‑шлюза или балансировщика, синхронный режим становится хрупким — лучше job‑based.

    2.3. Версионирование и совместимость

    Варианты, которые хорошо работают в production:

  • Версия в пути: /v1/predict.
  • Версия в заголовке: X-API-Version.
  • Версия в теле запроса (хуже для кеширования и контрактности, но иногда удобно для экспериментов).
  • Смысл: клиент должен явно понимать контракт. Версия модели и версия API — разные сущности: API может оставаться v1, но внутри переключаться модель model=2026-01-15 по конфигу или роутингу.

    3) Model runtime: как исполнять модель предсказуемо

    Model runtime — это “сердце” сервиса: загрузка артефактов, управление устройствами (CPU/GPU), батчинг, контроль параллелизма и памяти.

    3.1. Форматы и упаковка runtime

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

  • Python runtime (PyTorch/Transformers/etc.): максимальная гибкость, но выше риск непредсказуемых задержек из-за GIL/аллокатора/питоновских объектов.
  • ONNX Runtime: часто проще стабилизировать latency на CPU, хорошие опции оптимизаций.
  • TensorRT/компилированные графы: максимум производительности на GPU, но сложнее сборка и совместимость.
  • Критично: runtime должен иметь явные параметры (device, dtype, max_batch_size, ограничения контекста/разрешения, warmup), и эти параметры должны быть конфигурируемыми, а не “зашитыми”.

    3.2. Инициализация и жизненный цикл

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

  • Загрузка модели при старте процесса (startup hook).
  • Прогрев (warmup) на типовых размерах входов.
  • Отдельная проверка readiness: “модель загружена и выполняет тестовый прогон”.
  • Антипаттерн: лениво загружать модель при первом запросе — это создаёт хвостовые задержки и гонки инициализации.

    3.3. Параллелизм: “сколько запросов одновременно”

    Здесь важно разделить:

  • Параллелизм на уровне API (сколько соединений держим).
  • Параллелизм в runtime (сколько инференсов одновременно можно исполнять без OOM и без деградации latency).
  • Для GPU часто лучше контролируемая модель: ограниченное число одновременных инференсов + батчинг. Для CPU — больше конкурентности, но всё равно нужен предел, иначе получаются очереди в планировщике ОС и рост p99.

    3.4. Батчинг как часть runtime, а не API

    Батчинг должен жить рядом с моделью, потому что только runtime знает:

  • допустимые размеры батча,
  • “дорогие” размеры входов,
  • текущую загрузку GPU/CPU.
  • API слой лишь передаёт запросы в механизм батчинга через очередь/агрегатор.

    4) Storage: что хранить и где

    Storage в serving — это не только “где лежат веса”. В production обычно как минимум три класса хранилищ.

    4.1. Хранилище артефактов модели (model artifacts)

    Хранит:

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

    4.2. Конфигурационное хранилище

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

  • активная версия модели,
  • лимиты (max_tokens, max_image_side, max_payload),
  • фичи деградации,
  • проценты трафика в A/B.
  • Это может быть ConfigMap/Secret в Kubernetes или внешний конфиг‑сервис. Важнее не инструмент, а дисциплина: конфиг имеет версию и аудит изменений.

    4.3. Кеш/оперативное хранилище результатов

    Нужно не всегда, но часто окупается:

  • кеширование эмбеддингов,
  • кеширование ответов на повторяющиеся запросы,
  • дедупликация запросов (одинаковый input → один расчёт).
  • Требование: определить ключ (например, хэш нормализованного запроса + параметры инференса) и политику TTL/инвалидации.

    4.4. Данные запросов/ответов (для отладки)

    Сохранение “сырых” входов рискованно (PII/секьюрность/объём). Компромиссный подход:

  • логировать только метаданные,
  • хранить выборку для расследований по явному флагу,
  • маскировать чувствительные поля,
  • отделять диагностическое хранилище от основной линии обслуживания.
  • 5) Queue: контроль нагрузки и backpressure

    Очередь — это механизм, который превращает “шквал запросов” в управляемый поток.

    5.1. Виды очередей

  • In‑process очередь (в памяти процесса): минимальные задержки, но запросы пропадают при рестарте, сложно масштабировать между репликами.
  • Внешний брокер: устойчивость и масштабирование, но добавляет задержку и требует эксплуатации.
  • Выбор зависит от режима:

  • sync‑инференс: чаще in‑process + лимиты конкурентности.
  • async job‑based: чаще внешний брокер + отдельные воркеры.
  • 5.2. Backpressure как обязательная политика

    Backpressure — это “умение отказать заранее”, чтобы не умереть позже.

    Практика:

  • ограничить размер очереди,
  • ограничить число одновременно выполняемых инференсов,
  • при переполнении — возвращать контролируемый отказ (а не копить в памяти),
  • выделять отдельные очереди по классам запросов (например, быстрые/медленные, premium/free).
  • 5.3. Приоритизация и справедливость

    Если есть разные типы запросов (LLM с длинным контекстом vs короткие, CV с большим изображением vs маленьким), полезно:

  • вводить классы обслуживания,
  • давать им разные лимиты,
  • избегать ситуации, когда “тяжёлые” запросы забивают всё и растят p99 для всех.
  • 6) Observability: как “видеть” систему целиком

    Observability в референс‑архитектуре должна быть сквозной: API → очередь → runtime → внешние зависимости.

    6.1. Корреляция запросов

    Нужен единый идентификатор запроса, который проходит через все слои:

  • приходит от клиента или генерируется на входе,
  • попадает в логи,
  • попадает в трейсы,
  • по возможности добавляется в ответ.
  • Это превращает расследование инцидентов из “угадайки” в поиск по одному ключу.

    6.2. Трейсинг по стадиям

    Минимально полезная декомпозиция времени запроса:

  • валидация и парсинг,
  • препроцессинг (декодирование изображения / токенизация),
  • ожидание в очереди/батчере,
  • инференс,
  • постпроцессинг,
  • сериализация ответа.
  • Ценность: вы понимаете, что именно стало бутылочным горлышком (например, токенизация, а не модель).

    6.3. Метрики уровня сервиса vs уровня модели

    На практике разделяют два набора:

  • Сервисные: успешность, коды ответов, latency по endpoint’ам, насыщение очереди.
  • Модельные/рантайм: размер батча, время инференса, использование GPU памяти, частота OOM/перезапусков.
  • Главное архитектурное решение: метрики должны быть доступны независимо от того, умеет ли модель “логировать сама”. Поэтому наблюдаемость строится вокруг слоёв исполнения.

    6.4. Логи: структурированные, но “не болтливые”

    Логи должны быть:

  • структурированные (ключ‑значение),
  • с уровнями (info/warn/error),
  • без чувствительных данных по умолчанию,
  • с отдельным каналом для “тяжёлой” отладки по флагу.
  • Антипаттерн: логировать полные тексты промптов или бинарные изображения в обычный лог‑поток.

    7) Сводные “контуры” референс‑архитектуры

    Чтобы архитектура не расползалась, зафиксируйте контуры:

  • Контур API: контракт, лимиты, аутентификация, корреляция.
  • Контур управления нагрузкой: очередь, батчинг, лимиты конкурентности, приоритеты.
  • Контур runtime: загрузка/прогрев, устройство, управление памятью.
  • Контур данных: артефакты модели, конфиги, кеши.
  • Контур наблюдаемости: метрики/логи/трейсы, единые идентификаторы, разбиение по стадиям.
  • Если вы можете отдельно тестировать и масштабировать каждый контур — архитектура близка к production‑здоровой.

    ---

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

    1) Нарисуйте (в текстовом виде) свой вариант схемы компонентов для LLM‑сервиса в двух режимах: sync и async job‑based. Укажите, где появляется очередь и где происходит батчинг.

    2) Для CV‑инференса перечислите 5 причин, почему “толстый” API слой может убить производительность, и какие части логики вы бы вынесли из API.

    3) Опишите политику backpressure для ситуации: входящий RPS вырос в 5 раз, GPU занята, очередь растёт. Какие решения принимаются на уровнях API, очереди и runtime?

    4) Предложите схему ключа кеша для LLM‑ответов, чтобы не получать “ложные совпадения” при изменении параметров генерации.

    5) Составьте минимальный список стадий для трейса запроса (span’ы), чтобы быстро отличать проблемы токенизации, очереди и GPU‑инференса.

    <details> <summary> Ответы </summary>

    1) Пример схем (упрощённо).

    Sync:

    Async job‑based:

    Батчинг: рядом с workers/runtime, а не в FastAPI.

    2) Пример причин “толстого” API:

  • Декодирование изображений в API‑воркерах блокирует обработку других запросов.
  • Нестабильное время препроцессинга растит p99.
  • Риск OOM/утечек памяти в веб‑процессе → падает весь API.
  • Тяжёлые операции усложняют таймауты и ретраи.
  • API становится трудно масштабировать (нужно масштабировать CPU ради препроцессинга, хотя узкое место — GPU).
  • Что вынести: декодирование/ресайз/нормализацию, батчинг, работу с GPU, тяжёлую постобработку.

    3) Пример backpressure политики:

  • API: жёсткие лимиты на размер входа, ограничение одновременных запросов, быстрый отказ при перегрузе.
  • Очередь: ограниченный размер; при превышении — отказ новых задач или понижение приоритета “тяжёлых” классов.
  • Runtime: динамически уменьшить max_batch_size/параллелизм, включить более агрессивное усечение (LLM) или уменьшение входного размера (CV), отключить дорогие опции постпроцессинга.
  • 4) Пример ключа кеша:

  • Хэш от нормализованного prompt (и system prompt, если есть)
  • Версия модели
  • Параметры генерации (temperature, top_p, max_tokens, stop sequences)
  • Параметры препроцессинга (например, truncation policy)
  • Идея: любые изменения, влияющие на результат, должны менять ключ.

    5) Минимальный набор span’ов:

  • request_parse_validate
  • preprocess_tokenize / preprocess_decode_image
  • queue_wait_or_batch_wait
  • model_infer
  • postprocess
  • response_serialize
  • По этим стадиям обычно быстро видно, где ушло время.

    </details>

    20. Наблюдаемость: Prometheus metrics, custom latency histograms

    Наблюдаемость: Prometheus metrics, custom latency histograms

    В прошлых статьях мы уже зафиксировали какие сигналы нужны (логи/метрики/трейсы, p95/p99, backpressure) и почему request_id важен для корреляции. Здесь — строго про метрики в стиле Prometheus и про самый частый «острый» вопрос в serving: как правильно измерять latency хвосты через кастомные histogram’ы так, чтобы ими реально можно было управлять под high load (LLM/CV).

    ---

    1) Что именно считаем «метрикой» в Prometheus

    Prometheus опирается на модель: сервис экспортирует метрики (обычно HTTP endpoint /metrics), Prometheus их scrape’ит, а дальше вы строите алерты/дашборды.

    Ключевое отличие от логов: метрики — это числовые ряды, по которым вы агрегируете и сравниваете состояние системы.

    1.1. Базовые типы (и как они применяются в serving)

  • Counter (монотонно растёт): запросы, ошибки, отмены, OOM.
  • Gauge (может расти/падать): число активных запросов, длина очереди, in-flight.
  • Histogram: распределения (latency, размер батча, время ожидания в очереди).
  • Latency почти всегда надо мерить именно histogram’ом: иначе вы либо теряете хвосты, либо получаете метрики, которые невозможно агрегировать корректно.

    ---

    2) Договорённость по именам и единицам: чтобы метрики были пригодны

    Минимальные правила, которые резко упрощают эксплуатацию:

  • Единицы в имени:
  • 1. длительности — _seconds (а не миллисекунды в названии), 2. размеры — _bytes, 3. количества — без единиц.
  • Один смысл — одно имя: не смешивайте разные режимы в одной метрике без явного label.
  • Низкая кардинальность label’ов: не добавляйте request_id, сырой user_id, полный URL с query.
  • Вместо этого используйте стабильные, «группируемые» измерения: endpoint, method, status_code, profile, error_code.

    ---

    3) Минимальный набор метрик для production ML-serving

    Ниже — практический набор, который покрывает основные вопросы эксплуатации, не превращая Prometheus в свалку.

    3.1. HTTP-уровень

  • http_requests_total{endpoint, method, status_code} — Counter
  • http_request_duration_seconds{endpoint, method} — Histogram
  • http_in_flight_requests{endpoint} — Gauge
  • Важно: endpoint лучше нормализовать (например, шаблон /v1/jobs/{id}, а не фактический путь), иначе кардинальность взорвётся.

    3.2. Слой inference/runtime (то, что «объясняет» p99)

  • inference_queue_wait_seconds{profile, queue} — Histogram (если есть очередь/батчер)
  • inference_duration_seconds{profile, model} — Histogram (чистый runtime)
  • batch_size{profile} — Histogram или Summary (часто удобнее histogram)
  • overload_rejections_total{reason, profile} — Counter
  • reason должен быть коротким и стабильным (например: queue_full, concurrency_limit, cost_limit).

    3.3. Ошибки по причинам (интеграция с вашим error model)

    Если у вас есть единый error_code (как в статье про обработку ошибок), метрики должны «видеть» именно его:

  • errors_total{error_code, endpoint} — Counter
  • Не делайте error_message label’ом: это почти гарантированный взрыв кардинальности.

    ---

    4) Почему latency histogram — главный инструмент для SLO

    4.1. Что даёт histogram

    Histogram хранит не «одно число», а распределение по корзинам (buckets). Из него вы можете:

  • оценивать p95/p99 через histogram_quantile(...) в PromQL,
  • сравнивать хвосты между профилями (fast vs quality),
  • строить burn-rate алерты на «слишком медленно».
  • 4.2. Почему не Summary

    Summary выглядит удобно (сразу «p95 внутри сервиса»), но для production мониторинга обычно хуже:

  • агрегировать summary по репликам корректно сложно,
  • вы теряете гибкость по окнам и группировкам.
  • Исключение: локальные диагностики, но для «источника правды» по SLO — histogram почти всегда предпочтительнее.

    ---

    5) Custom latency histograms: как выбрать bucket’ы правильно

    Главная цель bucket’ов — сделать p95/p99 достаточно точными в области ваших SLO, не создавая лишнего шума.

    5.1. Практический алгоритм выбора bucket’ов

  • Определите рабочий диапазон latency для endpoint’а.
  • 1. embeddings на CPU: условно 10–300 мс, 2. LLM generation: может быть 0.3–10+ секунд.
  • Сделайте корзины плотнее вокруг порогов, важных для SLO (например 0.1s, 0.2s, 0.5s).
  • Держите верхнюю корзину (+Inf) — она всегда будет, но важно, чтобы «основная масса» не проваливалась в неё.
  • 5.2. Два типовых набора bucket’ов (как отправная точка)

    A) «Быстрые» endpoint’ы (валидация/embeddings/классификация)

  • 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2 (сек)
  • B) «Долгие» endpoint’ы (LLM generate sync)

  • 0.05, 0.1, 0.2, 0.5, 1, 2, 3, 5, 8, 13, 21, 34 (сек)
  • Важно: это не «магические» числа. Дальше вы подстраиваете корзины по фактическому распределению.

    5.3. Разделяйте разные виды latency

    Одна из самых частых ошибок: мерить только «общую» latency. Для управления p99 вам нужны хотя бы два histogram’а:

  • http_request_duration_seconds — «глазами клиента» внутри сервиса
  • inference_duration_seconds — чистый runtime
  • Если есть батчер/очередь, добавьте ещё один:

  • inference_queue_wait_seconds
  • Именно разложение по стадиям позволяет отличить «медленная модель» от «очередь распухла».

    ---

    6) Labels и кардинальность: как не убить Prometheus

    6.1. Что можно (обычно безопасно)

  • endpoint (нормализованный)
  • method
  • status_code
  • profile (ограниченное множество значений)
  • model / model_version (если версий немного и это реально нужно)
  • 6.2. Что почти всегда нельзя

  • request_id
  • полный текст ошибки
  • «сырые» идентификаторы пользователя, если их очень много
  • динамические параметры запроса (например, max_tokens=... как label)
  • Если вам нужно различать «тяжёлые» запросы, делайте это через бакетизацию/классы:

  • input_size_bucket="small|medium|large"
  • Границы классов фиксируйте политикой сервиса.

    ---

    7) FastAPI + несколько воркеров: важная оговорка про метрики

    При запуске нескольких процессов (gunicorn/uvicorn workers) есть две реальные стратегии:

  • Один процесс = один endpoint /metrics (Prometheus scrape’ит каждый pod/worker отдельно через service discovery)
  • Multiprocess mode (агрегация метрик нескольких воркеров в одном /metrics)
  • Вторая стратегия требует дисциплины (общая директория для метрик, корректный lifecycle очистки), иначе вы получите «битые» ряды и странные скачки.

    Практическое правило для serving: чем проще путь метрик, тем меньше сюрпризов. Если можете — выбирайте схему, где Prometheus видит метрики на уровне pod’а (а масштабирование делает через реплики).

    ---

    8) Что смотреть в PromQL (минимальный набор запросов)

  • RPS по endpoint:
  • - rate(http_requests_total{endpoint="/v1/embeddings"}[1m])
  • p95 latency по endpoint:
  • - histogram_quantile(0.95, sum by (le, endpoint) (rate(http_request_duration_seconds_bucket[5m])))
  • Доля ошибок (5xx):
  • - sum(rate(http_requests_total{status_code=~"5.."}[5m])) / sum(rate(http_requests_total[5m]))
  • Перегруз (отказы backpressure):
  • - rate(overload_rejections_total[1m])

    Эти запросы — «скелет» мониторинга. Дальше вы добавляете разрезы по profile, model и т.д.

    ---

    9) Антипаттерны, которые чаще всего ломают метрики

  • Один histogram на «всё подряд» (без endpoint/profile) → вы не можете управлять SLO.
  • Bucket’ы «по умолчанию» без привязки к вашим задержкам → p95/p99 становятся бессмысленными.
  • Высококардинальные labels → Prometheus начинает «тонуть».
  • Метрики без связи с политиками сервиса (нет метрик очереди/отказов) → вы не понимаете перегруз.
  • ---

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

    1) Составьте минимальный список из 8 метрик для ML-serving, который позволит ответить на вопросы:

  • «Мы перегружены или модель стала медленнее?»
  • «Падает ли сервис из-за ошибок клиента или сервера?»
  • «Что происходит с хвостами p99 по основному endpoint?»
  • 2) Для endpoint’а /v1/generate (LLM, sync) предложите набор bucket’ов histogram’а latency, исходя из того, что нормальная задержка 0.7–4 секунды, а в перегруз может уходить до 15 секунд.

    3) Укажите 6 labels, которые вы считаете опасными по кардинальности в production, и для двух из них предложите безопасную замену.

    4) У вас есть http_request_duration_seconds и inference_duration_seconds. Опишите 3 диагностических сценария, когда:

  • растёт только http_request_duration_seconds
  • растут оба
  • растёт только inference_duration_seconds
  • <details> <summary> Ответы </summary>

    1) Пример набора 8 метрик:

  • http_requests_total{endpoint, status_code} — нагрузка и коды ответа
  • http_request_duration_seconds{endpoint} — хвосты на уровне HTTP
  • http_in_flight_requests{endpoint} — насыщение по конкурентности
  • errors_total{error_code, endpoint} — причины ошибок (через стабильные коды)
  • overload_rejections_total{reason, profile} — явный backpressure/лимиты
  • inference_queue_wait_seconds{profile} — рост очереди/батчера
  • inference_duration_seconds{profile, model} — чистый runtime
  • batch_size{profile} — понять, работает ли micro-batching и какие размеры батча получаются
  • Этим набором вы отличаете «очередь/лимиты» от «модель стала медленнее», и разделяете ошибки клиента/сервера по status_code и/или error_code.

    2) Пример bucket’ов для /v1/generate:

  • 0.1, 0.2, 0.5, 0.75, 1, 1.5, 2, 3, 4, 6, 8, 10, 12, 15
  • Логика:

  • плотные корзины вокруг рабочей зоны 0.7–4s,
  • отдельные корзины в области перегруза до 15s, чтобы p95/p99 не «сливались» в +Inf.
  • 3) Примеры опасных labels:

  • request_id
  • полный URL с query
  • user_id (миллионы значений)
  • error_message (динамический текст)
  • «сырой» prompt hash, если запросов очень много и нет повторяемости
  • job_id для async
  • Безопасные замены:

  • вместо user_idtenant или class_of_service (ограниченное множество)
  • вместо полного URL → endpoint_template (нормализованный путь)
  • 4) Диагностические сценарии:

  • Растёт только http_request_duration_seconds:
  • - часто означает рост времени до/после модели: ожидание очереди, блокировка event loop, сериализация ответа, лимиты соединений, проблемы сети/ingress (если меряете на границе приложения), или рост времени в middleware.

  • Растут оба:
  • - модель реально стала медленнее (новая версия, другая конфигурация), либо общий перегруз CPU/GPU таков, что и runtime, и HTTP обвязка деградируют.

  • Растёт только inference_duration_seconds:
  • - чаще всего это деградация runtime: изменения модели/квантизации/ONNX, фрагментация GPU памяти, рост входов в рамках допустимых лимитов, неверные настройки потоков/конкурентности вокруг runtime.

    </details>

    21. Трейсинг: OpenTelemetry, traces до БД и внешних сервисов

    Трейсинг: OpenTelemetry, traces до БД и внешних сервисов

    Трейсинг (distributed tracing) отвечает на вопрос: куда ушло время конкретного запроса и какая зависимость стала причиной хвоста p95/p99. В предыдущих статьях курса уже были зафиксированы разбиение latency по стадиям, request_id и метрики-гистограммы; здесь сфокусируемся на том, как сделать сквозной trace через FastAPI → внутренние слои → БД/кеш/внешние сервисы → очередь/воркеры и как не превратить трейсинг в источник проблем (кардинальность, PII, оверхед).

    1) Базовая модель: trace, span, context

  • Trace — “история” обработки одного запроса/операции (например, один POST /v1/embeddings).
  • Span — участок работы внутри trace (например, db.query, model.infer, redis.get).
  • Context propagation — механизм передачи контекста трейсинга между компонентами (HTTP заголовки, message headers в очередях, внутренний контекст async).
  • Практическое правило: для production ML-serving ценность дают не «много спанов», а корректные границы и контролируемые атрибуты.

    Минимальная иерархия спанов для inference

    Эта структура напрямую поддерживает диагностику: “медленно из-за очереди/батчера”, “медленно из-за БД”, “медленно из-за runtime”.

    2) OpenTelemetry: что именно вы внедряете

    OpenTelemetry (OTel) — это стандарт и набор SDK/инструментов, которые дают:

  • API/SDK для создания спанов.
  • Автоинструментацию для популярных библиотек (FastAPI/ASGI, HTTP clients, DB drivers).
  • Экспорт трейс-данных в выбранный backend.
  • В production вы проектируете три вещи:

  • Где создаётся root span (обычно входящий HTTP запрос в FastAPI).
  • Какие зависимости инструментируются (Postgres, Redis, внешние HTTP, брокер очереди).
  • Как управляется sampling и атрибуты, чтобы не утечь PII и не убить производительность.
  • 3) Инструментация FastAPI: root span и маршрутизация

    3.1. Root span и корректные имена

    Для HTTP входа почти всегда нужен единый server-span, который:

  • имеет имя в стиле HTTP {method} {route} (важно: route-шаблон, а не “сырой path”, иначе будет кардинальность);
  • содержит признаки для разрезов: http.method, http.route, http.status_code.
  • Если у вас есть версионирование /v1/..., убедитесь, что в атрибутах фиксируется именно шаблон (/v1/jobs/{id}), а не конкретный ID.

    3.2. Связь с вашим request_id

    request_id (из middleware/логов) и trace-id — разные сущности. В трейсинге обычно достаточно:

  • добавить request_id как атрибут на root span;
  • прокидывать его в ваши структурированные логи (как уже обсуждалось в статье про логирование);
  • при необходимости возвращать клиенту и request_id, и trace-id (но не делайте trace-id публичным контрактом без причины).
  • 4) Traces до БД: Postgres и транзакции

    4.1. Что вы хотите увидеть в спанах БД

    В ML-serving БД чаще хранит метаданные (версии моделей, активные назначения, статусы job’ов). Для диагностики полезны:

  • время ожидания соединения/пула;
  • время выполнения запроса;
  • тип операции (SELECT/INSERT/UPDATE);
  • имя таблицы/“класс запросов” (вместо сырого SQL).
  • Главное ограничение: не логируйте и не кладите в атрибуты полный SQL, если там могут появиться пользовательские данные. Если нужна детализация — используйте нормализованные метки уровня “какой запрос” (например, db.operation=select_active_model_version).

    4.2. Важный эффект под high load

    Трейсинг БД помогает отличить:

  • проблему приложения (слишком частые обращения, лишние запросы, отсутствие кеша);
  • проблему пула (очередь за соединением);
  • проблему самой БД (медленные запросы, блокировки).
  • Это особенно полезно, когда p99 растёт, а по метрикам CPU/GPU “всё нормально”.

    5) Traces до внешних HTTP сервисов

    В ML-serving типичные внешние вызовы:

  • model registry / object storage gateway (если ходите за артефактами);
  • внешняя модерация/политики (LLM);
  • feature store / справочники;
  • внутренние микросервисы.
  • 5.1. Контекст-пропагация между сервисами

    Чтобы один trace продолжался через несколько сервисов, клиент обязан передать контекст в исходящих запросах, а сервер — принять.

    Практически это означает:

  • на входе FastAPI создаётся/восстанавливается контекст трейсинга;
  • при вызове внешнего HTTP-клиента контекст автоматически инжектится в headers;
  • downstream сервис тоже инструментирован и продолжает trace.
  • Без этого вы получите “обрыв” — отдельные несвязанные traces, что резко снижает ценность.

    5.2. Что класть в атрибуты внешнего вызова

    Полезно:

  • http.method, server.address/peer.service (логическое имя сервиса);
  • http.status_code;
  • retry-факт (если вы делаете ретраи внутри клиента).
  • Опасно:

  • полный URL с query параметрами;
  • body запроса/ответа;
  • токены авторизации.
  • 6) Очереди и воркеры: trace через async/job-based

    В job-based архитектуре “один запрос клиента” превращается в два ключевых участка:

  • publish job (API принял и положил задачу);
  • consume+execute job (воркер сделал инференс и сохранил результат).
  • 6.1. Как правильно связать эти части

    Правильная цель: trace должен иметь связь между “созданием job” и “выполнением job”. Для этого контекст нужно:

  • сохранить в message headers (или рядом в result store как trace-context);
  • восстановить в воркере перед выполнением;
  • продолжить trace в воркере.
  • Тогда расследование “почему job готовилась 40 секунд” раскладывается на:

  • очередь/ожидание воркера,
  • runtime,
  • запись результата/метаданных.
  • 6.2. Состояния job как события трейсинга

    Не нужно делать спан на каждую мелочь, но полезно фиксировать ключевые переходы:

  • job.queued (поставлена в брокер),
  • job.started (взята воркером),
  • job.completed / job.failed.
  • Они могут быть:

  • отдельными спанами,
  • или событиями (events) внутри спана job.
  • Выбор зависит от backend и требований к детализации.

    7) Ручные спаны: runtime модели и “границы ответственности”

    Автоинструментация не знает, что такое “tokenize” или “GPU infer”. Поэтому для ML-serving почти всегда добавляют ручные спаны в application/runtime слоях (с учётом архитектурных границ из статьи про Clean/Hexagonal).

    Правило: ручные спаны должны соответствовать вашим стадиям наблюдаемости, а не внутренним функциям “как получилось”.

    Минимум для LLM/CV runtime:

  • preprocess (токенизация/декодирование),
  • infer (чистое выполнение),
  • postprocess.
  • Дополнительно (если есть):

  • batch_wait/queue_wait,
  • cache_get/cache_set.
  • 8) Sampling и стоимость трейсинга

    Под high load трейсинг нельзя включать “на всё и всегда” без политики.

    8.1. Два практичных режима

  • Head-based sampling: решение “трейсим или нет” принимается на входе (дешевле).
  • Tail-based sampling: решение принимается после факта (например, сохранить все медленные/ошибочные traces) — требует инфраструктуры и обычно сложнее.
  • Частая production-политика:

  • ошибки — 100% (или близко к 100%),
  • медленные запросы — повышенная доля,
  • успешные быстрые — небольшой процент.
  • Важно согласовать это с тем, как вы строите метрики и error budget (см. статью про NFR/SLO).

    9) Атрибуты, кардинальность и безопасность данных

    Трейсинг легко “убить” высокой кардинальностью и утечкой данных.

    9.1. Что почти всегда нельзя класть в атрибуты

  • полный prompt/текст пользователя;
  • base64 изображений/файлов;
  • user_id высокой кардинальности (если нужны разрезы — используйте tenant/class_of_service);
  • сырой текст ошибок внешних библиотек, если он может содержать куски входа.
  • 9.2. Что можно и нужно

  • размеры/классы размеров (например, input_size_class=small|medium|large);
  • профиль (fast/quality);
  • версию модели (если число версий ограничено и это нужно для диагностики);
  • стабильные error_code (из вашего error model).
  • 10) Практическая “цель готовности”: что должно быть видно в одном трейсе

    Сервис можно считать “трейсинг-готовым”, если по одному trace вы можете ответить:

  • это задержка из-за очереди/батчера или из-за runtime;
  • был ли внешний вызов (БД/Redis/HTTP) и сколько он занял;
  • какой error_code произошёл и на какой стадии;
  • какая версия модели/профиль обслужили запрос.
  • ---

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

    1) Нарисуйте (текстом) дерево спанов для POST /v1/embeddings, если сервис:

  • проверяет политику лимитов,
  • читает active model version из Postgres,
  • проверяет кеш в Redis,
  • при miss делает инференс,
  • пишет метаданные запроса в Postgres.
  • 2) Для job-based режима опишите, где именно хранится trace-context:

  • в каком виде,
  • как он передаётся от API к воркеру,
  • что будет, если контекст потеряли.
  • 3) Придумайте 8 атрибутов для root span HTTP запроса inference, разделив их на:

  • безопасные по умолчанию,
  • опасные (и чем их заменить).
  • 4) Опишите sampling-политику для сервиса с 1500 RPS:

  • какой процент успешных запросов трейсите,
  • как поступаете с 5xx,
  • как поступаете с запросами медленнее порога.
  • <details> <summary> Ответы </summary>

    1) Пример дерева:

    Важное: имена db.* — нормализованные (класс операции), без сырого SQL.

    2) Trace-context в job-based:

  • Вид: сериализованный контекст (например, набор trace headers как key-value) или компактная структура trace_id/span_id/flags.
  • Передача: кладётся в headers сообщения брокера (или сохраняется рядом с job метаданными в result store и воркер читает его по job_id).
  • Если контекст потеряли: воркер создаст новый root trace, и связь “HTTP принял job → воркер выполнил” исчезнет. Тогда вы увидите два несвязанных trace и потеряете основную ценность диагностики очереди.
  • 3) Пример атрибутов:

    Безопасные:

  • http.method
  • http.route (шаблон)
  • http.status_code
  • profile (fast/quality)
  • model_id
  • model_version (если допустимо по кардинальности)
  • input_size_class (small/medium/large)
  • error_code (если ошибка)
  • Опасные:

  • prompt/text → заменить на input_chars или input_size_class.
  • image_base64 → заменить на payload_bytes и (после декодирования) image_width/height.
  • user_id → заменить на tenant_id или class_of_service.
  • 4) Пример sampling:

  • Успешные: 1–5% (зависит от бюджета и оверхеда).
  • 5xx: 100% (или максимально близко к 100%), чтобы расследования не были “слепыми”.
  • Медленные: повышенный sampling, например 20–50% для запросов, где latency > threshold (реализуется либо tail-based, либо отдельной политикой на уровне SDK/collector, если доступно).
  • </details>

    22. Healthchecks: liveness/readiness/startup probes

    Healthchecks: liveness/readiness/startup probes

    Healthchecks — это механизм, который позволяет оркестратору (чаще всего Kubernetes) понимать, жив ли процесс, готов ли он обслуживать трафик и нормально ли проходит старт. Для ML-serving (LLM/CV) probes критичны, потому что:

  • Инициализация может быть долгой (скачивание артефактов, загрузка в GPU, warmup).
  • Под нагрузкой процесс может «залипнуть» (deadlock, утечки памяти, зависшие внешние вызовы).
  • Частые рестарты вредны: они сбрасывают кеши, прогрев, увеличивают cold-start хвосты.
  • Про жизненный цикл runtime (загрузка/прогрев/readiness) мы уже говорили в статье про ML runtime — здесь фокус на том, как именно это экспонировать наружу и как настроить probes так, чтобы Kubernetes помогал, а не усугублял.

    ---

    1) Три вида probe и их контракт

    1.1. Liveness probe: «процесс жив?»

    Назначение: обнаружить состояние, когда процесс уже не способен работать в принципе (например, завис и не отвечает), и перезапустить контейнер.

    Что должно считаться успехом: минимальный ответ от приложения.

    Что НЕ нужно проверять в liveness:

  • Доступность БД/Redis/S3.
  • Наличие загруженной модели.
  • Свободная GPU память.
  • Причина: временная деградация зависимостей или «не готовность» — это не повод убивать процесс. Иначе вы получите каскадные рестарты (инфраструктура падает → liveness падает → рестартов ещё больше).

    Практический контракт: endpoint типа /health/live отвечает 200, если event loop/процесс способен обработать запрос.

    1.2. Readiness probe: «можно ли посылать трафик?»

    Назначение: управлять тем, получает ли pod запросы через Service/Ingress.

    Для ML-serving readiness — главный инструмент, чтобы:

  • Не отдавать трафик на pod до загрузки модели и warmup.
  • Вывести pod из ротации при деградации runtime (например, модель выгружена/сломалась).
  • Что обычно проверяют в readiness:

  • Runtime модели загружен.
  • Прогрев выполнен (или хотя бы минимальный тестовый прогон прошёл).
  • Критичные внутренние компоненты готовы: лимитер/очередь/батчер инициализированы.
  • Вопрос про внешние зависимости:

  • Если endpoint инференса жёстко зависит от внешней системы (например, вы обязаны читать active model version из Postgres на каждый запрос), то readiness может включать проверку доступности этой зависимости.
  • Но проверка должна быть быстрой и с жёстким таймаутом, иначе вы превратите readiness в источник ложных флапов.
  • 1.3. Startup probe: «старт ещё не завершился, но это нормально»

    Назначение: дать контейнеру много времени на инициализацию, не провоцируя рестарты.

    Startup probe особенно важен для ML, где cold start может занимать десятки секунд и больше.

    Ключевое поведение Kubernetes:

  • Пока startupProbe не успешен, Kubernetes не применяет livenessProbe (и часто не применяет readinessProbe в обычном режиме).
  • Это защищает от ситуации «не успели загрузить модель → liveness упал → вечный рестарт-луп».
  • ---

    2) Практическая схема endpoint’ов health

    Рекомендуемая поверхность API для health:

    2.1. Почему лучше разделять, а не делать один /health

    Разные probes решают разные задачи и требуют разной логики.

    Если вы смешаете всё в одном endpoint:

  • Вы не сможете отделить «не готов» от «умер».
  • Вы получите нестабильные рестарты из-за внешних зависимостей.
  • Будет трудно расследовать инциденты: какой именно контур сломался.
  • ---

    3) Что именно проверять внутри readiness для ML-serving

    Readiness должен отвечать на вопрос: “если сейчас придёт пользовательский запрос, мы обработаем его в рамках политики сервиса?”

    Минимальный «боевой» набор проверок для ML-serving:

  • Model runtime state: модель загружена, device корректен, токенизатор/препроцессор готов.
  • Warmup marker: хотя бы один прогон выполнен (или выполнен набор прогревов по профилям, если это часть вашей политики).
  • Backpressure/limiter state: лимитер/очередь создан(а) и не находится в аварийном состоянии.
  • Что важно:

  • Проверки должны быть константно быстрыми (не запускайте инференс на каждый readiness-запрос).
  • Любая проверка должна иметь таймаут, чтобы readiness сам не подвешивал процесс.
  • Практический компромисс:

  • В runtime вы делаете единичный “self-test inference” при старте (в lifespan) и сохраняете флаг runtime_ok=True.
  • Readiness читает этот флаг.
  • ---

    4) Что именно проверять в liveness (и как поймать “залип”)

    Если liveness слишком «умный», он становится опасным. Но если слишком «тупой», он не ловит зависания.

    Баланс, который обычно работает:

  • Проверить, что процесс отвечает быстро (например, выполнить очень лёгкую операцию в event loop).
  • (Опционально) добавить “heartbeat”/watchdog: периодический фоновой тик обновляет timestamp, а liveness проверяет, что тик не отстал.
  • Смысл heartbeat:

  • Если event loop завис (deadlock/блокировка), timestamp перестанет обновляться, и liveness справедливо перезапустит pod.
  • Но не включайте в liveness внешние вызовы (Redis/Postgres), иначе любой сетевой сбой станет рестартом.

    ---

    5) Настройки probes: как выбрать параметры

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

  • timeoutSeconds: сколько ждать ответа.
  • periodSeconds: как часто проверять.
  • failureThreshold: сколько раз подряд можно ошибиться.
  • initialDelaySeconds: задержка перед первым запуском (часто не нужна, если есть startupProbe).
  • successThreshold: сколько успехов нужно подряд (для readiness иногда полезно).
  • 5.1. Startup probe для тяжёлой модели

    Если cold start может быть 60–120 секунд:

  • Делайте startupProbe с большим допуском по времени через комбинацию periodSeconds * failureThreshold.
  • timeoutSeconds делайте небольшим (health должен отвечать быстро), а не огромным.
  • Идея: мы даём много попыток, но каждая попытка короткая.

    5.2. Readiness: быстро выключать из ротации, но без флапов

    Readiness должен реагировать быстрее, чем liveness:

  • чтобы pod быстро перестал получать трафик,
  • но при этом не уходил в «готов/не готов» каждую секунду.
  • Практика:

  • periodSeconds — несколько секунд.
  • failureThreshold — 2–3, чтобы отфильтровать краткие шумы.
  • 5.3. Liveness: консервативнее

    Liveness лучше делать более терпеливым:

  • меньше ложных рестартов,
  • больше шансов пережить кратковременный GC/пики CPU.
  • ---

    6) Частые ошибки в ML-serving probes

  • Readiness делает реальный инференс каждый раз → лишняя нагрузка и рост p99.
  • Liveness зависит от Redis/Postgres/S3 → каскадные рестарты при проблемах сети.
  • Нет startupProbe при долгой загрузке → рестарт-луп на старте.
  • Health endpoint без таймаутов на внутренние проверки → health сам зависает.
  • Одинаковая логика для live/ready → Kubernetes теряет возможность корректно управлять трафиком.
  • ---

    7) Как связать probes с наблюдаемостью и инцидентами

    Probes — не замена метрикам/логам/трейсам (это отдельная тема, уже разобранная в блоке observability). Но probes должны быть совместимы с вашими операционными сигналами:

  • При readiness=false логируйте событие (один раз на изменение состояния), чтобы было видно причину вывода из ротации.
  • Метрика вида “ready_state” (gauge 0/1) полезна для корреляции с ростом 5xx и p99.
  • request_id тут обычно не нужен (health — не пользовательский запрос), но лог-событие должно содержать минимум: event, pod, reason.
  • ---

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

    1) Опишите (1–2 фразы на пункт), что должно проверяться в:

  • liveness
  • readiness
  • startup
  • для сервиса, который грузит LLM на GPU ~90 секунд.

    2) У вас внешняя зависимость Postgres нужна только для endpoint /v1/jobs, а основной /v1/embeddings работает без неё. Должен ли readiness включать проверку Postgres? Обоснуйте.

    3) Придумайте 3 причины, почему liveness может падать у ML-serving под нагрузкой, и какая из них действительно требует рестарта, а какая — должна решаться деградацией/лимитами.

    4) Составьте минимальную схему health endpoint’ов (пути) и краткий контракт ответа (только смысл, не код).

    <details> <summary> Ответы </summary>

    1) Пример:

  • Liveness: «процесс отвечает и event loop не завис» — без проверки модели и внешних сервисов.
  • Readiness: «модель загружена, warmup выполнен, runtime готов принимать запросы, лимитер/очередь инициализированы».
  • Startup: «стартовые шаги (загрузка артефактов, инициализация GPU, первичный warmup) завершились»; допускается долгий период до успеха.
  • 2) Обычно нет: readiness должен отражать готовность основного пути обслуживания. Если Postgres нужен только для /v1/jobs, лучше:

  • либо держать /v1/jobs отдельным сервисом/деплоем,
  • либо проверять Postgres внутри самого use case /v1/jobs и отдавать контролируемую ошибку/деградацию для jobs,
  • а readiness оставить завязанным на основной инференс-путь. Иначе временная проблема Postgres выключит из ротации и embeddings тоже.

    3) Примеры причин:

  • Deadlock/зависание event loop (блокирующий код в async пути) — это кандидат на рестарт (liveness оправдан).
  • OOM killer/почти-OOM из-за неконтролируемой конкурентности — рестарт может временно помочь, но правильное решение: лимиты, backpressure, контроль батча/параллелизма.
  • Временная недоступность Redis/S3/Postgres — не причина для liveness; это должно отражаться в ошибках/метриках и (в зависимости от критичности) readiness или деградации.
  • 4) Пример:

  • /health/live: всегда быстрый ответ 200, если процесс способен обработать запрос.
  • /health/ready: 200, если модель загружена и прогрета; иначе 503.
  • /health/startup: 200 только после завершения стартовой последовательности (загрузка + warmup); иначе 503.
  • Контракт ответов: достаточно минимального тела (например, {"status":"ok"} или {"status":"not_ready","reason":"model_not_loaded"}), главное — стабильные коды и скорость ответа.

    </details>

    23. Тестирование: unit, integration, contract, load smoke tests

    Тестирование: unit, integration, contract, load smoke tests

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

    В прошлых статьях курса уже были разобраны: требования к production, контракт API (OpenAPI, backward compatibility), структура репозитория и quality gates, единая модель ошибок, конфигурация и наблюдаемость. Здесь — как превратить это в практическую стратегию тестов.

    1) Карта тестов: какой риск чем ловим

    Главная ошибка — пытаться закрыть всё интеграционными тестами или, наоборот, только unit’ами. Рабочий подход — пирамида с несколькими «верхними» защитными слоями.

    | Тип тестов | Что защищает | Что не должен делать | Типичный запуск | |---|---|---|---| | Unit | правила домена, use cases, маппинг ошибок/политик | не поднимать FastAPI, не трогать GPU/реальные внешние системы | каждый PR | | Integration | wiring контейнера, lifecycle (startup/warmup), реальная связка компонентов | не измерять throughput, не гонять большие модели | каждый PR или nightly | | Contract | стабильность API‑контракта: схемы, коды, error model | не проверять качество модели | каждый PR (блокирующий) | | Load smoke | “не разваливается под небольшой нагрузкой”, базовые p95/p99, backpressure | не «доказывать» финальную производительность | перед релизом / по расписанию |

    Важный принцип: каждый уровень тестов должен иметь чёткий бюджет времени. Если unit‑тесты стали «долгими» — их перестают запускать.

    2) Unit‑тесты: быстрые проверки ядра (domain/use cases)

    Unit‑уровень в ML‑serving — это прежде всего тестирование политик и оркестрации, а не «точности модели».

    Что обычно стоит тестировать unit’ами:

  • Проверка лимитов и политик
  • 1. превышение размера входа → доменная ошибка (InputTooLarge или ваш аналог) 2. несовместимые опции запроса → доменная ошибка 3. выбор профиля (fast/quality) и его ограничения
  • Backpressure решения на уровне use case
  • 1. очередь/лимитер отказал → доменная ошибка (Overloaded) 2. таймаут вычислений → доменная ошибка (Timeout), если это часть доменной модели
  • Кеш/дедуп логика как чистая оркестрация
  • 1. cache hit → runtime не вызывается 2. cache miss → runtime вызывается ровно один раз
  • Маппинг доменных ошибок в “причины” (без HTTP)
  • 1. error_code стабилен 2. details имеет ожидаемую структуру

    Практическая дисциплина:

  • Все внешние зависимости (runtime, Redis, Postgres, S3, метрики) в unit’ах — заглушки через порты (см. статью про Clean/Hexagonal).
  • Unit‑тест не должен зависеть от текущей модели/весов.
  • Мини‑паттерн: “табличные” тесты политик

    Для лимитов и правил деградации удобно делать табличные кейсы:

    Так вы фиксируете поведение политики как контракт, не смешивая его с HTTP.

    3) Contract‑тесты: защита API и error model

    Contract‑тесты отвечают на вопрос: “Клиенты не сломаются после этого PR?”

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

    Что именно проверять:

  • Коды ответа по сценариям
  • 1. успешный запрос → 200 (или 202 для job‑based) 2. превышение лимита → ваш ожидаемый код (часто 413) 3. перегруз/backpressure → ваш ожидаемый код (часто 429 или 503, но важно единообразие)
  • Единый формат ошибки
  • 1. наличие error_code, message, request_id 2. details структурирован и не содержит PII
  • Стабильность схем ответов
  • 1. обязательные поля ответа всегда присутствуют 2. неизвестные поля не появляются «случайно» (если вы выбрали строгий контракт)
  • Golden‑кейсы
  • 1. несколько эталонных запросов/ответов на каждый ключевой endpoint 2. отдельные golden для ошибок (validation, limit, overloaded)

    Важно: contract‑тест — это не «просто pytest на FastAPI». Это тест публичного контракта, который должен падать при любом breaking change.

    4) Integration‑тесты: проверка “собирается и живёт”

    Интеграционные тесты подтверждают, что сервис работает как система: app factory → DI контейнер → lifecycle → реальные адаптеры (частично).

    Что имеет смысл включать:

  • Проверка lifecycle
  • 1. startup проходит без ошибок 2. warmup выполнен (или установлен флаг готовности) 3. readiness сообщает “готов” только после инициализации
  • Проверка wiring зависимостей
  • 1. контейнер собирается с нужными портами 2. критичные настройки валидируются (fail fast)
  • Мини‑сквозняк по одному happy‑path
  • 1. HTTP запрос → use case → runtime (тестовый) → ответ

    Как не превратить integration в ад:

  • Используйте “лёгкий runtime” (фейковая модель/мок) или минимальную тестовую модель.
  • Не включайте тяжёлые внешние сервисы без необходимости.
  • Если поднимаете Postgres/Redis — делайте это только для тех тестов, где это реально важно (например, jobs/idempotency).
  • 5) Load smoke tests: минимальная нагрузка как предохранитель

    Load smoke — это не полноценное нагрузочное тестирование, а быстрый “дымовой” сценарий:

  • сервис под небольшой параллельностью не деградирует катастрофически
  • не происходит лавинообразных 5xx
  • backpressure работает (т.е. при перегрузе вы видите контролируемые 429/503, а не таймауты и падения)
  • Что измерять в load smoke (минимум)

  • p95/p99 latency на ключевом endpoint (server-side)
  • error rate с разбиением по error_code
  • доля overloaded/timeout при искусственном перегрузе (если вы делаете такой шаг)
  • стабильность по памяти (нет «утечки за 5 минут»)
  • Почему load smoke должен быть коротким

    Задача — поймать грубые регрессии (например, случайно убрали лимит, сломали батчер, создали клиента Redis на каждый запрос), а не “точно померить RPS”.

    Рабочий формат:

  • 2–5 минут стабильной нагрузки на фиксированном профиле
  • 30–60 секунд «пика» (burst), чтобы увидеть поведение очереди/лимитера
  • 6) Связь тестов с CI: какие “ворота” делать блокирующими

    Практичная политика для production‑serving:

  • Блокирующие на каждый PR:
  • 1. unit 2. contract 3. минимальный integration (smoke)
  • Неблокирующие (но регулярные):
  • 1. расширенные integration с внешними зависимостями 2. load smoke (или блокирующие только для релизных веток/тегов)

    Ключевая идея: в main нельзя сломать контракт и базовую работоспособность, но нельзя и “убить скорость разработки” слишком тяжёлыми тестами на каждый коммит.

    7) Типовые антипаттерны тестирования в ML‑serving

  • Один “гигантский” интеграционный тест вместо пирамиды → медленно, нестабильно, мало диагностической ценности.
  • Тестировать качество модели через HTTP как основную проверку → дорого и плохо локализует проблему.
  • Игнорировать contract‑тесты → breaking changes выявляются клиентами в проде.
  • Делать load smoke без фиксации входов/профилей → результаты не сравнимы между запусками.
  • Смешивать “ошибки клиента” и “ошибки сервера” в одну метрику/проверку → неправильные выводы о стабильности.
  • ---

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

    1) Составьте матрицу из 12 тест‑кейсов для одного endpoint инференса (например, /v1/embeddings): 6 unit, 4 contract, 2 integration. Для каждого кейса укажите, какой риск он покрывает.

    2) Придумайте 5 golden‑кейсов для contract‑тестов LLM генерации: 2 успешных, 3 ошибочных. Укажите, какие поля должны быть зафиксированы в golden‑ответе, а какие — нельзя фиксировать (и почему).

    3) Опишите сценарий load smoke на 5 минут для CV‑endpoint, который принимает изображения. Какие 4 метрики вы будете смотреть, и какие два “красных флага” заставят вас остановить релиз?

    4) Вы обнаружили: unit и contract проходят, но integration падает на старте в CI. Назовите 4 наиболее вероятные причины именно для ML‑serving (не общие “всё сломалось”), и по одному шагу диагностики для каждой.

    <details> <summary> Ответы </summary>

    1) Пример матрицы.

    Unit (6):

  • Вход пустой/после нормализации пустой → доменная ошибка “invalid input” (защита от мусора).
  • Вход слишком длинный → InputTooLarge(details: field/limit/got) (защита ресурсов).
  • Неверный профиль → доменная ошибка “validation/policy” (контроль режимов).
  • Cache hit → runtime не вызывается (проверка оркестрации и экономии).
  • Cache miss → runtime вызывается ровно один раз (без двойного вызова).
  • Лимитер/очередь отказали → Overloaded (backpressure на уровне use case).
  • Contract (4):

  • Успех: 200 + наличие request_id, model, result.
  • Невалидный body → 422 + error_code=validation_error + структурированный details.errors.
  • Payload слишком большой → 413 + error_code=input_too_large.
  • Перегруз → 429 (или ваша политика) + error_code=overloaded.
  • Integration (2):

  • Приложение стартует, проходит lifecycle, readiness становится “ok” (проверка wiring + warmup-флага).
  • Один сквозной запрос проходит через настоящий HTTP слой и тестовый runtime (проверка “система работает”).
  • 2) Пример golden для LLM.

    Успешные:

  • Короткий prompt, профиль fast.
  • Средний prompt, профиль quality (в рамках лимитов).
  • Ошибочные:

  • Слишком длинный prompt → input_too_large.
  • Неверная комбинация параметров (policy) → policy_violation (или ваш код).
  • Перегруз (искусственно включённый лимитер) → overloaded.
  • Фиксировать в golden‑ответе:

  • наличие и типы полей (структура JSON)
  • error_code и форму details
  • коды HTTP
  • Не фиксировать:

  • полный сгенерированный текст (может меняться при обновлении модели/seed/температуры)
  • точные тайминги (нестабильны по окружениям)
  • 3) Пример load smoke для CV.

    Метрики:

  • p95/p99 latency
  • error rate (и отдельно доля 5xx)
  • доля overloaded/timeout
  • память процесса (RSS) и/или GPU memory (если есть)
  • Красные флаги:

  • p99 вырос кратно относительно baseline или “упёрся” в таймауты.
  • пошли 5xx или OOM/рестарты (даже если RPS небольшой).
  • 4) Почему integration падает на старте (ML‑специфика) и диагностика.

  • Не хватает артефактов/неправильный путь к модели в CI: проверить, что test‑конфиг указывает на доступные фикстуры/артефакты.
  • Ошибка валидации settings (fail fast): посмотреть лог старта и конкретное поле, которое не проходит тип/диапазон.
  • Warmup использует “слишком тяжёлый” вход и падает (OOM/timeout): временно упростить warmup‑набор для тестового окружения и проверить, где именно падает.
  • Поднятые зависимости (Redis/Postgres) не доступны в CI сети/контейнере: проверить health этих сервисов и адреса/порты, а также таймауты подключения.
  • </details>

    24. Security scanning: SAST, dependency audit, secrets detection

    Security scanning: SAST, dependency audit, secrets detection

    Security scanning в production ML‑serving — это «страховочные сетки» в CI/CD, которые ловят проблемы раньше продакшна: уязвимые зависимости, опасные паттерны кода, утечки секретов. В отличие от runtime‑мер (JWT/mTLS/rate limits из статьи про безопасность API), scanning работает на уровне репозитория и артефактов сборки.

    Ниже — практическая дисциплина: что именно сканировать, какие классы проблем ловит каждый вид сканера, где его поставить в пайплайне, и как не превратить security‑проверки в «шум, который все игнорируют».

    ---

    1) Карта угроз именно для ML‑serving репозитория

    Для ML‑сервиса типовые источники уязвимостей в кодовой базе и сборке:

  • Ошибки в приложении (инъекции, небезопасная работа с файлами, SSRF, небезопасные десериализации, неправильные права доступа).
  • Уязвимые зависимости (Python‑пакеты, системные библиотеки в Docker‑образе, транзитивные зависимости).
  • Утечки секретов (API keys, токены к S3/Redis/LLM‑провайдерам, приватные ключи) в git‑истории или в образе.
  • Security scanning закрывает эти три направления тремя классами проверок:

    | Категория | Что сканируем | Что ловим | Типичный артефакт | |---|---|---|---| | SAST | исходный код | небезопасные паттерны | report по файлам/строкам | | Dependency audit | lockfile/установленные пакеты/образ | известные CVE, иногда лицензии | список уязвимостей + severity | | Secrets detection | git diff/история/артефакты | токены/ключи в явном виде | найденные секреты + местоположение |

    ---

    2) SAST (Static Application Security Testing)

    2.1. Что SAST реально умеет

    SAST анализирует исходники без запуска кода и ищет «опасные конструкции». Для FastAPI/ML‑serving это особенно полезно для:

  • Инъекций и небезопасного формирования команд (например, вызовы shell).
  • Небезопасной работы с файлами/путями (path traversal, чтение произвольных файлов по пользовательскому пути).
  • SSRF‑паттернов (когда сервис скачивает URL, переданный пользователем).
  • Небезопасных десериализаций и eval/exec.
  • Ошибок криптографии «по умолчанию» (слабые алгоритмы/режимы).
  • Важно: SAST находит паттерны, а не гарантированные эксплойты. В production дисциплина строится вокруг снижения числа ложных срабатываний и выделения того, что действительно блокирует релиз.

    2.2. Ограничения SAST (чтобы не ждать от него невозможного)

  • Сложные бизнес‑контексты SAST часто не понимает (например, что вход уже валидирован).
  • Динамические пути исполнения (особенно в Python) могут давать ложные positives.
  • SAST не решает проблему уязвимых зависимостей — это отдельный класс.
  • 2.3. Как встроить SAST в процесс

    Практика для курса и production‑репозитория:

  • Запускать SAST на каждый PR (как quality gate наряду с тестами), но блокировать только по заранее определённым правилам.
  • Разделить результаты по severity:
  • 1. high/critical — блокирующие. 2. medium — не блокируют, но требуют задачи/тикета. 3. low — обычно только отчёт.
  • Зафиксировать «политику исключений»:
  • 1. исключение должно быть привязано к конкретному месту в коде; 2. иметь причину (почему это не уязвимость) и срок пересмотра.

    2.4. Что особенно важно в ML‑serving

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

  • Обработки файлов и временных директорий.
  • Внешних загрузок (fetch по URL).
  • Использования сторонних парсеров (изображения, архивы, документы).
  • ---

    3) Dependency audit (уязвимости зависимостей)

    3.1. Почему это «must have» именно для ML

    ML‑стек тяжёлый и транзитивно сложный: один пакет тащит десятки зависимостей. В результате:

  • уязвимости появляются «глубоко в дереве»;
  • обновления могут ломать runtime/производительность;
  • часто есть смесь Python‑пакетов и системных библиотек (в Docker‑образе).
  • Dependency audit нужен, чтобы видеть риск до деплоя и принимать управляемые решения (обновить, зафиксировать, заменить, временно подавить).

    3.2. Что именно сканировать

    Минимально достаточно сканировать два слоя (они дополняют друг друга):

  • Python‑зависимости по lockfile (или по «frozen» установленному списку).
  • Контейнерный образ (OS packages + бинарные зависимости).
  • Почему оба:

  • lockfile покрывает Python‑граф и даёт воспроизводимость (см. статью про pinned deps и reproducible builds).
  • образ включает системные библиотеки, которые Python‑сканер может не увидеть.
  • 3.3. Ключевые политики, без которых audit превращается в шум

  • Точка отсечения по severity: что блокирует релиз.
  • Окно исправления:
  • 1. critical/high — исправить в короткий срок; 2. medium — планово; 3. low — по возможности.
  • Фиксация решений: если вы не обновляете пакет (например, конфликт с CUDA/runtime), это должно быть явным решением с ревизией.
  • 3.4. Практические ловушки

  • «Обновим всё» — часто ломает ML‑runtime. Обновления лучше делать пакетно и с минимальным smoke/интеграционным прогоном.
  • Уязвимость без эксплойта в вашем контексте всё равно может быть риск‑фактором (особенно если это библиотека парсинга входных данных).
  • Лицензии: иногда dependency audit включает license check. Для коммерческого продукта это может быть столь же важно, как CVE.
  • ---

    4) Secrets detection (утечки секретов)

    4.1. Что считается утечкой

    Утечка — это не только «ключ в файле .env». Типовые случаи:

  • Секрет закоммичен в код, конфиг или тестовую фикстуру.
  • Секрет попал в CI‑логи (например, печать env/stacktrace).
  • Секрет оказался в Docker‑образе (копирование локальных файлов или неправильный build context).
  • Секрет находится в git‑истории (даже если вы его удалили в последнем коммите).
  • 4.2. Где сканировать

    Нужны минимум два контура:

  • Pre‑commit / локально (чтобы ловить до push).
  • CI на PR (чтобы ловить независимо от дисциплины разработчика).
  • Дополнительно полезно:

  • периодическое сканирование всей истории репозитория (особенно после миграций/импортов).
  • 4.3. Реакция на найденный секрет (обязательная процедура)

    Если детектор нашёл секрет, правильная реакция — это не только «удалить строку»:

  • Считайте секрет скомпрометированным.
  • Ротируйте (выпустите новый ключ/токен).
  • Отзовите старый (если возможно).
  • Проверьте логи/доступы: был ли он использован.
  • Уберите из истории (если секрет реально успел попасть в git), иначе он останется доступным.
  • Отдельный важный момент для ML‑serving: часто секреты дают доступ к S3/registry/LLM‑провайдеру и приводят к denial‑of‑wallet.

    ---

    5) Как разместить сканирование в CI/CD (без «вечного красного пайплайна»)

    Ниже — минимальная структура стадий. Это не код пайплайна, а логика порядка.

    Практические правила, чтобы это работало:

  • Не смешивать ответственность: SAST не должен «маскировать» проблему зависимостей и наоборот.
  • Блокирующие условия должны быть редкими и объяснимыми (иначе разработчики начнут обходить проверки).
  • Baseline‑подход для legacy:
  • 1. фиксируете текущие известные проблемы как baseline; 2. запрещаете появление новых; 3. постепенно уменьшаете baseline.

    ---

    6) Мини‑чеклист «production‑готового» security scanning

  • SAST включён на PR и имеет явные блокирующие правила по severity.
  • Dependency audit проверяет и Python‑граф (lockfile), и контейнерный образ.
  • Secrets detection работает локально и в CI; есть процедура ротации/отзыва.
  • Исключения оформляются как управляемые решения (срок пересмотра, причина).
  • Результаты сканирования привязаны к артефакту релиза (чтобы понимать, что именно было проверено).
  • ---

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

    1) Составьте политику «что блокирует merge в main» для трёх классов проверок: SAST, dependency audit, secrets detection. Укажите хотя бы по 2 правила для каждого класса.

    2) У вас нашли секрет в PR (токен к S3), но он «в тестовом окружении». Опишите пошагово, что делаете в первые 30 минут и что делаете в течение дня.

    3) Почему dependency audit по lockfile и audit по контейнерному образу не взаимозаменяемы? Дайте 3 причины.

    4) Придумайте стратегию baselining для проекта, где уже есть 25 уязвимостей medium и 3 high. Как сделать так, чтобы команда не «парализовалась», но риск снижался?

    5) Назовите 6 примеров «ложных срабатываний» SAST или secrets detection в ML‑serving и как вы документируете исключение так, чтобы не потерять контроль.

    <details> <summary> Ответы </summary>

    1) Пример политики блокировок.

    SAST:

  • Блокировать PR при critical/high (например, обнаружен path traversal или небезопасная десериализация).
  • Блокировать PR, если появилось новое предупреждение в «критичных» директориях (например, inbound HTTP adapters или обработка файлов) даже при medium, если оно связано с пользовательским вводом.
  • Dependency audit:

  • Блокировать PR, если в lockfile/образе есть critical/high с доступным фикс‑апдейтом.
  • Блокировать PR, если добавлена новая зависимость, которая приносит high уязвимость (даже если она была в baseline для других пакетов).
  • Secrets detection:

  • Блокировать PR при любой находке, классифицированной как «реальный секрет» (ключ/токен/приватный ключ).
  • Блокировать PR, если секрет найден в файлах сборки образа (Docker build context), даже если он «не используется в коде».
  • 2) Действия при утечке токена S3.

    Первые 30 минут:

  • Остановить распространение: закрыть/заблокировать PR или сделать его приватным по процедуре команды.
  • Считать токен скомпрометированным: отозвать или ротировать в secret manager/K8s Secret.
  • Проверить права токена (read/write), бакеты/префиксы, куда он имел доступ.
  • В течение дня:

  • Проверить, не попал ли секрет в git‑историю (а не только в текущий diff). Если попал — очистка истории по принятой процедуре.
  • Провести аудит доступа/логов у провайдера (если возможно): были ли обращения с этого токена.
  • Добавить правило/паттерн в secrets detector (если это был новый формат), чтобы ловить раньше.
  • 3) Почему lockfile audit и image audit дополняют друг друга.

  • Lockfile покрывает Python‑граф, но не гарантирует видимость системных библиотек внутри образа.
  • Образ содержит OS‑пакеты и бинарные зависимости (libjpeg, openssl, glibc и т.п.), которые могут иметь CVE независимо от Python.
  • Итоговый образ может включать то, чего нет в lockfile (например, утилиты, установленные через пакетный менеджер ОС, или зависимости, пришедшие через wheels).
  • 4) Пример baselining.

  • Немедленно: сделать high блокирующими (включая 3 текущих). Для них — план устранения с приоритетом.
  • Для medium: зафиксировать baseline «как есть» (25) и запретить появление новых medium.
  • Ввести SLA на уменьшение baseline: например, минус 5 medium в спринт или по 1–2 в неделю.
  • Для сложных обновлений ML‑стека: разрешить исключение только с документированным риском и сроком пересмотра.
  • 5) Примеры ложных срабатываний и как оформлять исключение.

    Примеры:

  • Тестовые строки, похожие на токены (например, "AKIA..." как фиктивный пример).
  • Случайные base64‑фрагменты в тестовых данных, похожие на ключ.
  • SAST ругается на "подозрительное" формирование пути, но путь формируется только из констант.
  • SAST отмечает SSRF, но URL берётся только из allowlist конфигурации.
  • Детектор секретов ловит публичный ключ (не приватный) и это допустимо по политике.
  • CVE в зависимости, которая реально не входит в runtime‑образ (например, только dev‑группа), но сканер смотрит на полный граф.
  • Оформление исключения:

  • Привязка к точному месту (файл/строка/пакет).
  • Короткое объяснение контекста (почему это не уязвимость/не секрет).
  • Срок пересмотра (например, 30–90 дней) и владелец решения.
  • При необходимости — компенсирующая мера (например, дополнительная валидация входа или ограничение allowlist).
  • </details>

    25. Dockerfile production: multi-stage, non-root, slim images

    Dockerfile production: multi-stage, non-root, slim images

    Dockerfile для production ML-serving (FastAPI + high load) — это не “как собрать, чтобы работало”, а как собрать воспроизводимо, безопасно и предсказуемо по старту/размеру/поведению. В предыдущих статьях уже были разобраны pinned deps и воспроизводимость, конфигурация/секреты, security scanning. Здесь фокус — как эти принципы приземляются в Dockerfile: multi-stage сборка, non-root runtime, slim-образы и практические ловушки ML-зависимостей.

    ---

    1) Цели production Docker-образа для ML-serving

  • Минимальная поверхность атаки: меньше пакетов ОС и утилит в runtime-слое.
  • Воспроизводимость: фиксированный базовый образ + установка зависимостей строго по lockfile (подход описан в статье про toolchain).
  • Предсказуемый cold start: разделение “сборка/установка” и “запуск”, корректные права, отсутствие «ленивых установок».
  • Управляемая диагностика: логирование в stdout/stderr, отсутствие интерактивных костылей.
  • Разделение CPU/GPU вариантов: один Dockerfile-паттерн, но разные базовые образы и зависимости.
  • ---

    2) Multi-stage: базовый паттерн и зачем он нужен

    Multi-stage позволяет вынести всё тяжёлое (компиляторы, build-tools, скачивание зависимостей) в builder-слой и оставить runtime максимально “тонким”.

    2.1. Что обычно кладут в builder-stage

  • Системные пакеты для сборки (если какие-то зависимости собираются из исходников).
  • Установку Python-зависимостей строго по lockfile.
  • (Опционально) сборку wheelhouse (набор .whl), чтобы ускорить и стабилизировать установку.
  • 2.2. Что оставляют в runtime-stage

  • Только необходимые runtime библиотеки ОС (например, libstdc++, libgomp, libjpeg — зависит от стека).
  • Готовое окружение Python (venv/сайт-пакеты) + код приложения.
  • Non-root пользователь.
  • ---

    3) Slim images: как уменьшать образ без “сломалось в проде”

    Slim — это про контроль того, что реально нужно сервису.

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

  • Фиксируйте базовый образ (не latest). Это часть воспроизводимости.
  • Минимизируйте apt-зависимости:
  • 1. ставьте только нужные runtime-либы, 2. не ставьте build-essential в runtime, 3. очищайте apt-кэш.
  • Не копируйте лишнее: используйте .dockerignore (особенно для tests/, docs/, локальных артефактов, кешей, .git).
  • Не храните секреты в образе (см. статью про конфигурацию/секреты): секреты — только через окружение/Secret mounts.
  • 3.2. Типовая ловушка ML

    Некоторые пакеты “вдруг” требуют системные библиотеки в runtime. Если вы агрессивно “ужали” образ и забыли нужную lib — получите ImportError уже в проде.

    Практика:

  • Builder может быть “толстым”.
  • Runtime должен содержать минимальный набор библиотек, но этот набор надо зафиксировать и проверять интеграционным smoke-тестом в CI.
  • ---

    4) Non-root: обязательный минимум для production

    Запуск под root внутри контейнера — частая и ненужная дырка. В ML-serving это особенно важно, потому что сервис принимает внешние данные (текст/изображения), и ошибки в обработчиках/библиотеках могут стать вектором атаки.

    4.1. Что нужно сделать

  • Создать пользователя и группу (например, app).
  • Дать права на рабочие директории:
  • 1. директория приложения, 2. директория для временных файлов (если нужна), 3. директория для модельного кэша/артефактов, если вы тянете их на старте.
  • Переключиться на пользователя USER app в runtime stage.
  • 4.2. Важно для Kubernetes

  • Если вы используете readOnlyRootFilesystem: true, вам заранее нужна writable-директория (обычно emptyDir), и приложение должно писать только туда.
  • Если вы пишете кеш артефактов модели на диск, продумайте путь и права (и не пишите в корень файловой системы контейнера).
  • ---

    5) Референс-шаблон Dockerfile (CPU): multi-stage + venv

    Ниже — шаблон, не догма. Он показывает ключевые решения: слой зависимостей кэшируется отдельно, runtime без build-tools, non-root.

    Комментарий:

  • Здесь показан принцип “сначала lockfile — потом код” для кэширования.
  • Не воспринимайте requirements.txt как обязательный: в production чаще ставят зависимости напрямую из lockfile выбранным инструментом в “frozen” режиме. Детали — в статье про toolchain.
  • ---

    6) GPU образы: отдельная реальность

    Для GPU вы почти всегда:

  • Используете CUDA-base образ (а не python:slim).
  • Разводите CPU и GPU образы как разные targets или разные Dockerfile.
  • Ключевой принцип: не пытайтесь одним образом покрыть CPU и GPU условной логикой — это регулярно приводит к конфликтам зависимостей и неожиданным падениям.

    Практика:

  • Делайте runtime-cpu и runtime-gpu как разные stage/targets.
  • Пины версий CUDA/cuDNN и совместимых ML-пакетов — часть воспроизводимости.
  • ---

    7) Ускорение сборки: кэширование слоёв и wheelhouse

    В high-load проекте сборки будут частыми (CI/CD), а ML-зависимости — тяжёлыми. Основные рычаги:

  • Слой зависимостей кэшируется: lockfile отдельно от кода.
  • Wheel strategy:
  • 1. в builder скачивать/собирать колёса, 2. в runtime устанавливать только из wheelhouse.

    Это уменьшает шанс “внезапно начали собирать из исходников” и ускоряет CI.

    ---

    8) Runtime-практика: старт, сигналы и корректное завершение

    Dockerfile влияет на эксплуатацию.

  • Приложение должно корректно обрабатывать SIGTERM (Kubernetes shutdown). Практически это означает:
  • 1. не игнорировать сигналы, 2. корректно закрывать ресурсы в lifespan (см. статью про lifecycle).
  • Логи должны идти в stdout/stderr (см. статью про JSON-логи).
  • Health endpoints не “встраиваются” в Dockerfile, но образ должен содержать только то, что нужно для их корректной работы.
  • ---

    9) Частые антипаттерны Dockerfile для ML-serving

  • Установка зависимостей в runtime stage вместе с build-tools.
  • pip install -U ... без lockfile (дрейф окружения).
  • Копирование всего репозитория без .dockerignore.
  • Запуск под root.
  • Хранение секретов в образе.
  • Один универсальный образ “и CPU, и GPU”.
  • ---

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

    1) Опишите, какие 4 слоя (по смыслу) вы хотите видеть в Dockerfile для ML-serving, чтобы ускорить сборку при частых изменениях кода.

    2) Составьте чек-лист из 7 пунктов “non-root готовности” контейнера для Kubernetes (включая writable директории).

    3) У вас CV endpoint декодирует изображения. Назовите 5 причин, почему “slim образ” может сломать сервис в runtime, и как это диагностировать на CI.

    4) Предложите стратегию разделения CPU и GPU образов:

  • как назвать targets,
  • что будет общим,
  • что будет различаться.
  • <details> <summary> Ответы </summary>

    1) Пример слоёв:

  • Базовый образ + системные зависимости (минимум).
  • Слой Python-зависимостей (установка строго по lockfile) — кэшируется и редко меняется.
  • Слой приложения (код) — меняется часто.
  • Слой конфигурации запуска (CMD/entrypoint, пользователь, переменные среды по умолчанию).
  • 2) Пример чек-листа non-root:

  • Создан пользователь и группа (например, app).
  • USER app установлен в финальном stage.
  • WORKDIR принадлежит app.
  • Все директории, куда приложение пишет (временные файлы, кеш артефактов, логи если есть), доступны на запись.
  • Нет записи в системные директории (/, /root, /usr) во время работы.
  • (Для K8s) совместимо с readOnlyRootFilesystem: true при наличии emptyDir и корректного пути.
  • Нет секретов в файлах образа; секреты приходят извне.
  • 3) Причины поломки slim:

  • Не хватает системной библиотеки для декодирования изображений (JPEG/PNG), проявляется как ImportError или падение при decode.
  • Не хватает libstdc++/libgcc для бинарных wheels.
  • Отсутствует CA bundle (ca-certificates), ломаются загрузки артефактов по HTTPS.
  • Разные ABI/архитектуры, и wheel не подходит (особенно на aarch64).
  • Слишком агрессивно удалили runtime-либы вместе с build stage.
  • Диагностика в CI: интеграционный smoke-тест внутри собранного runtime-образа + проверка старта приложения (и, если нужно, минимальный прогон декодирования на фиксированной fixture).

    4) Стратегия CPU/GPU:

  • Targets: runtime-cpu, runtime-gpu (и, при необходимости, builder-cpu, builder-gpu).
  • Общее: структура приложения, non-root политика, entrypoint, конфиги, health endpoints, уровни логирования.
  • Отличается: базовый образ (python-slim vs CUDA-base), набор системных библиотек, набор ML-пакетов и их версии (совместимые с CUDA), иногда параметры запуска (например, 1 воркер на GPU).
  • </details>

    26. Docker Compose для локальной разработки и интеграционных тестов

    Docker Compose для локальной разработки и интеграционных тестов

    Docker Compose в production‑курсе нужен не как «мини‑Kubernetes», а как быстрый, воспроизводимый локальный стенд: поднять FastAPI‑сервис вместе с Redis/Postgres/S3‑эмулятором, прогнать интеграционные тесты, проверить миграции и базовые healthchecks.

    Ранее в курсе уже обсуждались воспроизводимые сборки, конфигурация/секреты, health/readiness и тестовые контуры. Здесь сфокусируемся на том, как собрать локальную среду через Compose так, чтобы она:

  • повторяла ключевые зависимости продакшна;
  • была удобна для разработки (hot reload, volume mounts);
  • была пригодна для интеграционных тестов (детерминизм, «поднять → проверить → снести»).
  • ---

    1) Два режима Compose: dev и integration

    Практика: держите один базовый compose.yaml и разделяйте режимы:

  • dev: разработчик запускает сервис с hot reload, сохраняет данные в volumes.
  • integration: тестовый запуск в чистом окружении, данные можно не сохранять, важны healthchecks и предсказуемый старт.
  • Самые рабочие механизмы Compose для этого:

  • profiles (включать/выключать сервисы по сценарию);
  • отдельный файл compose.override.yaml для локальных удобств;
  • разные команды запуска и разные переменные окружения.
  • ---

    2) Базовая карта сервисов для ML‑serving стенда

    Минимальный стенд для типичного production‑serving (адаптируйте под ваш проект):

  • app — FastAPI сервис.
  • redis — кеш / очередь (если используете job‑based).
  • postgres — метаданные (версии моделей, jobs), если сервис обращается к БД.
  • migrations — одноразовый контейнер, который применяет миграции (удобно для интеграционных тестов).
  • (опционально) s3‑эмулятор — если в локальном режиме нужно тестировать загрузку артефактов.
  • Важно: Compose‑стенд не обязан копировать все прод‑компоненты. Он обязан покрывать границы интеграции, которые реально ломаются: сеть, переменные окружения, миграции, старт/готовность.

    ---

    3) Референс compose.yaml (скелет)

    Ниже — пример структуры (упрощённо). Идея: app зависит от Redis/Postgres, а миграции выполняются отдельной одноразовой задачей.

    Ключевые моменты:

  • depends_on.condition: service_healthy помогает избежать гонок старта.
  • migrations — отдельный сервис для тестового сценария, чтобы не «зашивать миграции» внутрь app.
  • profiles дают две траектории: dev и integration.
  • ---

    4) compose.override.yaml: удобства разработки, которые нельзя тащить в тесты

    compose.override.yaml Compose подхватывает автоматически при локальном запуске (если файл рядом). Это отличное место для:

  • volume‑mount кода (./app:/app/app) и команды с reload;
  • проброса дополнительных портов для отладки;
  • включения debug‑логов локально.
  • Пример:

    Почему это важно: интеграционные тесты должны запускаться без dev‑магии. Поэтому override удобно иметь только для локальной разработки.

    ---

    5) Healthchecks и «готовность стенда»

    В прод‑логике вы разделяете live/ready/startup. В Compose полезно использовать readiness‑эндпойнт как критерий: тесты стартуют только когда сервис готов обслуживать запросы.

    Практика для интеграционных тестов:

  • postgres и redis — healthcheck на уровне контейнера.
  • app — healthcheck на /health/ready.
  • тестовый раннер ждёт app healthy (или вы делаете отдельную wait‑команду).
  • Важно: healthcheck должен быть быстрым и не выполнять реальный инференс на каждый вызов (логику readiness вы уже проектировали ранее).

    ---

    6) Сеть, порты и «не светить лишнее»

    Compose создаёт общую сеть для сервисов; внутри неё используйте имена сервисов (postgres, redis) как host.

    Рекомендации:

  • Порты наружу (ports) пробрасывайте только в dev и только то, что нужно для отладки.
  • В integration‑режиме порты наружу часто не нужны: тесты могут жить в отдельном контейнере в той же сети.
  • Если хотите строгую изоляцию, вынесите ports: в compose.override.yaml (тогда CI не «случайно» экспонирует БД наружу).

    ---

    7) Томá (volumes): сохранять или не сохранять

    Для dev обычно нужны persistent volumes (Postgres, кеши артефактов), чтобы не терять состояние.

    Для integration чаще полезнее эпhemeral‑поведение:

  • не использовать volume или использовать временный volume;
  • после тестов выполнять cleanup.
  • Полезные команды:

    ---

    8) Интеграционные тесты как отдельный контейнер (тест‑раннер)

    Для интеграций удобно запускать тесты в контейнере, а не на хосте:

  • одинаковые зависимости/версии Python;
  • доступ к сервисам по внутренней сети Compose;
  • меньше «оно работает у меня».
  • Паттерн:

  • добавить сервис tests, который зависит от app (healthy);
  • tests запускает pytest -m integration (или аналог).
  • При этом:

  • тесты должны использовать тестовые настройки (APP_ENV=integration, отдельные лимиты, отключение тяжёлых прогревов);
  • тесты должны валидировать контракт (формат ошибок, базовые ответы) без привязки к реальным весам модели.
  • ---

    9) GPU и Compose: практическая оговорка

    Если у вас GPU runtime, локально можно держать отдельный compose‑профиль (например, gpu) и отдельный target сборки образа (runtime-gpu).

    Рекомендации:

  • CPU‑стенд используйте как «дефолт для тестов» (быстрее, меньше требований к окружению).
  • GPU‑стенд — для ручной проверки производительности/совместимости.
  • Не пытайтесь смешать CPU и GPU в одном и том же сервисе условной логикой: лучше разные профили/targets.

    ---

    10) Частые ошибки Compose‑стендов (и почему это больно)

  • Запуск миграций внутри app без контроля порядка — флапающий старт и нестабильные тесты.
  • Полагаться только на depends_on без healthchecks — гонки, которые «иногда» воспроизводятся.
  • Хранить секреты в compose.yaml — утечки в репозиторий; используйте env‑инъекцию/локальные .env (для dev) и секреты CI.
  • Тянуть артефакты модели при каждом старте integration‑стенда — тесты становятся медленными и неустойчивыми.
  • Использовать один и тот же volume в разных сценариях — интеграционные тесты начинают зависеть от «грязного» состояния.
  • ---

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

    1) Спроектируйте список сервисов для Compose‑стенда вашего ML‑serving проекта (минимум 4). Для каждого напишите: зачем он нужен именно локально/в интеграционных тестах.

    2) Объясните, где вы будете хранить:

  • persistent данные dev (что именно и почему);
  • ephemeral данные integration (как гарантируете чистоту прогонов).
  • 3) Придумайте схему profiles для трёх сценариев: dev, integration, gpu.

    4) Опишите, какие 3 healthchecks вы добавите, и какие 2 ошибки вы хотите поймать ими (не тестами).

    5) Опишите минимальный сценарий «поднять стенд → прогнать тесты → снести» одной командной последовательностью (без конкретного инструмента CI).

    <details> <summary> Ответы </summary>

    1) Пример набора сервисов:

  • app: сам FastAPI serving.
  • redis: кеш/дедуп/очередь для job‑based.
  • postgres: метаданные (версии моделей, jobs), проверка миграций.
  • migrations: одноразовый контейнер для alembic upgrade head (в integration).
  • (опционально) s3-emulator: только если локально реально тестируете загрузку артефактов.
  • 2) Хранение данных:

  • Dev persistent:
  • 1) pg_data volume для Postgres (чтобы не терять состояние и не ждать пересоздания), 2) (опционально) volume для локального кеша артефактов.
  • Integration ephemeral:
  • 1) не использовать persistent volume (или использовать отдельный), 2) всегда завершать прогон docker compose down -v, 3) не полагаться на состояние от предыдущего запуска.

    3) Пример profiles:

  • dev: app + postgres + redis, порты наружу, override для reload.
  • integration: app + postgres + redis + migrations + tests, без наружных портов.
  • gpu: app-gpu + (опционально) redis/postgres, отдельный build target.
  • 4) Пример healthchecks:

  • postgres: pg_isready.
  • redis: redis-cli ping.
  • app: запрос к /health/ready.
  • Ошибки, которые ловим:

  • Гонка старта (app поднялся, но зависимости ещё не готовы).
  • Сервис стартовал, но runtime не готов (readiness не проходит).
  • 5) Пример последовательности:

  • docker compose --profile integration up -d --build
  • дождаться app healthy (либо через docker compose ps, либо через отдельную wait‑команду)
  • запустить тестовый контейнер/команду (например, docker compose run --rm tests)
  • docker compose down -v
  • </details>

    27. ASGI сервер: Uvicorn/Gunicorn, workers, timeouts, keep-alive

    ASGI сервер: Uvicorn/Gunicorn, workers, timeouts, keep-alive

    ASGI‑сервер — это «двигатель», который принимает соединения, управляет воркерами (процессами/потоками), таймаутами и тем, как ваше FastAPI‑приложение живёт под нагрузкой. В ML‑serving это особенно важно: инференс может быть CPU/GPU‑тяжёлым, с хвостовыми задержками, а неправильные настройки сервера превращают любые оптимизации модели в бессмысленные.

    > Мы уже обсуждали разделение ответственности (API слой vs runtime), backpressure, healthchecks и метрики. Здесь фокус только на том, как правильно запускать ASGI и какие параметры реально влияют на устойчивость.

    ---

    1) Uvicorn vs Gunicorn: кто за что отвечает

    Uvicorn

    Uvicorn — ASGI‑сервер, который умеет:

  • принимать HTTP соединения;
  • управлять event loop;
  • запускать приложение;
  • иметь несколько воркеров через --workers.
  • Когда его достаточно:

  • простой деплой;
  • вы явно управляете количеством процессов;
  • вам не нужны «фишки» менеджера процессов (перезапуски по лимитам, hooks и т.п.).
  • Gunicorn + UvicornWorker

    Gunicorn — это process manager. Он сам не ASGI, но умеет:

  • держать master‑процесс;
  • форкать воркеры;
  • перезапускать их по правилам;
  • управлять graceful shutdown;
  • ограничивать «жизнь» воркеров (полезно при утечках памяти/фрагментации).
  • Для production под high load Gunicorn часто удобнее как «надёжный контроллер», а UvicornWorker — как ASGI‑исполнитель внутри воркера.

    Практический вывод:

  • Нужно меньше операционных сюрпризов и проще конфиг — берите Uvicorn.
  • Нужно управляемое поведение воркеров (recycle, более строгая дисциплина shutdown) — берите Gunicorn + UvicornWorker.
  • ---

    2) Workers: процессы, потоки и модель конкурентности

    2.1. Главная мысль

    Количество воркеров — это не «больше = быстрее». Это настройка того, сколько независимых процессов будут конкурировать за CPU/RAM и (если есть) за GPU.

    Важное разделение:

  • HTTP‑конкурентность: сколько соединений/запросов вы держите одновременно.
  • Конкурентность инференса: сколько запросов реально можно исполнять параллельно без деградации p99 и без OOM.
  • Если вы уже внедряли limiter/очередь рядом с runtime (см. материал про backpressure), то ASGI‑воркеры — это внешний «контур» параллелизма. Он не должен противоречить внутренним лимитам.

    2.2. Процессы (workers)

    Процесс = отдельная память, отдельная копия модели (если модель грузится в процесс).

    Это даёт:

  • изоляцию (падение воркера не валит всех);
  • обход GIL для CPU‑работы;
  • предсказуемость при утечках (воркер можно перезапустить).
  • Но стоит дорого:

  • память умножается на число воркеров;
  • для GPU почти всегда опасно: несколько процессов могут конкурировать за VRAM.
  • Практика для ML:

  • CPU‑inference: часто 2–N воркеров на pod (зависит от CPU и модели).
  • GPU‑inference: часто 1 воркер на GPU (а дальше уже limiter/batching внутри процесса).
  • 2.3. Потоки и threadpool

    В ASGI мире вы можете иметь async‑handlers, но любая CPU‑тяжёлая операция (токенизация, декодирование картинки, часть постпроцессинга) часто уходит в threadpool.

    Риск: «разогнать» потоков слишком много и получить рост p99 из‑за планировщика ОС и конкуренции за кеши CPU.

    Полезное правило:

  • масштабирующий рычаг №1 — процессы;
  • потоки — для точечного переноса блокирующих операций, но с контролем.
  • 2.4. Как выбирать число воркеров (практичная эвристика)

  • Если сервис CPU‑bound и модель/препроцессинг реально грузят CPU: начните с числа воркеров около числа CPU‑ядер (или чуть меньше) и меряйте p95/p99.
  • Если сервис упирается в GPU: начните с 1 воркера на GPU.
  • Если сервис упирается в память: уменьшайте воркеры, иначе получите частые OOM/restart.
  • Визуально (упрощённо):

    ---

    3) Timeouts: где они бывают и почему «одного таймаута» недостаточно

    Таймауты в production — это цепочка. Срыв в одном месте провоцирует ретраи и усиление нагрузки.

    3.1. Виды таймаутов, которые нужно различать

  • Timeout на уровне ingress / LB: сколько времени балансировщик ждёт ответа.
  • Timeout на уровне клиента: сколько клиент ждёт до отмены.
  • Timeout на уровне ASGI сервера: политики keep‑alive, graceful shutdown и иногда request timeouts (в зависимости от связки).
  • Timeout на уровне вашего runtime/use case: сколько вы даёте на очередь/батчинг/инференс (это вы уже проектировали в политике деградации).
  • Ключевое правило: серверные таймауты не должны «молчаливо зависать». Если инференс не успевает — лучше контролируемая ошибка (как обсуждалось в модели ошибок), чем подвешенный воркер.

    3.2. Gunicorn timeout vs реальность async

    У Gunicorn есть настройка timeout (убить воркер, если тот «не отвечает»). Для ASGI это тонко:

  • async‑воркер может держать соединение долго (streaming, долгий generate, job‑enqueue);
  • если вы поставите слишком агрессивный timeout, вы начнёте убивать воркеры во время легитимных долгих запросов.
  • Практика:

  • делайте долгие операции либо streaming‑режимом (и настраивайте цепочку таймаутов), либо job‑based;
  • timeout в Gunicorn используйте как защиту от реальных зависаний/дедлоков, а не как «контроль инференса».
  • 3.3. Graceful shutdown и drain

    При деплое/скейле Kubernetes посылает SIGTERM. Вам нужно время, чтобы:

  • перестать принимать новые запросы (readiness станет false);
  • закончить активные запросы;
  • корректно закрыть ресурсы.
  • Если время на shutdown маленькое — вы получите:

  • оборванные ответы;
  • всплеск 5xx;
  • повторные запросы от клиентов.
  • Сервер должен поддерживать graceful режим: не убивать воркер сразу, а дать ему «додышать». Это сочетается с вашим /health/ready (см. статью про probes), но само по себе readiness не спасает, если server убивает процессы мгновенно.

    ---

    4) Keep-alive: скрытый множитель нагрузки

    Keep‑alive — это сколько времени сервер держит TCP‑соединение открытым после ответа, ожидая следующий запрос.

    4.1. Почему keep-alive важен

  • Слишком маленький keep‑alive → больше новых TCP/TLS‑рукопожатий → лишняя задержка и CPU.
  • Слишком большой keep‑alive → много «висящих» соединений → расход файловых дескрипторов и памяти, особенно при медленных/шумных клиентах.
  • 4.2. Keep-alive и streaming

    Streaming удерживает соединение долго по определению. Здесь важно:

  • иметь отдельные лимиты на длительность стрима на уровне приложения;
  • понимать, что keep‑alive для обычных запросов и «долгое соединение» стрима — разные режимы.
  • Если у вас много streaming‑клиентов, то ограничения по соединениям становятся частью capacity‑планирования (не только RPS).

    4.3. Балансировка и keep-alive

    Если перед вами proxy/ingress:

  • он тоже имеет keep‑alive к клиентам;
  • он имеет keep‑alive к upstream (вашему сервису).
  • Несогласованные значения приводят к странным эффектам: upstream закрывает соединение раньше, чем proxy ожидает, и вы ловите ошибки уровня сети, которые выглядят как 5xx/502.

    ---

    5) Worker recycling: защита от деградации со временем

    Даже аккуратный сервис может деградировать из‑за:

  • фрагментации памяти (особенно с большими тензорами);
  • редких утечек в зависимостях;
  • накопления внутренних кешей.
  • Перезапуск воркеров по правилам — нормальная production‑практика.

    Что важно:

  • делать это постепенно, чтобы не устроить общий cold‑start;
  • сочетать с readiness (воркер не должен принимать трафик до warmup/готовности; механизм warmup обсуждался в статье про runtime).
  • Gunicorn здесь удобен: он умеет «обновлять» воркеры по лимитам запросов/времени жизни.

    ---

    6) Минимальные «боевые» профили запуска (ориентиры)

    Ниже — не догма, а отправные точки для тестов нагрузки.

    CPU‑serving (часто много коротких запросов)

  • Несколько воркеров (процессов) на pod.
  • Keep‑alive умеренный (чтобы не держать лишние соединения, но и не убивать производительность рукопожатиями).
  • Лимиты конкурентности инференса — внутри приложения (очередь/семафор), а не «на глаз» числом воркеров.
  • GPU‑serving (тяжёлые запросы)

  • Обычно 1 воркер на GPU.
  • Внутренний limiter + batching (если применимо) определяют реальную пропускную способность.
  • Timeouts и политики деградации важнее, чем «пытаться обслужить всех параллельно».
  • ---

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

    1) У вас LLM‑сервис на 1 GPU. Клиенты жалуются на OOM и резкий рост p99 после увеличения workers с 1 до 4. Объясните причинно‑следственную цепочку и предложите корректную схему запуска.

    2) Опишите, какие таймауты вы обязаны согласовать в цепочке «клиент → ingress → ASGI → приложение/runtime» для sync генерации, чтобы не получить шторм ретраев.

    3) Вам нужно поддержать streaming генерацию для 1000 одновременных клиентов. Какие риски появляются на уровне ASGI сервера и какие 3 параметра/политики вы будете контролировать в первую очередь?

    4) Сервис «плывёт» через сутки: растёт RAM и иногда воркеры подвисают. Какие 2 механизма на уровне process manager вы добавите, и какие метрики/сигналы будете смотреть, чтобы доказать, что стало лучше?

    <details> <summary> Ответы </summary>

    1) При 4 workers на одной GPU у вас 4 процесса, которые:

  • грузят модель 4 раза (VRAM и/или pinned RAM),
  • параллельно аллоцируют большие тензоры,
  • конкурируют за CUDA‑контекст.
  • Итог: пики памяти становятся выше, появляется фрагментация и OOM, а p99 растёт из‑за конкуренции и внутренних блокировок.

    Корректная схема: 1 worker на GPU + внутренний concurrency limiter/очередь/батчер (в зависимости от профиля) + масштабирование горизонтально по числу GPU.

    2) Минимально согласовать:

  • клиентский timeout (сколько ждёт клиент),
  • timeout на ingress/LB (сколько ждёт прокси),
  • серверные политики (graceful shutdown и запрет «вечных» зависаний),
  • runtime timeout внутри приложения (сколько вы реально даёте на инференс/ожидание очереди).
  • Правило: runtime timeout должен быть меньше внешнего (ingress/client), чтобы вы успевали вернуть контролируемую ошибку, а не получать обрыв соединения и ретраи.

    3) Риски streaming для ASGI:

  • много долгоживущих соединений (FD/memory),
  • медленные клиенты (сервер пишет быстрее, чем читают),
  • давление на воркер (если стримы блокируют event loop или создают лишние буферы).
  • Первые параметры/политики:

  • лимит максимальной длительности стрима (и/или max_tokens),
  • лимит одновременных стрим‑соединений на pod/воркер,
  • keep-alive/политики соединений (чтобы не держать лишнее) + мониторинг in-flight соединений.
  • 4) Два механизма:

  • worker recycling (перезапуск воркеров по лимиту запросов/времени жизни),
  • более строгий контроль зависаний (консервативные timeout/health‑политики, чтобы зависший воркер был заменён).
  • Сигналы:

  • RSS/память процесса во времени (доказательство, что рост «срезается»),
  • p95/p99 latency до/после (нет ли деградации от частых рестартов),
  • частота рестартов и коды ошибок (не выросли ли 5xx),
  • ready_state/health события (воркеры корректно уходят на drain и возвращаются).
  • </details>

    28. Kubernetes базово: Deployments, Services, Ingress, HPA

    Kubernetes базово: Deployments, Services, Ingress, HPA

    В production ML‑serving на FastAPI Kubernetes обычно становится «плоскостью управления»: он запускает реплики, обновляет их без простоя, даёт сетевую точку входа и масштабирует под нагрузку. В этой статье — базовые объекты, на которых держится 80% деплоя: Deployment, Service, Ingress, HPA.

    Чтобы не дублировать материалы курса: детали про healthchecks (liveness/readiness/startup) и их смысл уже разобраны в статье про probes; ниже мы покажем, где они живут в манифестах и как связаны с rollouts/масштабированием.

    ---

    1) Как эти 4 сущности складываются в один поток

    Ключевая идея:

  • Deployment управляет тем, какие Pod’ы запущены и как обновляться.
  • Service даёт стабильный адрес и балансировку трафика на Pod’ы.
  • Ingress — внешний HTTP‑вход (роутинг, TLS), ведёт к Service.
  • HPA — автомасштабирование (обычно по CPU/пользовательским метрикам), меняет replicas у Deployment.
  • ---

    2) Deployment: «как и сколько процессов сервиса должно работать»

    2.1. Что реально важно для ML‑serving

  • Replica модель
  • 1. CPU‑inference обычно масштабируется «в ширину» достаточно хорошо. 2. GPU‑inference почти всегда требует дисциплины: часто 1 Pod = 1 GPU, и конкурентность/батчинг контролируется внутри Pod’а.

  • Rolling updates (обновления без простоя)
  • - Deployment делает rollout, создавая новые Pod’ы и убирая старые. - Чтобы не ловить «холодные» хвосты и падения при старте, ваш readiness должен быть привязан к факту загрузки/прогрева runtime (см. статью про runtime lifecycle и статью про probes).

  • Requests/Limits по ресурсам
  • - Kubernetes планирует Pod’ы на ноды по requests; ограничивает потребление через limits. - Для ML‑serving это влияет на предсказуемость latency: если вы поставите слишком маленькие requests, Pod может оказаться «на шумной ноде» и хвосты p99 вырастут.

  • Graceful shutdown
  • - При обновлениях Kubernetes посылает SIGTERM и ждёт terminationGracePeriodSeconds. - Если timeout маленький, вы получите оборванные запросы и лишние ретраи.

    2.2. Минимальный скелет Deployment (с пояснениями)

    Практические замечания:

  • maxUnavailable: 0 полезно, когда вы хотите сохранять пропускную способность во время rollout. Цена — нужно, чтобы кластер мог вместить «лишний» Pod.
  • readinessProbe — ваш главный «предохранитель» от трафика на не прогретую модель.
  • 2.3. Частые ошибки Deployment’а именно для ML

  • Запуск нескольких Pod’ов (или процессов) на одну GPU без расчёта VRAM → OOM и рост p99.
  • Слишком агрессивные обновления (много Pod’ов одновременно) → cold‑start хвосты и каскадные 5xx.
  • Отсутствие requests или слишком низкие requests → нестабильная производительность.
  • ---

    3) Service: «стабильный адрес и балансировка на Pod’ы»

    Service выбирает Pod’ы по label‑селекторам и даёт:

  • стабильный DNS‑имя внутри кластера;
  • балансировку на все готовые (ready) Pod’ы.
  • 3.1. Типы Service (что выбрать)

  • ClusterIP — стандарт для backend‑сервисов (доступ внутри кластера). Почти всегда так и делаем, если вход снаружи идёт через Ingress.
  • NodePort/LoadBalancer — обычно не нужны напрямую, если есть Ingress Controller.
  • 3.2. Минимальный Service

    Критичный эффект для эксплуатации: Service будет слать трафик только на Pod’ы, которые прошли readiness.

    ---

    4) Ingress: внешний HTTP‑вход, TLS и маршрутизация

    Ingress — это правила для Ingress Controller (Nginx/Traefik/etc.), который уже выполняет роль edge‑прокси.

    4.1. Зачем он нужен в serving

  • Единая точка входа снаружи.
  • Роутинг по host/path на разные сервисы (например, inference и отдельный admin сервис).
  • TLS‑терминация.
  • 4.2. Что важно учитывать для ML‑эндпойнтов

  • Таймауты на уровне прокси
  • - Для sync‑генерации/долгих запросов вам нужно согласовать таймауты по всей цепочке (клиент → ingress → приложение/runtime). Иначе вы получите обрывы соединений и ретраи.

  • Ограничения на размер тела запроса
  • - Для CV (base64/файлы) это критично: без лимитов вы рискуете и по памяти, и по DoS.

  • Streaming
  • - Если используете streaming‑ответы, убедитесь, что ingress поддерживает корректный проксинг потоков и не буферизует ответ «до конца».

    4.3. Пример Ingress (идея, без привязки к конкретному контроллеру)

    Смысл:

  • Весь /v1/... трафик идёт на Service.
  • TLS секрет хранит сертификат/ключ (управление секретами — отдельная дисциплина, см. статью про конфигурацию и секреты).
  • ---

    5) HPA: автомасштабирование (и его границы в ML)

    HPA (Horizontal Pod Autoscaler) меняет spec.replicas у Deployment на основании метрик.

    5.1. Как HPA принимает решения (в терминах практики)

  • Вы задаёте целевую метрику (например, средний CPU utilization).
  • HPA периодически смотрит текущие значения.
  • Если выше цели — увеличивает replicas; если ниже — уменьшает (с учётом задержек и стабилизационных окон).
  • 5.2. Важные ограничения для ML‑serving

  • GPU‑сервисы масштабируются не так, как CPU
  • - CPU utilization часто плохой прокси для GPU‑inference. - Часто нужен кастомный сигнал: очередь/время ожидания, in‑flight, p95 latency или специализированные метрики runtime.

  • HPA не спасает от отсутствия backpressure
  • - Если вы не ограничиваете очередь/конкурентность, HPA может «не успеть», и сервис будет деградировать раньше.

  • Масштабирование добавляет cold‑start
  • - Новые Pod’ы должны скачать/загрузить/прогреть модель. Поэтому readiness/startup‑логика должна быть корректной.

    5.3. Пример HPA по CPU

    Инженерный смысл:

  • minReplicas — базовая мощность (плюс устойчивость к одной ноде/поду).
  • maxReplicas — предохранитель от «раздувания» при аномалиях.
  • ---

    6) Мини‑чеклист «работает ли базовая связка»

  • Deployment: rollout проходит, Pod’ы становятся ready только после готовности runtime.
  • Service: трафик идёт только на ready Pod’ы.
  • Ingress: внешний вход маршрутизируется на нужный Service, таймауты/лимиты соответствуют режиму (sync/streaming).
  • HPA: умеет добавлять реплики без каскадных отказов; метрика реально отражает перегруз (не только «в среднем CPU»).
  • ---

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

    1) Нарисуйте (текстом) поток запроса от клиента до Pod’а, указав где участвуют Ingress и Service, и где именно отсекаются «не ready» Pod’ы.

    2) Вы деплоите LLM на 1 GPU. Какие два решения вы примете в Kubernetes‑манифестах, чтобы избежать конкуренции за VRAM? (Подсказка: одно — про реплики/планирование, второе — про модель запуска.)

    3) Придумайте политику rollout для сервиса, у которого cold start ~90 секунд. Какие параметры Deployment вы бы выбрали (на уровне идеи), чтобы избежать деградации во время обновления?

    4) Почему HPA по CPU может быть бесполезен для GPU‑inference? Назовите 3 причины и 2 альтернативных сигнала для масштабирования.

    5) Для CV‑endpoint с большими payload’ами перечислите 3 риска на уровне Ingress и 3 меры снижения риска.

    <details> <summary> Ответы </summary>

    1) Пример потока:

    2) Пример двух решений:

  • Планирование/реплики: держать 1 реплику на 1 GPU (или 1 Pod на GPU‑ноде с корректными ресурсами/селектором), чтобы не запускать несколько экземпляров модели на одной VRAM «случайно».
  • Модель запуска внутри Pod’а: избегать нескольких процессов‑воркеров, которые грузят модель независимо (часто для GPU — один процесс + внутренний limiter/батчер в приложении/runtime).
  • 3) Пример rollout‑идеи:

  • RollingUpdate с небольшим maxSurge (например, 1) и maxUnavailable: 0, чтобы не терять мощность во время обновления.
  • Достаточный terminationGracePeriodSeconds, чтобы активные запросы успели завершиться.
  • Обязательная readiness/startup логика: Pod не должен становиться ready до окончания загрузки и warmup (иначе трафик попадёт на «холодный» runtime).
  • 4) Почему CPU‑HPA может не работать для GPU:

  • Основная нагрузка уходит в GPU, а CPU может оставаться «низким» даже при перегрузе по VRAM/очереди.
  • CPU utilization плохо отражает рост очереди и хвостов p99 (особенно при батчинге).
  • Перегруз может проявляться как рост ожидания слота GPU, а не рост CPU.
  • Альтернативные сигналы:

  • Время ожидания в очереди/батчере (queue_wait).
  • In‑flight/активные запросы или доля overloaded отказов (как показатель capacity exceeded).
  • 5) Риски и меры на Ingress:

    Риски:

  • Слишком большой body → нагрузка на память и сеть.
  • Неправильные таймауты → обрывы и ретраи (усиление нагрузки).
  • Буферизация/неэффективный проксинг → рост задержек и расход ресурсов.
  • Меры:

  • Лимит размера запроса на уровне ingress (и согласованный лимит на уровне приложения).
  • Таймауты и лимиты соединений, согласованные с режимом работы endpoint’а.
  • Явная политика для тяжёлых режимов: вынос в async/job‑based или отдельный путь/Ingress‑правила (если нужно разделять классы трафика).
  • </details>

    29. Helm chart: values, templates, secrets, configmaps

    Helm chart: values, templates, secrets, configmaps

    Helm в production ML‑serving полезен не “как ещё один способ написать YAML”, а как механизм упаковки и параметризации деплоя: один chart → разные окружения (dev/stage/prod), воспроизводимость, управляемые обновления и откаты. Kubernetes‑объекты (Deployment/Service/Ingress/HPA) и probes уже разобраны в предыдущих статьях; здесь — как это правильно собрать в Helm chart и как дисциплинированно управлять конфигом и секретами.

    1) Что такое chart и где в нём “источник правды”

    Chart — это каталог с:

  • Chart.yaml: метаданные (name/version/appVersion).
  • values.yaml: значения по умолчанию (конфигурация “как код”).
  • templates/: шаблоны Kubernetes‑манифестов.
  • templates/_helpers.tpl: функции/шаблоны для переиспользования (имена, лейблы, selector’ы).
  • (Опционально) values.schema.json: валидация значений.
  • (Опционально) charts/ или dependencies: зависимости (например, redis/postgres как subchart).
  • Ключевая дисциплина: Kubernetes‑манифесты — это templates, а вариативность окружений — это values, а не “копипаста YAML на каждое окружение”.

    2) Values: как проектировать, чтобы chart не превратился в “мешок флагов”

    2.1. Структура values должна отражать контуры сервиса

    Опирайтесь на архитектурные контуры из статей про конфигурацию и референс‑архитектуру:

  • image: репозиторий/тег/дижест.
  • service: порты, тип Service.
  • ingress: host/path/TLS/аннотации.
  • resources: requests/limits.
  • probes: пути и тайминги (ссылка на статью про healthchecks).
  • autoscaling: HPA параметры.
  • appConfig: не‑секретные настройки приложения.
  • appSecrets: ссылки на секреты (не сами секреты).
  • Так проще review: изменение batch window, лимитов, воркеров, ресурсов видно как изменение конкретного блока.

    2.2. Values layering: один chart, много окружений

    Практика:

  • values.yaml — безопасные дефолты.
  • values-dev.yaml, values-stage.yaml, values-prod.yaml — различия окружений.
  • CI/CD выбирает нужный файл значений.
  • Правило: prod должен отличаться значениями, а не отдельным chart (иначе расползаются патчи и откаты).

    2.3. Валидация values (values.schema.json)

    Если у вас “production‑опасные” параметры (лимиты входа, таймауты инференса, batch window, max concurrency), имеет смысл валидировать их схемой:

  • типы (int/string/bool)
  • допустимые диапазоны
  • required поля
  • Это снижает риск деплоя с некорректной конфигурацией.

    3) Templates: минимальный набор паттернов, которые нужны почти всегда

    Шаблоны Helm — это Kubernetes YAML + Go‑templating. Цель не в “сложной логике”, а в переиспользуемости и предсказуемости.

    3.1. _helpers.tpl: стандартизируйте имена и labels

    Рекомендуется вынести в helpers:

  • fullname ресурса (учёт release name)
  • common labels (app.kubernetes.io/name, app.kubernetes.io/instance, версия)
  • selector labels (строго стабильные; не включайте туда динамические поля)
  • Почему это важно: неправильные selector’ы ломают rollout и масштабирование.

    3.2. Принципы шаблонов

  • Держите templates “тупыми”: минимум вычислений; максимум — вставка toYaml | nindent.
  • Используйте required для критичных значений (например, image tag/digest, host ingress, имя существующего secret).
  • Не генерируйте случайности (рандомные имена/пароли) внутри chart для production без чёткого жизненного цикла.
  • Ограничивайте conditionals: если параметр выключает ресурс — пусть это читаемо (ingress.enabled, autoscaling.enabled).
  • 3.3. Типовой состав templates для serving

    Не дублируя Kubernetes‑материал:

  • Deployment
  • Service
  • Ingress (если нужен)
  • HPA (если включён)
  • ServiceAccount/Role/RoleBinding (если есть доступы)
  • ConfigMap
  • Secret (или ссылка на внешний)
  • 4) ConfigMap: управление не‑секретной конфигурацией

    4.1. Что класть в ConfigMap

    Только то, что:

  • не является секретом
  • должно меняться без пересборки образа
  • относится к runtime‑поведению сервиса
  • Примеры: лимиты, флаги включения фич, параметры батчинга, переключатели профилей. Конкретные поля и их смысл — в статье про конфигурацию.

    4.2. Как подать ConfigMap приложению

    Два основных способа:

  • env vars: просто, прозрачно, хорошо для pydantic‑settings.
  • volume mount файлов: удобно, если конфиг большой или вы придерживаетесь файлового формата.
  • Выберите один стиль и придерживайтесь его. Смешивание “половина env, половина файл” быстро усложняет отладку.

    4.3. Rolling restart при изменении ConfigMap

    Один из типичных production‑багов: вы обновили ConfigMap, но Pod’ы не перезапустились, и часть реплик работает со старым конфигом.

    Паттерн: добавлять в Deployment аннотацию с checksum от содержимого ConfigMap (и отдельно Secret), чтобы изменение конфигурации гарантированно меняло Pod template и триггерило rollout.

    5) Secrets: как не “утечь” и как не сломать эксплуатацию

    5.1. Главное правило: секреты не должны лежать в values

    Значения Helm часто:

  • попадают в CI‑логи
  • хранятся в Git
  • попадают в release history
  • Поэтому в values.yaml обычно хранят:

  • имя Secret (existingSecretName)
  • ключи внутри Secret (redisPasswordKey, s3TokenKey)
  • А сам Secret создаётся:

  • либо внешним механизмом (секрет‑менеджер/оператор)
  • либо отдельной процедурой в CI/CD (с ограниченными правами)
  • либо (для учебных/простых окружений) helm‑ресурсом, но тогда надо осознанно решать хранение и ротацию
  • 5.2. Secret как env vs Secret как файл

  • env: удобно для токенов и простых секретов.
  • файл (volume mount): удобнее для сертификатов, больших ключей, случаев, когда библиотека ожидает путь.
  • Секреты и их эксплуатация (ротация, запреты на логирование) уже обсуждались в статье про конфигурацию/секреты — в Helm важно не нарушить эту дисциплину.

    5.3. Разделяйте “секрет” и “ссылка на секрет”

    Хороший values‑интерфейс:

  • appSecrets.existingSecretName: ml-serving-secrets
  • appSecrets.keys.redisPassword: REDIS_PASSWORD
  • Плохой:

  • redis.password: "supersecret"
  • 6) Практика релизов Helm: обновления, откаты, атомарность

    Для production‑обновлений важны не команды Helm, а свойства процесса:

  • Имутабельность артефактов: chart version фиксируется, image tag лучше привязывать к commit SHA или digest.
  • Rollback‑готовность: Helm хранит историю релизов; откат должен возвращать и манифесты, и конфигурацию.
  • Ожидание готовности: деплой должен ждать readiness (иначе вы “успешно задеплоили” неготовые Pod’ы). Readiness‑логика — в статье про probes.
  • Разделение изменений:
  • 1. изменение образа (код) 2. изменение конфигурации (ConfigMap) 3. изменение секретов (Secret)

    Эти изменения имеют разные риски и должны быть читаемы в diff.

    7) Частые ошибки Helm в ML‑serving

  • Секреты в values → утечки и неконтролируемая ротация.
  • Selector labels зависят от версии/тега → Deployment не обновляется корректно.
  • Нет checksum‑аннотаций → ConfigMap/Secret обновили, а Pod’ы остались на старом.
  • Слишком “умные” шаблоны с логикой условий → сложно review, сложно тестировать.
  • Использование latest image tag → невоспроизводимые откаты.
  • ---

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

    1) Спроектируйте структуру values.yaml (8–12 ключей верхнего уровня) для ML‑serving сервиса, чтобы она отражала контуры: ingress, resources, probes, autoscaling, appConfig, appSecrets.

    2) Опишите интерфейс values для секретов: какие поля вы оставите в values, чтобы секреты не попадали в Git, но chart оставался самодостаточным.

    3) Придумайте, какие 3 ресурса вы сделаете условными (enabled) и почему.

    4) Опишите, как вы обеспечите rollout при изменении ConfigMap и Secret без ручного рестарта.

    5) Сформулируйте 5 правил code review для PR, который меняет Helm chart (ориентируясь на безопасность и предсказуемость деплоя).

    <details> <summary>

    Ответы

    </summary>

    1) Пример структуры values.yaml:

  • image.repository, image.tag (или image.digest)
  • service.port, service.type
  • ingress.enabled, ingress.host, ingress.tls.enabled
  • resources.requests, resources.limits
  • probes.livenessPath, probes.readinessPath, тайминги
  • autoscaling.enabled, autoscaling.minReplicas, autoscaling.maxReplicas, метрика/цель
  • appConfig (лимиты/таймауты/профили)
  • appSecrets.existingSecretName, appSecrets.keys.*
  • 2) Интерфейс values для секретов:

  • хранить только:
  • 1. existingSecretName 2. карту ключей внутри секрета (keys.redisPassword, keys.s3Token) 3. способ подачи (env или volume mount)
  • не хранить:
  • 1. реальные значения токенов/паролей 2. base64‑строки сертификатов

    3) Три условных ресурса:

  • Ingress (не всегда нужен, иногда вход через другой gateway)
  • HPA (на некоторых окружениях фиксированная мощность)
  • ServiceAccount/RoleBinding (если сервису не нужны доп. права в кластере)
  • 4) Rollout при изменении ConfigMap/Secret:

  • в шаблон Deployment добавить аннотации вида checksum/config и checksum/secret, зависящие от содержимого соответствующих ресурсов
  • при изменении ConfigMap/Secret меняется Pod template → Kubernetes делает rolling update
  • 5) 5 правил review Helm PR:

  • Нет секретов в values и в templates (кроме ссылок на Secret).
  • Selector labels стабильны и не включают версии/случайные значения.
  • Изменения конфигурации/ресурсов читаемы: понятно, что изменилось в resources, probes, appConfig.
  • Есть механизм обновления Pod’ов при изменении ConfigMap/Secret (checksum‑паттерн).
  • Образ иммутабелен (не latest), и откат воспроизводим (по tag/digest и chart version).
  • </details>

    3. Архитектурные паттерны: Clean Architecture, Hexagonal, DDD-lite

    Архитектурные паттерны: Clean Architecture, Hexagonal, DDD-lite

    Production ML‑serving сервис (LLM/CV) быстро обрастает “обвязкой”: лимиты, очереди/батчинг, версии моделей, кеши, наблюдаемость, политики деградации. Если всё это складывать прямо в обработчики FastAPI, получается хрупкий монолит: трудно тестировать, трудно менять runtime, сложно разделять ответственность.

    В предыдущих статьях уже разобраны требования к production‑сервису и референс‑компоненты (API → очередь/батчинг → runtime → observability). Здесь фокус — как организовать код и границы модулей, чтобы эти контуры не смешивались.

    1) Clean Architecture: слои и правило зависимостей

    1.1. Идея

    Clean Architecture предлагает слои, где зависимости направлены внутрь: внешние фреймворки и инфраструктура не “протекают” в бизнес‑логику.

    Упрощённая схема:

    Правило: внутренние слои не импортируют внешние. Например, домен не должен знать про FastAPI, HTTP‑коды, Redis, конкретный движок инференса.

    1.2. Как это “ложится” на ML‑serving

    В ML‑serving есть особенность: “бизнес‑логика” — это не только “вызвать модель”, а ещё:

  • политика лимитов (max tokens / max image side / max payload)
  • выбор профиля инференса (например, fast vs quality)
  • решение “кешировать или нет”
  • решение “идти в sync или job‑based” (если сервис поддерживает оба)
  • Домен (Domain) — это правила и модели предметной области инференса:

  • InferenceRequest (уже нормализованный, без транспортных деталей)
  • InferenceOptions (параметры генерации/препроцессинга)
  • ModelId/ModelVersion как value object (чтобы не таскать голые строки)
  • ошибки домена: InputTooLarge, PolicyViolation, Overloaded
  • Use Cases (Application) — сценарии:

  • PredictText / GenerateText / EmbedText (LLM)
  • ClassifyImage / Detect / EmbedImage (CV)
  • оркестрация: валидация политик → (опционально) кеш → очередь/батчинг → runtime → постобработка
  • Interface Adapters — перевод между мирами:

  • вход: HTTP DTO (Pydantic) → доменный запрос
  • выход: доменный результат → HTTP response
  • реализации портов (например, репозиторий артефактов модели поверх S3/FS)
  • Frameworks & Drivers — конкретика:

  • FastAPI маршруты
  • PyTorch/Transformers/ONNX Runtime/TensorRT
  • Redis, брокер очереди, клиент метрик
  • 1.3. Практическая польза

  • Тестируемость: use case тестируется без поднятого FastAPI и без GPU — через заглушки портов.
  • Заменяемость runtime: можно мигрировать с PyTorch на ONNX Runtime, не переписывая use cases.
  • Контроль “протечек”: нет ситуаций, где доменная логика возвращает JSONResponse или кидает HTTPException.
  • 2) Hexagonal Architecture (Ports & Adapters): сервис как “ядро”

    2.1. Идея

    Hexagonal (она же Ports & Adapters) мыслит систему как ядро (application/domain) и набор адаптеров:

  • Inbound adapters: кто вызывает ядро (HTTP, gRPC, queue consumer)
  • Outbound adapters: кого вызывает ядро (runtime модели, кеш, хранилище, метрики)
  • Схема:

    Главное отличие от “слоёв” в восприятии: в центре — набор портов (интерфейсов), которые описывают, что нужно ядру.

    2.2. Порты, которые почти всегда нужны в ML‑serving

    Минимально полезные порты (как контракты, не как конкретные библиотеки):

  • ModelRuntimePort: “выполни инференс для батча запросов с профилем”
  • ArtifactsPort: “дай активную версию модели и связанные файлы/метаданные”
  • CachePort (опционально): “get/set по ключу с TTL”
  • ObservabilityPort: “запиши метрики/события/тайминги”
  • ClockPort (мелочь, но полезно): для таймаутов и измерений времени в тестах
  • Почему это особенно важно для high load:

  • ядро может реализовать backpressure‑решения (например, отказ при перегрузе) независимо от того, какая очередь/кеш используются
  • адаптеры можно оптимизировать отдельно (например, заменить кеш или поменять формат артефактов)
  • 3) DDD-lite для ML‑serving: достаточно “смысловых границ”

    3.1. Что такое DDD-lite в контексте serving

    Полный DDD часто избыточен для serving‑сервиса (особенно если это stateless инференс). Но DDD‑lite даёт две критичные вещи:

  • Единый язык (ubiquitous language): “профиль инференса”, “политика усечения”, “версия модели”, “класс обслуживания”, “батч‑окно”.
  • Границы контекстов (bounded contexts): чтобы не смешивать независимые части.
  • 3.2. Пример bounded contexts

    Для production ML‑serving удобно мыслить как минимум так:

  • Inference Context: запрос/ответ, профили, правила совместимости, постобработка.
  • Model Lifecycle Context: активная версия, прогрев, readiness, метаданные, откат.
  • Traffic Policy Context: лимиты, классы обслуживания, деградация, приоритизация.
  • Observability Context: корреляция, метрики по стадиям, события ошибок.
  • Не обязательно разводить это на отдельные сервисы — цель DDD‑lite: не смешивать модели данных и термины.

    3.3. Value objects и ошибки как часть домена

    Два практичных приёма DDD‑lite:

  • Value objects вместо строк:
  • 1. ModelId, RequestId, CacheKey 2. TokenLimit, ImageSizeLimit

  • Ошибки как типы домена:
  • 1. InvalidInput — доменная причина (не “400”, а именно “почему”) 2. Overloaded — доменная причина отказа

    Потом адаптер HTTP маппит это в конкретные коды/формат ответа.

    4) Как выбрать и комбинировать паттерны (без религии)

    На практике для FastAPI ML‑serving чаще всего работает комбинация:

  • Hexagonal как “скелет”: порты/адаптеры вокруг application core.
  • Clean Architecture как дисциплина слоёв внутри core: domain → use cases.
  • DDD‑lite как способ удерживать смысл и границы (термины, контексты, типы).
  • Признак удачной комбинации: вы можете заменить FastAPI на другой inbound (например, consumer очереди) и заменить runtime (PyTorch ↔ ONNX) без переписывания use cases.

    5) Типовые антипаттерны в ML‑serving кодовой базе

  • “Толстые” HTTP‑handlers: в одном месте и валидация, и токенизация/декодирование, и вызов runtime, и метрики.
  • Импорт FastAPI внутрь домена: доменные функции возвращают Response или бросают HTTPException.
  • Смешивание DTO и доменных моделей: Pydantic‑модели начинают жить везде, и изменения API ломают core.
  • Глобальные синглтоны без явных зависимостей: “где-то в модуле лежит загруженная модель” → сложно тестировать и контролировать жизненный цикл.
  • Инфраструктура диктует домен: например, ключ кеша определяется тем, “как удобно Redis”, а не тем, “что действительно влияет на результат”.
  • 6) Минимальная “карта модулей” для репозитория

    Один из практичных вариантов структуры (не единственный):

    Важно не название папок, а то, что зависимости направлены “к ядру”, а адаптеры находятся на краях.

    ---

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

    1) Для вашего LLM или CV сервиса перечислите: 1. 3 доменных сущности/value objects 2. 2 доменные ошибки 3. 3 порта (ports), которые нужны application core

    2) Нарисуйте (текстом) hexagonal‑схему вашего сервиса: какие inbound и outbound адаптеры есть в sync‑режиме и какие добавятся в async job‑based.

    3) Придумайте пример изменения требований, при котором архитектура “выстрелит себе в ногу”, если у вас всё в FastAPI‑handlers, и объясните, как порты/адаптеры это упрощают.

    4) Составьте правило: какие типы данных запрещено импортировать в домен (минимум 4 пункта) и почему.

    <details> <summary> Ответы </summary>

    1) Пример (LLM):

  • Value objects/сущности:
  • 1) ModelId (идентификатор модели) 2) InferenceProfile (fast/quality + лимиты) 3) RequestId (корреляция)

  • Доменные ошибки:
  • 1) InputTooLarge (превышены лимиты контекста/пейлоада) 2) Overloaded (очередь/лимитер отказал в приёме)

  • Порты:
  • 1) ModelRuntimePort (выполнить инференс) 2) ArtifactsPort (получить активную версию/метаданные) 3) ObservabilityPort (тайминги/счётчики/события)

    2) Пример схемы (упрощённо):

    3) Пример изменения: решили заменить runtime с PyTorch на ONNX Runtime для стабилизации latency.

  • Если логика “размазана” по handlers: в обработчиках напрямую импортируется PyTorch, тензоры, device‑логика → придётся переписывать много мест.
  • Если есть ModelRuntimePort: use case вызывает абстракцию, а вы меняете только outbound‑адаптер runtime.
  • 4) Пример “что нельзя импортировать в домен”:

  • FastAPI/Starlette типы (Request/Response/HTTPException) — домен не должен зависеть от транспорта.
  • Pydantic DTO из API слоя — изменение контракта не должно ломать правила домена.
  • Клиенты Redis/брокера — домен не должен знать, как хранятся/доставляются данные.
  • Конкретные ML‑библиотеки (torch, onnxruntime) — домен описывает намерение (“сделай инференс”), а не механизм.
  • Почему: иначе вы теряете тестируемость и заменяемость инфраструктуры, а изменения внешних библиотек начинают “протекать” в ядро.

    </details>

    30. Ресурсы и GPU: requests/limits, node selectors, runtimeClass

    Ресурсы и GPU: requests/limits, node selectors, runtimeClass

    Высоконагруженный ML‑serving в Kubernetes чаще ломается не из‑за «не того YAML», а из‑за неправильной модели ресурсов: Pod попадает на «не ту» ноду, получает меньше CPU/памяти, чем вы предполагали, или вообще не получает доступ к GPU‑устройству. В статье про Kubernetes базово мы уже касались requests/limits и общего смысла rollout’ов; здесь — концентрат про ресурсы и планирование именно для LLM/CV‑инференса.

    1) requests и limits: что это означает для планировщика и производительности

    1.1. requests — «гарантированный минимум» для планирования

    Kubernetes планирует Pod на ноду исходя из requests:

  • cpu request — сколько CPU «зарезервировано».
  • memory request — сколько памяти «зарезервировано».
  • для device‑ресурсов (GPU) request — это фактически «выдай устройство».
  • Практический смысл для serving:

  • Если занижать cpu request, Pod легко окажется на перегруженной ноде → вырастут p95/p99.
  • Если занижать memory request, Pod будет чаще вытесняться при memory pressure.
  • Если вы ожидаете стабильную задержку, requests — это часть ваших NFR (см. статью про latency/throughput/SLO).
  • 1.2. limits — «верхняя граница» и источник двух разных проблем

    limits ограничивают потребление ресурса:

  • CPU limit: при превышении контейнер будет throttling’иться (в Linux cgroups) → p99 может резко ухудшиться без явных ошибок.
  • Memory limit: при превышении контейнер получит OOM kill → рестарт Pod’а.
  • Для ML‑serving обычно:

  • CPU limit ставят аккуратно (или вообще не ставят), потому что throttling часто хуже предсказуемого использования CPU.
  • Memory limit обязателен, иначе один «аномальный» запрос или утечка могут повлиять на ноду целиком.
  • 1.3. QoS классы Pod’ов (почему это важно под давлением памяти)

    Kubernetes классифицирует Pod’ы по QoS:

    | QoS класс | Условия (упрощённо) | Что это значит при pressure | |---|---|---| | Guaranteed | requests == limits для CPU и memory во всех контейнерах | вытесняются последними | | Burstable | requests заданы, но не равны limits | средний приоритет | | BestEffort | requests/limits не заданы | вытесняются первыми |

    Для production serving минимум — быть не BestEffort.

    Практика для инференса:

  • Если сервис критичный и память «впритык» (особенно с GPU + большие модели), стремятся к Guaranteed по memory.
  • CPU чаще оставляют Burstable, чтобы не ловить throttling.
  • 2) GPU в Kubernetes: как запросить устройство и не получить «случайную конкуренцию»

    2.1. GPU — это device resource, обычно без overcommit

    GPU в Kubernetes обычно предоставляется как расширенный ресурс (extended resource), например nvidia.com/gpu.

    Ключевые свойства:

  • Планировщик выделяет GPU «целыми штуками» (1, 2, …), если не используется специальное шаринг‑решение.
  • «Лимиты» для GPU на практике совпадают с запросом: вы либо получили устройство, либо нет.
  • Минимальная дисциплина для serving:

  • 1 Pod = 1 GPU (самый предсказуемый вариант).
  • Внутри Pod — контролируемая конкурентность/батчинг (см. статьи про runtime и batching), иначе GPU можно «убить» даже с одной репликой.
  • 2.2. MIG и «доли GPU» (если нужна сегментация)

    На некоторых GPU (например, A100) можно включать MIG (Multi‑Instance GPU) и получать «кусочки» GPU как отдельные устройства.

    Смысл в serving:

  • Вы можете планировать несколько Pod’ов на одну физическую GPU, но каждый Pod получает свой MIG‑инстанс.
  • Это предсказуемее, чем «шарить» одну GPU несколькими процессами без аппаратной изоляции.
  • Что важно зафиксировать архитектурно:

  • MIG — это изменение инфраструктуры нод (а не просто параметр Pod).
  • Лимиты батча/VRAM в runtime всё равно нужны, но модель становится стабильнее.
  • 2.3. Почему «несколько процессов на одну GPU» обычно плохо

    Даже если Kubernetes позволяет запустить несколько Pod’ов/воркеров на одной GPU (через ошибки конфигурации или ручные хаки), типовые последствия:

  • VRAM расходуется несколькими копиями модели.
  • Пики аллокаций тензоров накладываются → OOM.
  • Вырастает p99 из‑за конкуренции и фрагментации памяти.
  • Эта причинно‑следственная цепочка уже проявляется на уровне ASGI‑воркеров (см. статью про ASGI server), а на уровне Kubernetes она усугубляется неверным ресурсным планированием.

    3) Node selectors и affinity: как попасть на «нужные» ноды

    3.1. nodeSelector: простой фильтр по label’ам

    nodeSelector — это «Pod может быть запущен только на нодах с такими label’ами».

    Типовые применения для ML:

  • Разделить CPU‑ноды и GPU‑ноды.
  • Развести разные поколения GPU (например, T4 vs A100).
  • Отделить «дешёвые» spot/preemptible ноды от стабильных.
  • Плюс: простота и предсказуемость.

    Минус: нет гибких предпочтений (только жёсткое соответствие).

    3.2. Node affinity: “required” и “preferred”

    Affinity позволяет:

  • requiredDuringSchedulingIgnoredDuringExecution — жёсткое требование (как nodeSelector, но богаче).
  • preferredDuringSchedulingIgnoredDuringExecution — предпочтение (если есть выбор).
  • Практическое правило для production GPU‑serving:

  • Жёстко требуйте наличие правильного класса нод (GPU/тип GPU).
  • Предпочтениями можно «раскладывать» нагрузку (например, предпочитать ноды в определённой зоне).
  • 3.3. Pod anti-affinity: чтобы реплики не оказались на одной ноде

    Если сервис CPU‑масштабируемый и у вас несколько реплик, часто полезно анти‑аффинити по hostname, чтобы:

  • не потерять все реплики при падении одной ноды;
  • снизить конкуренцию за CPU/диск/сеть.
  • Для GPU‑serving это также актуально, если у вас несколько GPU‑нод: вы хотите распределить реплики, а не «упаковать» их на одну.

    4) Taints и tolerations: защита GPU‑нод от “случайных” Pod’ов

    GPU‑ноды обычно дорогие. Частая практика — таинтить их, чтобы обычные Pod’ы туда не попадали.

    Модель:

  • Нода имеет taint вроде gpu=true:NoSchedule.
  • Только Pod’ы с соответствующим toleration могут быть запланированы.
  • Зачем это важно:

  • Без taints на GPU‑ноды может «утечь» обычный сервис, и вы потеряете GPU‑ёмкость на фоновые штуки.
  • Для serving это снижает риск «почему на GPU‑ноде внезапно не хватает CPU/памяти».
  • 5) runtimeClass: зачем он нужен и где реально применяется

    runtimeClass в Kubernetes выбирает контейнерный runtime/конфигурацию запуска.

    Типичный кейс для GPU:

  • У вас настроен отдельный runtime (например, NVIDIA runtime) для корректного проброса устройств.
  • Для Pod’ов, которым нужен GPU, выставляется нужный runtimeClassName.
  • Почему это полезно в эксплуатации:

  • Явно отделяет GPU‑Pod’ы от обычных.
  • Снижает шанс «Pod запросил GPU, но внутри контейнера устройства не видно» из‑за несовместимого runtime.
  • Что важно не перепутать:

  • runtimeClass не заменяет запрос ресурса GPU (вы всё равно должны запросить nvidia.com/gpu).
  • runtimeClass — это про то, как запускается контейнер, а не про решение планировщика, куда его поставить.
  • 6) Дополнительные ресурсы, которые часто забывают (и потом ловят сюрпризы)

    6.1. Ephemeral storage

    В ML‑serving часто есть временные данные:

  • скачивание/кеш артефактов;
  • временные файлы декодирования;
  • логи/спулы.
  • Если ephemeral storage не контролировать, Pod может:

  • забить диск ноды;
  • попасть под eviction.
  • 6.2. Общая память (/dev/shm)

    Некоторые пайплайны (особенно CV/декодирование, multiprocessing) используют shared memory. В контейнерах /dev/shm по умолчанию может быть маленьким.

    Решение обычно инфраструктурное (volume emptyDir с medium: Memory) и должно быть осознанным, иначе получите редкие падения/деградации.

    7) Практическая матрица решений для LLM/CV serving

    | Вопрос | CPU‑serving (embeddings/CV на CPU) | GPU‑serving (LLM/CV на GPU) | |---|---|---| | Requests | достаточно близко к реальному потреблению | жёстко планировать по GPU + адекватная память | | Limits CPU | осторожно (throttling ухудшает p99) | часто вторично относительно GPU, но CPU для препроцессинга важен | | Limits memory | обязательно | обязательно (VRAM отдельно, но RAM тоже критична) | | Планирование | nodeSelector/affinity по классу нод, anti-affinity по репликам | nodeSelector/affinity по типу GPU, taints/tolerations обязательны | | runtimeClass | обычно не нужно | часто нужно для корректного GPU runtime |

    8) Что обязательно проверить руками при первом деплое GPU‑сервиса

  • Pod действительно запланирован на GPU‑ноду (а не «висит в Pending»).
  • В контейнере видно GPU‑устройство.
  • Readiness становится true только после загрузки и warmup (см. статью про healthchecks и статью про runtime lifecycle).
  • При увеличении реплик не возникает «скрытой» конкуренции за одну GPU.
  • ---

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

    1) Объясните разницу между requests и limits на примере CPU и memory. Какие две разные проблемы могут появиться из‑за слишком низкого CPU limit и слишком низкого memory limit?

    2) Почему для GPU‑inference часто выбирают модель «1 Pod = 1 GPU»? Назовите минимум 3 причины, связанные с производительностью и стабильностью.

    3) Вам нужно, чтобы только GPU‑Pod’ы попадали на GPU‑ноды, а остальные сервисы — никогда. Какие два механизма Kubernetes вы примените (один на стороне нод, один на стороне Pod)?

    4) Опишите роль runtimeClass в GPU‑деплое. Что он решает и что он не решает?

    5) Приведите пример, когда стоит использовать anti‑affinity для реплик serving‑сервиса, и какой риск она снижает.

    <details> <summary> Ответы </summary>

    1)

  • requests — минимум, по которому Kubernetes планирует Pod на ноду и «резервирует» ресурсы в модели планировщика. Если requests занижены, Pod может оказаться в более конкурентной среде и p99 вырастет.
  • limits — верхняя граница потребления.
  • Две типовые проблемы:

  • Слишком низкий CPU limit → CPU throttling: процесс не падает, но резко растут хвосты latency (p95/p99), особенно при всплесках.
  • Слишком низкий memory limit → OOM kill: контейнер будет убит, Pod перезапустится, вы получите ошибки/ретраи и cold-start эффекты.
  • 2) Причины «1 Pod = 1 GPU»:

  • Каждая реплика держит свою копию модели и свои буферы → если несколько процессов на одной GPU, VRAM быстро заканчивается.
  • Пики аллокаций тензоров накладываются → растёт риск OOM и нестабильности.
  • Конкуренция за один CUDA‑контекст ухудшает предсказуемость и увеличивает p99.
  • 3) Механизмы:

  • На стороне нод: taint на GPU‑нодах (например, gpu=true:NoSchedule).
  • На стороне Pod: toleration, который разрешает GPU‑Pod’у быть запланированным на tainted‑ноду.
  • Дополнительно обычно используют nodeSelector/affinity, чтобы GPU‑Pod точно попадал на нужный класс нод.

    4) runtimeClass:

  • Решает: выбор контейнерного runtime/конфигурации запуска, которая обеспечивает корректный проброс GPU‑устройств и специфичных настроек окружения.
  • Не решает: планирование по GPU ресурсу само по себе. Чтобы получить GPU, Pod всё равно должен запросить соответствующий device‑resource (например, nvidia.com/gpu).
  • 5) Anti‑affinity полезна, когда:

  • у вас несколько реплик serving‑сервиса, и вы хотите, чтобы они не оказались на одной ноде.
  • Снижает риски:

  • «Одна нода упала → сервис полностью недоступен».
  • Конкуренция между репликами за общие ресурсы ноды (CPU, диск, сеть), что может ухудшать хвосты latency.
  • </details>

    31. Паттерны релизов: blue-green, canary, rollback, migrations

    Паттерны релизов: blue-green, canary, rollback, migrations

    Релиз в production ML‑serving — это управляемая смена кода, конфигурации, модели/артефактов и иногда схемы данных так, чтобы:

  • пользователи не теряли доступность и SLO;
  • хвосты latency (p95/p99) не «взрывались» из‑за cold start, прогрева и очередей;
  • вы могли быстро откатиться (и понимали, что именно откатывать).
  • Ниже — практические паттерны релизов и их взаимодействие с миграциями БД. Детали healthchecks, SLO/error budget, схемы хранилищ и миграции в стиле expand/contract уже разобраны ранее — здесь мы фокусируемся на механике релиза и рисках именно serving‑сервиса.

    ---

    1) Что именно мы «релизим» в ML‑serving

    В отличие от обычного веб‑сервиса, «релиз» почти всегда составной:

  • Application release: код FastAPI + runtime‑обвязка (лимиты, батчер, кеш, observability).
  • Model release: новая версия артефактов (веса/токенизатор/конфиги) и/или параметры исполнения (dtype, max_tokens, image_size).
  • Config release: переключатели профилей, лимиты, параметры батчинга.
  • Data release: миграции схемы БД (метаданные/джобы/версии моделей).
  • Ключевой практический принцип: откат должен быть возможен на уровне каждого слоя, а не только «откатить всё и молиться».

    ---

    2) Blue‑green: два полностью готовых окружения

    2.1. Суть

    Blue‑green — это когда у вас одновременно живут две версии системы:

  • blue — текущая production версия;
  • green — новая версия, прогрета и проверена.
  • Трафик переключается атомарно (почти мгновенно) с blue на green.

    2.2. Когда это особенно хорошо для ML‑serving

  • Долгий cold start и warmup (LLM/CV на GPU): green можно полностью прогреть заранее.
  • Сильные изменения runtime/зависимостей (например, смена движка инференса): нужен «чистый» новый контур.
  • Требуется максимально быстрый rollback: вернуть трафик на blue — это переключение маршрутизации.
  • 2.3. Операционные нюансы

  • Readiness — gate на трафик. Для green readiness должен зависеть от факта загрузки модели и прогрева (как обсуждалось в материале про runtime lifecycle и probes).
  • Данные и state. Blue‑green проще для stateless serving. Если есть state (jobs, метаданные) — нужно обеспечить совместимость обеих версий с одной и той же БД (см. раздел про migrations).
  • Стоимость. Вы платите ресурсами за два полноценных стека одновременно.
  • 2.4. Типовые ошибки

  • Переключили трафик на green, но забыли, что Ingress/gateway имеет свои таймауты → рост обрывов и ретраев.
  • Прогрели «идеальным» входом, но реальные запросы иные → p99 сразу после переключения.
  • Green использует другую конфигурацию лимитов, но контракт и метрики не разделены → сложно понять, что изменилось.
  • ---

    3) Canary: постепенное увеличение трафика

    3.1. Суть

    Canary — это rollout новой версии на малой доле трафика с постепенным увеличением при отсутствии регрессий.

    3.2. Что измерять, чтобы canary был «автоматизируемым»

    Canary без критериев — это «покатили и посмотрели на глаз». Для ML‑serving обычно достаточно трёх групп сигналов:

  • Latency хвосты (p95/p99) по ключевым endpoint’ам и профилям.
  • Error rate с разбиением на причины (ваши error_code, а не текст исключений).
  • Saturation сигналы: рост очереди/ожидания батча/отказы backpressure.
  • Важно: смотреть метрики в разрезе версии (например, лейбл release или model_version) — иначе вы не отличите проблему canary от фонового шума.

    3.3. Когда canary лучше blue‑green

  • Когда важна экономия ресурсов (нет «двух полных окружений»).
  • Когда риск регрессии качества/поведения модели нужно ловить на реальном трафике постепенно.
  • Когда релиз частый, и команда хочет стандартный, повторяемый процесс.
  • 3.4. Специфика ML‑нагрузки

  • Тяжёлые запросы могут не попасть в canary случайно. Если трафик гетерогенен (короткие/длинные промпты, разные изображения), canary должен либо:
  • 1. учитывать стратификацию (например, по профилю/классу обслуживания), либо 2. иметь достаточно времени/объёма, чтобы увидеть «тяжёлые хвосты».

  • GPU‑сервисы часто ограничены по параллельности. Даже 5% canary может перегрузить отдельную реплику, если распределение трафика неравномерно.
  • ---

    4) Rollback: что именно откатываем и какой ценой

    4.1. Виды откатов (практически полезная классификация)

  • Rollback трафика (blue‑green переключение назад, или остановка canary и возврат веса на старую версию).
  • Rollback кода/образа (вернуть предыдущий image tag/digest).
  • Rollback конфигурации (вернуть значения лимитов/батчинга/профилей).
  • Rollback модели (вернуть активную model_version/артефакты).
  • Rollback схемы БД (самый дорогой и часто нежелательный; см. ниже).
  • Главное: эти откаты должны быть развязаны. Например, модель можно откатить без смены кода, если контракт и runtime совместимы.

    4.2. Быстрый vs «правильный» rollback

    Под инцидентом обычно нужен самый быстрый безопасный рычаг:

  • если горит latency/ошибки на новой версии runtime — откатить трафик или образ;
  • если горит качество/семантика ответа модели — откатить модельную версию;
  • если внезапно «сломали» лимиты/батчинг — откатить конфиг.
  • Отдельная дисциплина: заранее определить, какой сигнал триггерит откат. Примерно:

  • рост p99 выше порога;
  • рост доли internal_error/runtime_error;
  • рост overloaded/timeout (capacity exceeded) после включения новой версии.
  • 4.3. Почему rollback схемы БД — крайняя мера

    Откат БД часто:

  • теряет данные;
  • требует простоя;
  • усложняет синхронизацию нескольких реплик.
  • Поэтому нормальная стратегия — совместимые миграции вперёд и откат на уровне приложения/модели, а не «крутить схему назад».

    ---

    5) Migrations + релиз: как не сломать совместимость при rolling update

    5.1. Основная проблема

    Во время rolling update у вас некоторое время сосуществуют старая и новая версия приложения.

    Следствие: схема БД и данные должны быть совместимы с обеими версиями.

    5.2. Рабочий паттерн: expand → совместимость → contract

    Полный разбор этого подхода и рисков блокировок/индексов — в материале про миграции. Здесь — как это связывается с релизами:

  • Шаг A (expand миграция): добавляем новые поля/таблицы так, чтобы старый код продолжал работать.
  • Шаг B (релиз приложения): новый код начинает писать в новые поля (и часто читает с fallback).
  • Шаг C (backfill): заполняем данные отдельной процедурой, не «одним гигантским UPDATE в миграции».
  • Шаг D (contract миграция): ужесточаем ограничения/удаляем старое, когда уверены, что старая версия не используется.
  • 5.3. Как это сочетается с blue‑green и canary

  • Blue‑green + миграции:
  • 1. применяете expand миграции до переключения; 2. поднимаете green, проверяете совместимость; 3. переключаете трафик; 4. выполняете backfill/contract уже после стабилизации.

  • Canary + миграции:
  • 1. только совместимые изменения схемы до canary; 2. canary версии приложения, которая делает dual‑write; 3. backfill после подтверждения стабильности; 4. contract — только когда canary полностью завершён и старой версии нет.

    5.4. «Релиз миграции» как отдельная сущность

    Практика: считать миграции отдельным релизным артефактом с собственным риском и окном выполнения:

  • миграции применяются управляемо (один исполнитель, блокировки/локи — как обсуждалось ранее);
  • выполнение миграций должно быть наблюдаемым (логи/метрики времени выполнения);
  • «опасные» миграции (индексы на больших таблицах) планируются отдельно от модельных релизов.
  • ---

    6) Как выбрать паттерн: краткая матрица

    | Ситуация | Предпочтительнее | Почему | |---|---|---| | Долгий warmup, нужен мгновенный rollback | Blue‑green | заранее прогрели, переключили/откатили | | Частые релизы, важно экономить ресурсы | Canary | постепенный rollout без двойного стека | | Изменение только модели (артефакты) при совместимом runtime | Canary или быстрый model‑switch | можно катить модель отдельно | | Есть риск несовместимости с БД | Expand/contract + canary | минимизируем окно, где старая версия ломается | | Требуется «почти нулевой риск» для крупного изменения | Blue‑green + canary внутри green | сначала изоляция, потом постепенность |

    ---

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

    1) Вы релизите новую LLM‑модель с другим токенизатором и изменёнными лимитами контекста. Какой паттерн выберете (blue‑green или canary) и какие 3 метрики будут критичны именно для принятия решения “продолжаем/откатываем”?

    2) Опишите план релиза, если одновременно меняются:

  • код сервиса (новая политика батчинга),
  • конфиг лимитов,
  • схема Postgres (добавляется поле в таблицу jobs).
  • Разбейте на шаги так, чтобы rollback был максимально простым.

    3) Сервис в canary на 10% трафика. p95 стабилен, но растёт доля overloaded. Назовите 3 возможные причины и 2 корректирующих действия, не сводя всё к «добавим железа».

    4) Перечислите 5 «артефактов релиза», которые должны иметь версию и быть откатываемыми независимо (подумайте шире, чем только Docker image).

    <details> <summary> Ответы </summary>

    1) Возможный выбор:

  • Если токенизатор и лимиты меняются существенно, а ошибка может проявиться как резкий рост latency/ошибок на реальных запросах — чаще выбирают canary, чтобы увидеть эффект на реальном трафике и не переключать 100% сразу.
  • Если при этом warmup очень долгий и вы боитесь cold‑start пиков при каждом шаге canary, возможен blue‑green (подготовить green полностью), а уже внутри green — canary по клиентам/классам обслуживания.
  • 3 метрики для решения:

  • p99 latency (по профилям/эндпойнтам) — ловит хвосты от новых лимитов и очередей.
  • error rate по error_code (особенно runtime_error, timeout, validation_error) — показывает новые классы сбоев.
  • метрика перегруза/backpressure (overloaded/отказы лимитера, queue_wait) — показывает, что новая версия «съела capacity».
  • 2) Пример безопасного плана:

  • Применить expand‑миграцию БД (новое поле nullable/без строгих ограничений), чтобы старый код не сломался.
  • Деплой кода с canary (или blue‑green) — новая логика батчинга.
  • Отдельно (после стабилизации) включить новые лимиты конфигом, как управляемое изменение (чтобы можно было откатить только конфиг).
  • Выполнить backfill данных (если нужно) отдельной процедурой.
  • Применить contract‑миграцию (ограничения/cleanup) только после завершения rollout и когда старого кода нет.
  • Rollback‑простота:

  • если батчинг ломает latency → откатить трафик/образ;
  • если лимиты ломают поведение → откатить конфиг;
  • миграции назад не делаем, потому что expand шаг совместим.
  • 3) Возможные причины роста overloaded при стабильном p95:

  • Новая версия увеличила “стоимость” запроса (например, больше токенов/больше времени на infer), но среднее ещё не просело.
  • Canary трафик распределяется неравномерно и перегружает часть реплик (горячая реплика).
  • Изменился батчер: выросло ожидание слота/окна, и limiter чаще отказывает, чтобы удержать p95.
  • Действия (не «добавим железа»):

  • Уточнить политику лимитов/стоимости: снизить max_tokens/жёстче ограничить тяжёлые запросы на canary.
  • Перенастроить backpressure/батчинг: уменьшить batch window или max concurrency, чтобы снизить накопление и сделать деградацию предсказуемее; либо изменить правила совместимости батча.
  • 4) Примеры 5 артефактов релиза:

  • Docker image (код сервиса).
  • Helm values/ConfigMap (конфигурация лимитов/батчинга/таймаутов).
  • Модельные артефакты (model_id + model_version + manifest/checksum).
  • Миграции схемы БД (ревизии Alembic) и факт применённой ревизии.
  • Политики маршрутизации трафика (веса canary/правила Ingress/Service selector’ы) — как часть «релизного состояния».
  • </details>

    32. CI pipeline: lint, mypy, tests, build, SBOM, push image

    CI pipeline: lint, mypy, tests, build, SBOM, push image

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

    В курсе уже зафиксированы структура репозитория, quality gates, воспроизводимые зависимости/сборки, тестовые контуры и security scanning. Здесь фокус — как собрать это в один CI pipeline: lint → mypy → tests → build → SBOM → push image.

    ---

    1) Дизайн пайплайна: стадии и инварианты

    Практичная схема стадий выглядит так:

    Инварианты (важно закрепить их как правила проекта, а не «совет»):

  • PR не должен менять окружение молча: любое изменение lockfile — видимый и проверяемый шаг.
  • Контракт API проверяется так же строго, как unit‑логика (contract‑тесты блокирующие).
  • Образ должен собираться из одного источника правды (репозиторий + lockfile).
  • Публикуем только иммутабельные артефакты (не latest), чтобы rollback был воспроизводимым.
  • ---

    2) Lint: что считать «блокирующим»

    Lint в CI решает две задачи:

  • не пропустить очевидные ошибки/антипаттерны;
  • удерживать кодовую базу в едином стиле (чтобы ревью было про смысл).
  • Что важно именно для ML‑serving:

  • Линт должен поддерживать правила импортов/слоёв (из статьи про архитектурные границы и структуру репозитория): например, чтобы domain не импортировал FastAPI/клиенты внешних систем.
  • Линт должен быть детерминированным: одинаковый результат локально и в CI.
  • Линт не должен быть «медленным». Если он тормозит, команда начинает его обходить.
  • Мини‑практика по этапу lint:

  • форматирование и линтинг разделяют на «автофикс» (локально через pre‑commit) и «проверку» (в CI);
  • CI выполняет только проверку и падает, если форматирование отличается.
  • ---

    3) mypy (typecheck): зачем в serving это окупается

    Typechecking в ML‑serving особенно полезен на трёх стыках:

  • Ports/Adapters: контракты интерфейсов (runtime/cache/artifacts/observability) должны быть стабильными.
  • DTO и маппинг: преобразование HTTP DTO → доменные модели → ответ.
  • Settings: типизированная конфигурация (ошибки конфигурации должны падать «fail fast» при старте, а не в проде).
  • Практические правила внедрения mypy:

  • Начните со строгих проверок там, где больше всего «ломается молча»: app/ports, app/use_cases, app/bootstrap/settings.
  • Не пытайтесь сделать «идеально строго везде за один PR». Лучше: строгий режим для ядра + постепенное ужесточение по мере закрытия legacy.
  • Если часть ML‑стека плохо типизирована, держите границу: типы внутри ядра, а в runtime‑адаптере допустимы точечные ослабления.
  • ---

    4) Tests: что запускаем в PR, а что оставляем на релиз/ночные

    Тестовую пирамиду и назначение контуров мы уже разобрали в статье про тестирование. В CI важно зафиксировать порядок и бюджет времени.

    4.1. PR pipeline (блокирующий минимум)

  • Unit: доменные политики, use cases с заглушками портов.
  • Contract: схемы, коды ответов, единый error model, golden‑кейсы.
  • Минимальный integration: старт приложения (app factory + lifecycle) и один «сквозной» happy‑path на тестовом runtime.
  • Почему это достаточно для PR:

  • ловим регрессии контракта и логики до merge;
  • не тащим тяжёлые внешние компоненты и нагрузку в каждый PR.
  • 4.2. Ночные/релизные проверки (не обязательно блокирующие PR)

  • расширенные integration с реальными зависимостями (Redis/Postgres/S3‑эмулятор);
  • load smoke (короткий прогон на фиксированном профиле);
  • security scanning (часто тоже блокирующий, но по отдельной политике severity — см. статью про scanning).
  • ---

    5) Build image: воспроизводимость и «что именно проверяем»

    Сборка образа в CI — это одновременно:

  • проверка, что Dockerfile и зависимости реально работают «внутри контейнера»;
  • подготовка артефакта доставки.
  • Ключевые принципы сборки (не повторяя детали Dockerfile/toolchain):

  • Build должен быть тестируемым: pipeline собирает образ и (минимум) запускает smoke‑команду внутри него, чтобы поймать missing system libs/ошибки entrypoint.
  • Build должен быть повторяемым: установка зависимостей строго по lockfile; базовый образ фиксирован.
  • Build должен быть быстрым: кеширование слоёв и отделение зависимостей от кода (multi‑stage помогает).
  • Что важно измеримо фиксировать в CI:

  • какой git‑коммит соответствует образу;
  • какие зависимости были внутри образа (не «примерно», а конкретный набор);
  • какой Docker digest получился.
  • ---

    6) SBOM: зачем он нужен именно в вашем pipeline

    SBOM (Software Bill of Materials) — это инвентаризация состава артефакта (обычно контейнера): какие компоненты/пакеты вошли в сборку.

    Зачем SBOM в ML‑serving:

  • быстрый ответ на вопрос «где у нас этот уязвимый пакет и в каких релизах»;
  • воспроизводимость расследований (вы знаете состав релизного образа даже спустя время);
  • связка с dependency audit: SBOM — артефакт факта сборки, а не только декларации.
  • Практика хранения SBOM:

  • как CI‑artifact для каждого build;
  • как release‑artifact для main/release (рядом с информацией о digest образа).
  • Важное ограничение: SBOM — это не замена сканированию, а «данные для него» и для аудита.

    ---

    7) Push image: правила тегирования и «что можно публиковать»

    Публикация образа — это место, где легко потерять rollback‑воспроизводимость. Поэтому политика должна быть скучной и строгой.

    7.1. Что публикуем

  • Публикуем только после прохождения quality gates.
  • Публикуем только из доверенного контекста (main/release, а не из случайного PR).
  • 7.2. Как тегировать

    Рекомендуемая минимальная модель:

  • тег по коммиту (например, SHA) — для точного отката;
  • опционально «человеческий» тег релиза (version) — для процессов релиз‑менеджмента.
  • Важно: в эксплуатационных системах «источник правды» лучше считать digest, а тег — удобным алиасом.

    7.3. Безопасность публикации

  • секреты registry должны жить в CI secret store;
  • CI не должен печатать токены в логах;
  • права токена должны быть минимальными (push только в нужный репозиторий).
  • ---

    8) Частые ошибки CI для ML‑serving (и почему они дорогие)

  • Смешали быстрые и долгие проверки: каждый PR запускает тяжёлую интеграцию/нагрузку → команда начинает отключать проверки.
  • Нет разделения PR vs release: публикуются образы из PR → невозможно контролировать происхождение артефакта.
  • SBOM не сохраняется как артефакт: вы «генерировали», но потом не можете ответить, что реально было в релизе.
  • Контракт API не блокирующий: breaking change обнаруживается клиентами.
  • Build без smoke‑запуска: missing OS libs/entrypoint ошибки всплывают только при деплое.
  • ---

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

    1) Составьте схему стадий для двух пайплайнов: PR и release. Укажите:

  • какие стадии блокирующие;
  • какие можно вынести в nightly;
  • где появляется SBOM.
  • 2) Опишите политику тегирования образов (минимум 3 правила), которая обеспечивает воспроизводимый rollback.

    3) Вы заметили, что mypy стал «вечно красным» из‑за ML‑runtime модуля. Предложите 3 шага, как вернуть пользу typecheck’а, не отключая его полностью.

    4) Назовите 5 типичных причин, почему сборка образа проходит, а контейнер падает при запуске (в контексте ML‑serving). Для каждой причины — по одному способу поймать это в CI.

    <details> <summary>

    Ответы

    </summary>

    1) Пример схемы.

    PR:

  • Lint/format check — блокирующий.
  • mypy — блокирующий (хотя бы для ядра).
  • Unit + contract + минимальный integration — блокирующие.
  • Build image (без push) — блокирующий или «почти блокирующий» (часто да, чтобы не ломать main).
  • SBOM — генерируем и сохраняем как artifact.
  • Release:

  • Повторяем все PR‑гейты.
  • Build image (release) — блокирующий.
  • SBOM (release) — сохраняем как release‑artifact.
  • Push image — только после успешных шагов.
  • Nightly:

  • расширенные integration (с Redis/Postgres/S3‑эмулятором);
  • load smoke;
  • расширенные security scanning задачи (если это слишком дорого для каждого PR).
  • 2) Пример политики тегирования.

  • Каждый релизный образ имеет тег, однозначно связанный с коммитом (например, SHA), чтобы всегда можно было вернуться к конкретному состоянию кода.
  • “Человеческий” тег версии (например, semver) — алиас на тот же digest и меняется только через релизный процесс.
  • Запрещены плавающие теги как источник правды (например, latest), чтобы rollback не превращался в «угадай, что там было».
  • 3) Как вернуть пользу mypy, если runtime плохо типизирован.

  • Ограничить строгий режим на app/ports, app/use_cases, app/bootstrap (ядро), а для runtime‑адаптера временно ослабить требования (точечно).
  • Ввести отдельную цель/конфиг: “mypy‑core” как блокирующий, “mypy‑full” как nightly, чтобы не останавливать команду.
  • Стабилизировать типовые точки интеграции: на границе runtime возвращать строго типизированные доменные структуры, а не «любой объект».
  • 4) Причины «build ок, run падает» и как ловить.

  • Не хватает системной библиотеки в runtime‑образе (часто CV decode / бинарные wheels) → ловить: smoke‑запуск контейнера в CI + минимальный импорт/инициализация.
  • Неверный CMD/entrypoint или отсутствует модуль запуска → ловить: запуск контейнера командой по умолчанию в CI и проверка выхода/логов.
  • Ошибка конфигурации (обязательная env переменная не задана) → ловить: запуск контейнера с минимальным набором env, ожидаем fail fast с понятной ошибкой.
  • Проблемы прав non‑root (нет доступа к writable директории, кешу артефактов) → ловить: запуск под non‑root в CI и простая операция записи в нужный каталог.
  • Миграции/инициализация ожидают внешнюю зависимость (Postgres/Redis), но в runtime окружении её нет → ловить: integration тест через Compose или тестовый стенд, плюс явные таймауты старта.
  • </details>

    33. CD pipeline: deploy to Kubernetes, approvals, environments

    CD pipeline: deploy to Kubernetes, approvals, environments

    CD (Continuous Delivery/Deployment) — это процесс, который берёт проверенный артефакт (Docker image + конфиг/Helm values) и предсказуемо доводит его до нужного Kubernetes‑окружения, сохраняя контроль: кто разрешил, что именно выкатили, как быстро откатить.

    В CI мы уже получили доверие к коду (линт/типы/тесты/сборка/SBOM). CD отвечает за «доставку» и операционный контроль. Про Kubernetes/Helm, probes, релиз‑паттерны и миграции уже говорили — здесь фокус на пайплайне выкладки, approval gates и окружениях.

    ---

    1) Окружения (environments) как контракт поставки

    Типичная модель: dev → stage → prod.

  • Dev
  • 1. цель: скорость; допускаются частые выкладки 2. источник изменений: любые merge в main (или даже feature branches) 3. требования: минимальные, но воспроизводимые

  • Stage
  • 1. цель: максимально похожее на prod поведение 2. источник: promotion артефакта из dev (не пересборка) 3. требования: smoke/integration проверки, совместимость миграций, наблюдаемость

  • Prod
  • 1. цель: выполнение SLO и управляемость рисков 2. источник: promotion из stage 3. требования: approvals, строгая политика тегов/дижестов, контролируемая стратегия rollout

    Ключевая дисциплина: между окружениями “переезжает” один и тот же артефакт, а не “собрали заново, но вроде то же самое”. В практике это означает:

  • image фиксируется по digest (или по commit‑tag, но digest — источник правды)
  • Helm chart version фиксирован
  • различия окружений — только в values-*.yaml (лимиты, ресурсы, внешние интеграции)
  • ---

    2) Два подхода к CD: push‑based и GitOps

    2.1. Push‑based (CI/CD “толкает” в кластер)

    CD‑джоб получает доступ к Kubernetes и выполняет helm upgrade/kubectl apply.

    Плюсы:

  • проще стартовать
  • явно видно в пайплайне “деплой выполнен здесь и сейчас”
  • Минусы:

  • секреты доступа к кластеру живут в CI и требуют строгой защиты
  • сложнее гарантировать отсутствие drift (ручных правок в кластере)
  • 2.2. GitOps (кластер “тянет” состояние из Git)

    CI публикует артефакт (image) и обновляет декларативное состояние (например, values/manifest в репозитории окружения). В кластере агент синхронизирует состояние.

    Плюсы:

  • Git становится журналом: «что и когда выкатили»
  • drift проще контролировать
  • Минусы:

  • выше порог входа (доп. компонент и дисциплина репозиториев)
  • Выбор — организационный. Важно не название подхода, а инвариант: деплой должен быть воспроизводимым и наблюдаемым.

    ---

    3) Как выглядит “скелет” CD пайплайна

    Ниже — логика стадий (не привязка к конкретной системе CI).

    Практическое правило для ML‑serving: стадия “wait rollout” должна учитывать долгий startup/warmup и корректные probes, иначе CD будет считать “успешно”, когда pod’ы ещё не готовы.

    ---

    4) Approvals: где они нужны и что именно утверждаем

    Approval — это не “формальность”, а контроль риска. Обычно approvals включают:

  • Прод (prod) — всегда
  • Стейдж — часто (если стейдж шарится между командами или если стоимость высокая — GPU)
  • Что именно утверждают:

  • артефакт: image digest, chart version
  • дифф конфигурации: изменения лимитов, ресурсов, стратегий rollout
  • изменения модели: model_version/артефакты, если это часть конфигурации
  • миграции: факт, что применены безопасно (см. expand/contract из материала про миграции)
  • Рекомендация по дисциплине approvals:

  • approval должен быть привязан к конкретному commit/digest
  • approval должен быть повторяемым: тот же ввод → тот же деплой
  • Антипаттерн: “аппрувим выкладку”, но деплой берёт latest или неприкреплённый тег.

    ---

    5) Environment protection: границы доступа и ролей

    CD в Kubernetes всегда упирается в вопрос: кто имеет право менять prod.

    Минимальный набор практик:

  • разные Kubernetes credentials для dev/stage/prod
  • RBAC по namespace: CD‑джоб не должен иметь доступ “cluster‑admin”
  • read‑only доступ для просмотра (логов/статусов) можно дать шире, чем write‑доступ
  • запрет ручных правок как политика (или хотя бы аудит)
  • Если CD push‑based, секрет доступа к кластеру — критичный актив:

  • ограничить scope (namespace, ресурсы)
  • ограничить время жизни (если возможно)
  • исключить вывод kubeconfig/токенов в логи
  • ---

    6) Деплой в Kubernetes: что значит “успешно”

    Успех деплоя в production — это не “apply прошёл”, а последовательность условий:

  • манифесты применились (Helm/kubectl без ошибок)
  • rollout завершился
  • 1. Deployment обновился 2. новые pod’ы стали ready 3. старые pod’ы корректно завершились
  • post-deploy verification (минимум):
  • 1. /health/ready для новой ревизии 2. отсутствие всплеска 5xx / internal_error / runtime_error 3. отсутствие лавины overloaded (если это не запланированный эффект)

    Важно: healthchecks и probes уже разобраны отдельно; для CD главное — ждать именно readiness, а не просто “контейнер запущен”.

    Практика для Helm‑деплоев:

  • использовать режим, который ждёт готовности и умеет прерывать деплой при провале
  • фиксировать timeout с запасом на cold start (особенно GPU)
  • ---

    7) Promotion: как продвигать релиз между окружениями

    Promotion — это перенос того же релиза по цепочке.

    7.1. Что считаем релизом

    Минимальный “пакет” релиза:

  • image digest
  • chart version
  • values для окружения (или набор параметров, из которых они собираются)
  • 7.2. Чем отличается от “пересобрать и выкатить”

  • пересборка создаёт новый артефакт → другой риск
  • promotion сохраняет трассируемость: “тот же артефакт прошёл стейдж”
  • ---

    8) Пост‑деплой проверки (verification) именно для ML‑serving

    Помимо общих healthchecks, для ML‑serving важно подтвердить, что runtime реально живой:

  • минимальный smoke‑запрос (очень лёгкий) — чтобы поймать отсутствие системных libs/ошибки runtime
  • проверка “служебных” сигналов:
  • 1. рост inference_queue_wait (если есть) 2. рост timeout/runtime_error

    Смысл: ML‑сервис может быть ready по HTTP, но фактически “ломаться” на первой же попытке инференса (например, не загрузились артефакты/плохая совместимость).

    Чтобы не повторять полноценные нагрузочные тесты, verification в CD обычно:

  • короткое
  • фиксированное
  • имеет строгие таймауты
  • ---

    9) Откат в CD: что откатываем “кнопкой”

    Откат должен быть описан как операция CD, а не “ручной kubectl”. В реальности вы откатываете один из слоёв:

  • код/образ: вернуть предыдущий digest
  • конфиг: вернуть предыдущие values
  • модель: вернуть model_version (если модель переключается конфигом)
  • Про стратегии blue‑green/canary и взаимодействие с миграциями мы уже говорили в статье про релиз‑паттерны — в CD важно, чтобы pipeline умел:

  • быстро вернуться к известной “хорошей” ревизии
  • не пытаться откатывать схему БД “автоматом”, если это не часть вашей безопасной политики
  • ---

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

    1) Составьте (текстом) минимальный “release manifest” — список полей, которые однозначно описывают релиз ML‑serving сервиса для promotion между окружениями.

    2) Опишите политику approvals для prod:

  • какие 3 типа изменений требуют обязательного ручного подтверждения
  • какие 2 изменения вы бы разрешили выкатывать без ручного approval (и почему)
  • 3) Придумайте 6 post‑deploy проверок для ML‑serving (коротких), разделив их на:

  • проверки доступности
  • проверки корректности runtime
  • проверки “перегруза”
  • 4) Вам нужно выбрать push‑based или GitOps для CD. Назовите 3 критерия выбора и какое решение вы примете для команды из 3 инженеров, где нет выделенного SRE.

    <details> <summary>

    Ответы

    </summary>

    1) Пример release manifest:

  • service_name
  • image_digest (источник правды)
  • chart_name
  • chart_version
  • values_version или ссылка на конкретный commit файла values-prod.yaml
  • config_hash (опционально, но полезно для сравнения)
  • model_id + model_version (если модель переключается конфигом)
  • deployed_at, deployed_by (для аудита)
  • 2) Пример approvals для prod.

    Требуют ручного подтверждения:

  • смена image digest (новый код)
  • изменения runtime‑опасных параметров: max_concurrency, batch_window, лимиты входов/выходов
  • смена model_version (особенно если меняются артефакты/токенизатор/размеры)
  • Можно без ручного approval (пример, зависит от вашей культуры):

  • правки аннотаций/лейблов, не влияющие на поведение (операционные метки)
  • изменение только dev‑окружения (если dev не имеет SLO и служит песочницей)
  • 3) Пример post‑deploy проверок.

    Доступность:

  • Deployment rollout завершён (все replicas ready)
  • /health/ready отвечает 200 у новой ревизии
  • Корректность runtime:

  • лёгкий smoke‑запрос на inference endpoint (минимальный вход, строгий timeout)
  • проверка, что сервис возвращает ожидаемые метаданные (например, model_version в ответе соответствует конфигу)
  • Перегруз:

  • доля 5xx не растёт выше порога за короткое окно
  • не появляется лавинообразный overloaded/timeout (если это не ожидаемый эффект новой конфигурации)
  • 4) Критерии выбора push‑based vs GitOps:

  • кто будет владеть инфраструктурой и разбираться с агентом/синхронизацией
  • насколько важен audit trail “Git как источник правды”
  • насколько команда готова поддерживать дополнительную сущность (репозиторий окружений, правила синхронизации)
  • Для команды из 3 инженеров без выделенного SRE часто разумнее начать с push‑based (проще операционно), но сразу:

  • жёстко ограничить RBAC
  • зафиксировать артефакт по digest
  • сделать понятные approvals для prod
  • </details>

    34. Нагрузочное тестирование: k6/Locust сценарии и SLO-отчёты

    Нагрузочное тестирование: k6/Locust сценарии и SLO‑отчёты

    Нагрузочное тестирование в production ML‑serving нужно не для «максимального RPS любой ценой», а чтобы подтвердить:

  • сервис выдерживает целевые режимы нагрузки внутри SLO;
  • backpressure и деградация срабатывают контролируемо (429/503, ваши error_code);
  • хвосты p95/p99 не «взрываются» из‑за очереди/батчинга/препроцессинга/рантайма.
  • Определения SLI/SLO, метрики, гистограммы и error budget уже были разобраны ранее (см. статьи про NFR/SLO и про Prometheus histograms). Здесь фокус: как писать сценарии в k6/Locust и как выпускать SLO‑отчёт по результатам прогона.

    ---

    1) Когда нужен k6, а когда Locust

    k6 (лучше для “SLO‑гейта”)

    Подходит, когда вам нужно:

  • быстро и детерминированно прогонять фиксированные профили нагрузки;
  • использовать встроенные thresholds (pass/fail) как «ворота» релиза;
  • удобно отдавать отчёт в CI (JSON/summary) и хранить baseline.
  • Сильные стороны: простые сценарии, хорошие thresholds, удобен для автоматизации.

    Locust (лучше для сложного пользовательского поведения)

    Подходит, когда важно:

  • описывать сложные пользовательские потоки (несколько endpoint’ов, branching);
  • управлять поведением «пользователей» (think time, случайные переходы);
  • интерактивно наблюдать прогон (UI), делать exploratory нагрузку.
  • Сильные стороны: гибкая модель «virtual users» и сценариев.

    Практика для ML‑serving: часто делают k6 как обязательный SLO‑smoke (регулярно/в релизе) и Locust для сложных user‑journey и экспериментов.

    ---

    2) Подготовка прогона: что фиксируем, чтобы результаты были сравнимы

    Чтобы нагрузка была инженерным инструментом, а не «один раз померили», фиксируйте:

  • версию артефакта (image digest/commit SHA, model_version);
  • профиль запроса (например, fast/quality, batch on/off, streaming on/off);
  • лимиты входа (длина текста/размер изображений), иначе сравнение бессмысленно;
  • окружение (CPU/GPU, количество реплик, лимиты ресурсов, cold/warm состояние);
  • критерии остановки: что считаем fail (p99, error rate, overloaded, timeouts).
  • Отдельный принцип: прогрев. Если у сервиса есть warmup на старте, в нагрузке обычно делают:

  • warmup‑фазу (не считать в отчёт);
  • steady‑state фазу (считать);
  • short burst (проверить backpressure).
  • ---

    3) Дизайн сценариев: что именно моделируем в ML‑serving

    3.1. Базовые типы прогонов

  • Capacity within SLO (steady): держим заданный RPS/конкурентность и проверяем p95/p99 + error budget.
  • Burst / spike: короткий скачок (2–10×), чтобы увидеть:
  • 1) рост очереди/ожидания, 2) появление 429/503, 3) отсутствие лавины 5xx.
  • Soak / stability (дольше): ловит утечки памяти, деградацию со временем, фрагментацию.
  • 3.2. Разделение по endpoint’ам и режимам

    Не смешивайте в одном «главном» тесте разные режимы (это ломает выводы):

  • /v1/embeddings (короткий) — отдельный профиль.
  • /v1/generate sync — отдельный профиль.
  • /v1/generate:stream — отдельный профиль (другие метрики: TTFT, completion).
  • /v1/jobs + GET /jobs/{id} — отдельный профиль (job enqueue vs completion).
  • 3.3. Наборы данных (payload sets)

    Для LLM/CV особенно важно тестировать распределение входов:

  • small / medium / near‑limit;
  • «плохие входы» (пустой текст, слишком большой payload, неверные поля) — чтобы убедиться, что отказ быстрый и контролируемый;
  • повторяемые входы (для проверки кеша и дедупликации).
  • Фиксируйте наборы как артефакт (например, tests/fixtures/load/…), чтобы сравнивать прогоны.

    ---

    4) k6: структура сценариев и thresholds

    4.1. Типовая структура сценария (понятия)

    В k6 вы обычно описываете:

  • stages (ramp‑up → steady → ramp‑down);
  • VUs (виртуальные пользователи) или rate‑модель;
  • checks (функциональная корректность ответа);
  • thresholds (SLO‑гейт).
  • 4.2. Что проверять в checks

    Минимальные проверки для inference:

  • HTTP код соответствует ожиданию (200/202/429/413/422);
  • ответ соответствует контракту (ключевые поля: request_id, model, result или error_code);
  • для ошибок: error_code принадлежит ожидаемому множеству (например, overloaded, validation_error, input_too_large).
  • 4.3. Thresholds как SLO‑гейт

    Thresholds должны отражать ваш SLO для конкретного режима. Типовой набор:

  • p95/p99 на уровне HTTP (или отдельной кастомной метрики),
  • error rate (часто отдельно: 5xx и 4xx),
  • доля overloaded — как сигнал capacity exceeded (по вашей политике: либо часть SLO, либо отдельный лимит).
  • Важно: thresholds должны быть «скучными» и стабильными. Любая смена thresholds — это изменение эксплуатационного контракта и должна ревьюиться как изменение лимитов/батчинга.

    ---

    5) Locust: сценарии “пользовательских” потоков

    Locust удобен, когда вы хотите моделировать реальный паттерн:

  • часть трафика идёт в /embeddings, часть в /generate;
  • разные классы обслуживания (например, fast и quality) с разной долей;
  • для async: POST /jobs → polling GET /jobs/{id} до succeeded/failed.
  • Практический совет: в Locust легко случайно сделать тест «про клиента», а не «про сервис».

    Чтобы не исказить результаты:

  • задайте таймауты HTTP‑клиента и явные ретраи (или их отсутствие);
  • следите за нагрузкой на сторону генератора (Locust сам может стать bottleneck);
  • фиксируйте seed/наборы данных, иначе распределение входов «плавает».
  • ---

    6) SLO‑отчёт: что должно быть в результате прогона

    SLO‑отчёт — это короткий артефакт, который отвечает: проходим/не проходим и почему.

    6.1. Минимальный шаблон отчёта

    | Раздел | Метрика | Цель | Факт | Verdict | |---|---|---:|---:|---| | Load profile | RPS / concurrency | фиксировано | фиксировано | pass/fail | | Latency | p95 (endpoint/profile) | ≤ X ms/s | Y | pass/fail | | Latency | p99 (endpoint/profile) | ≤ X ms/s | Y | pass/fail | | Errors | 5xx rate | ≤ A% | B% | pass/fail | | Backpressure | overloaded rate | ≤ C% (или отдельный лимит) | D% | pass/fail | | Correctness | schema / required fields | 100% | … | pass/fail |

    6.2. Откуда брать «факт»

    Есть два источника:

  • клиентская сторона (k6/Locust): latency и коды ответов.
  • серверная сторона (Prometheus/логи): причины (error_code), queue_wait, runtime infer time.
  • Если отчёт строится только на клиентских метриках, вы увидите «что плохо», но хуже поймёте «почему». Поэтому для production‑готового процесса обычно делают:

  • k6/Locust как генератор + итог по latency/error;
  • параллельно снимают server‑side метрики по стадиям (см. статью про Prometheus histograms) для диагностики.
  • 6.3. Как интерпретировать типовые провалы

  • p99 вырос, а 5xx нет → часто очередь/батчер/конкурентность или препроцессинг.
  • вырос overloaded, а p95 норм → сервис честно защищается, но capacity ниже ожиданий (или перекос трафика по репликам).
  • выросли 422/413 → тестовые данные вышли за лимиты или контракт поменялся.
  • ---

    7) Частые ошибки нагрузочного тестирования ML‑serving

  • Не фиксируют входы: каждый прогон «другая модель нагрузки».
  • Смешивают режимы (sync + async + streaming) и делают неинтерпретируемый отчёт.
  • Тестируют «максимум RPS» вместо «максимум внутри SLO».
  • Не проверяют формат ошибок: потом клиенты ломаются на изменениях error_code/details.
  • Генератор становится bottleneck (особенно на больших payload’ах и TLS).
  • ---

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

    1) Спроектируйте два load‑сценария для вашего сервиса:

  • steady‑state на 5 минут,
  • burst на 60 секунд.
  • Для каждого укажите: endpoint, профиль, ограничения входа, критерии fail.

    2) Для endpoint /v1/generate (sync) сформулируйте thresholds как SLO‑гейт:

  • по latency (p95/p99),
  • по 5xx,
  • по overloaded.
  • 3) Придумайте набор тестовых данных (payload sets) из 6 элементов:

  • 2 «малых» запроса,
  • 2 «средних»,
  • 2 «на границе лимита».
  • Для каждого опишите, какой риск он ловит.

    4) У вас в отчёте: p95 норм, p99 ухудшился, 5xx почти нет, но overloaded вырос. Назовите 3 гипотезы причины и 2 корректирующих действия (не сводя к «добавить железо»).

    <details> <summary>

    Ответы

    </summary>

    1) Пример.

  • Steady:
  • 1. Endpoint: /v1/embeddings 2. Профиль: fast 3. Ограничения: 1–16 строк, каждая ≤ 2000 символов 4. Fail: p99 > 200 мс, 5xx > 0.1%, overloaded > 0.5%

  • Burst:
  • 1. Endpoint: /v1/embeddings 2. Профиль: fast 3. Нагрузка: 60 секунд 5× от steady 4. Fail: лавина 5xx, отсутствие контролируемых 429/503 при перегрузе (то есть вместо backpressure — таймауты/500), резкий рост p99 до «таймаутной стены».

    2) Пример thresholds для /v1/generate (sync) при ограничениях входа (например, prompt ≤ N символов, max_tokens ≤ M):

  • Latency: p95 ≤ 2.5s, p99 ≤ 5s
  • Errors: 5xx rate ≤ 0.2%
  • Overload/backpressure: overloaded ≤ 1% (или отдельный лимит, если по вашей SLO 429 считаются «ожидаемой деградацией»)
  • 3) Пример payload sets:

  • Малые:
  • 1) короткий текст (проверка базовой latency) 2) короткий повторяющийся текст (проверка кеша/дедупа)

  • Средние:
  • 3) типичный продовый размер (реалистичная нагрузка) 4) типичный, но с включённой опцией (проверка политики профиля)

  • Near‑limit:
  • 5) вход близко к лимиту по длине (ловит рост препроцессинга и worst‑case runtime) 6) вход близко к лимиту + максимальные допустимые параметры генерации (ловит риск OOM/таймаутов и корректность ограничения)

    4) Пример гипотез и действий.

    Гипотезы:

  • Нагрузка распределяется неравномерно: часть реплик перегружена, limiter чаще отказывает, при этом средняя latency у «успевших» запросов нормальная.
  • Параметры батчинга/очереди создают “длинный хвост”: часть запросов ждёт слот слишком долго и получает backpressure.
  • “Тяжёлые” запросы (near‑limit) попали в непропорционально большую долю трафика на canary/часть реплик.
  • Действия:

  • Ужесточить/стратифицировать политику: отдельные лимиты/очереди для тяжёлых запросов, запрет смешивания классов (например, по длине) в одном батче.
  • Перенастроить лимитеры: снизить max concurrency, уменьшить batch window (если это рост ожидания), или изменить правила приоритизации, чтобы тяжёлые запросы не забивали очередь.
  • </details>

    35. Профилирование: py-spy, scalene, flamegraphs, hot paths

    Профилирование: py-spy, scalene, flamegraphs, hot paths

    Профилирование — это инструмент локализации узких мест (hot paths) в реальном коде сервиса: где тратится CPU, где растёт память, где появляются блокировки и «хвосты» p95/p99. Метрики и трейсы (см. статьи про Prometheus histograms и OpenTelemetry) показывают что деградировало и на какой стадии, а профилировщик отвечает почему именно эта стадия стала медленной: конкретные функции/стек вызовов, аллокации, удержание GIL.

    Ниже — практическая дисциплина профилирования для production ML-serving на FastAPI: py-spy (быстрое sampling CPU), scalene (CPU+memory профили), flamegraphs и методика поиска hot paths.

    ---

    1) Что такое hot path в ML-serving (и почему «профилировать всё» бесполезно)

    Hot path — участок кода, который доминирует по времени/ресурсам в вашем целевом режиме нагрузки.

    Типовые hot paths в serving:

  • CPU препроцессинг
  • 1. токенизация (LLM) 2. декодирование/resize изображений (CV) 3. сериализация/десериализация больших payload
  • Очередь/лимитер/батчер
  • 1. ожидание слота конкурентности 2. ожидание окна micro-batch (это часто видно в метриках как рост queue_wait, но профилировка помогает понять, какой код держит слот или почему event loop блокируется)
  • Питоновская обвязка вокруг runtime
  • 1. конвертация данных (bytes → numpy/torch tensors) 2. постпроцессинг (нормализация, ранжирование, фильтрация)
  • Память (RSS / утечки / фрагментация)
  • 1. рост кэшей 2. накопление буферов 3. удержание больших объектов между запросами

    Ключевой принцип: профилирование делается под конкретный сценарий нагрузки, иначе вы найдёте «hot path» не там, где горит p99.

    ---

    2) Профилирование в production-контуре: правила безопасности и воспроизводимости

    Перед инструментами — дисциплина (чтобы не навредить сервису и не получить мусорные выводы):

  • Сначала сузьте область
  • 1. по метрикам/трейсам определите стадию деградации (например, preprocess или queue_wait) 2. выберите один endpoint + профиль (fast/quality) + тип входов
  • Ограничьте окно
  • 1. профилируйте 10–60 секунд, а не «часами» 2. делайте несколько повторов
  • Профилируйте ту же модель конкурентности
  • 1. важно понимать: один pod может иметь несколько процессов (workers) 2. CPU hot path может быть в одном воркере, а p99 — из-за конкуренции между воркерами
  • Не включайте тяжёлую диагностику навсегда
  • 1. profiling — краткосрочная операция 2. результат — артефакт (файл профиля), а не постоянный режим

    Практика для продакшна: профилировать либо на stage, либо на выделенной canary-реплике с контролируемым трафиком.

    ---

    3) py-spy: быстрый sampling CPU-профайлер (attach к работающему процессу)

    3.1. Почему py-spy — базовый инструмент для serving

    py-spy хорош тем, что:

  • умеет подключаться к уже запущенному процессу Python (без изменения кода)
  • работает как sampling профайлер → обычно приемлемый overhead
  • даёт понятные режимы:
  • 1. «кто сейчас ест CPU» (top) 2. запись профиля для последующего анализа

    Это особенно полезно для incident-style диагностики: «в проде вырос p99, подозреваем токенизацию/декод».

    3.2. Что именно смотреть в выводе

    В py-spy top (или аналогичном режиме) вам важны:

  • функции с высоким self time (время в самой функции)
  • функции с высоким total time (время вместе с вызовами)
  • наличие большого числа стэков, «застрявших» в одном месте (симптом блокировки/сериализации)
  • Отдельный класс проблем: если большую долю времени занимает что-то вроде ожидания/сна/синхронизации — это намекает на блокировки, конкурентность или очередь.

    3.3. Multi-worker и контейнеры: как не ошибиться с PID

    В ML-serving часто:

  • несколько процессов (ASGI workers)
  • каждый процесс может иметь разное поведение
  • Практический шаблон:

  • сначала определите, какой именно процесс обслуживает «медленный» трафик (через логи с request_id, метрики, либо просто профилируйте несколько воркеров)
  • профилируйте несколько PID и сравните
  • В контейнере обычно нужно подключаться внутри контейнера (или иметь права/возможность attach к PID в namespace). Это организационный вопрос окружения, но технически важно помнить: «PID на хосте» и «PID в контейнере» могут отличаться.

    ---

    4) Flamegraphs: как читать «карту времени»

    Flamegraph — это визуальное представление профиля стека вызовов.

    Важно: в инфраструктуре могут использоваться разные форматы вывода (часто это файл профиля, который потом открывают в viewer’е). В рамках курса принцип один: вы превращаете профиль в дерево стеков, где ширина = доля времени.

    4.1. Как интерпретировать flamegraph (без религии)

    Смотрите на:

  • самые широкие блоки — кандидаты в hot path
  • глубину стека
  • 1. глубоко внизу — «корневая причина» (часто библиотечная функция) 2. выше — ваша обвязка (где можно оптимизировать быстрее)
  • повторяющиеся паттерны
  • 1. много одинаковых стеков → стабильный bottleneck 2. «размазанные» стеки → возможно, разные классы запросов смешаны

    4.2. Типичные «плохие» находки для serving

  • большая доля времени в JSON сериализации/парсинге
  • большая доля в преобразованиях данных (копирования, encode/decode)
  • heavy Python loops в постпроцессинге
  • блокировки вокруг shared state (например, глобальный lock, очередь без нужной политики)
  • Ниже — текстовая схема того, как выглядят разные случаи:

    ---

    5) Scalene: CPU + memory профилирование (и чем оно отличается)

    scalene полезен, когда проблема не только в CPU, но и в памяти:

  • рост RSS со временем
  • частые аллокации
  • неожиданные копирования
  • деградация из-за GC/фрагментации
  • Ключевые отличия от «чистого CPU sampling»:

  • вы получаете разделение времени на Python vs native (часто критично в ML-стеке, где часть времени уходит в C/C++ библиотеки)
  • вы видите memory hotspots: где аллоцируется память и какие строки/функции её создают
  • 5.1. Когда Scalene применять осторожно

  • на высоком RPS в проде — может быть тяжелее, чем py-spy
  • при multi-process — нужно понимать, как запускается профилирование (по процессам)
  • Практика: Scalene чаще используют на stage/локально, воспроизводя «плохой» сценарий нагрузки (см. статью про load smoke / k6/Locust).

    ---

    6) Методика: как найти bottleneck за 30–60 минут (итеративно)

  • Зафиксируйте симптом и условия
  • 1. какой endpoint/профиль 2. какие входы (small/medium/near-limit) 3. какой режим (sync/async/streaming)
  • Сопоставьте со стадиями из метрик/трейсов
  • 1. растёт http_request_duration? 2. растёт inference_duration? 3. растёт queue_wait?
  • Снимите py-spy профили (несколько коротких)
  • 1. один — при нормальной нагрузке 2. один — при деградации
  • Постройте flamegraph и найдите 1–2 top hot paths
  • Проверьте гипотезу micro-экспериментом
  • 1. отключите/упростите постпроцессинг 2. измените лимит/батчинг 3. временно переключите профиль
  • Если есть подозрение на память — подключите Scalene или отдельное memory профилирование
  • Цель итерации — не «идеальный профиль», а конкретное решение:

  • что оптимизировать (и где)
  • как проверить эффект (через те же метрики/SLO, что вы уже определили)
  • ---

    7) Частые ловушки профилирования FastAPI/ML-serving

  • Профилируют без нагрузки → hot path не тот (в проде очередь и конкурентность меняют картину)
  • Смешивают разные классы запросов → flamegraph «размазан», выводы нерелевантны
  • Путают процессы → профилируют «idle worker», пока проблема в другом
  • Не отделяют Python и native → обвиняют «не ту» часть пайплайна
  • Оптимизируют не то
  • 1. чинят 2% времени, игнорируя 60% ожидания очереди 2. «ускоряют модель», когда bottleneck — декодирование

    ---

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

    1) У вас вырос p99 на /v1/embeddings, но inference_duration почти не изменился, а http_request_duration вырос. Назовите 3 наиболее вероятных класса причин (не конкретные функции) и для каждого — какой сигнал в профиле вы ожидаете увидеть.

    2) Вы запускаете сервис с несколькими воркерами (процессами). Опишите пошагово, как вы убедитесь, что профилируете именно «проблемный» воркер.

    3) По flamegraph видно, что 40% времени уходит в сериализацию/десериализацию. Назовите 4 практических направления оптимизации в serving-контуре (не «переписать всё на Rust») и какие риски/компромиссы у каждого.

    4) Когда предпочтительнее py-spy, а когда scalene? Дайте по 3 критерия выбора.

    <details> <summary> Ответы </summary>

    1) Возможные причины и ожидания:

  • Очередь/лимитер/батчер: в CPU профиле будет значимая доля времени в функциях ожидания/синхронизации (например, ожидание семафора, ожидание батч-окна), а не в вычислениях модели.
  • Проблемы event loop / блокирующий код в async пути: в профиле будут видны блокирующие операции в горячем пути (системные вызовы, тяжёлые Python-функции до/после инференса), часто повторяющиеся у многих запросов.
  • Сериализация/парсинг/валидация больших данных: в профиле доминируют функции JSON encode/decode, преобразования типов, копирования буферов.
  • 2) Как найти «проблемный» воркер:

  • Подтвердить, что деградация воспроизводится (фиксированный сценарий нагрузки).
  • Посмотреть процессы воркеров и их загрузку (CPU/time) и выбрать кандидатов.
  • Снять короткий py-spy профиль с каждого кандидата (или с 2–3 воркеров) и сравнить: у проблемного будет выраженный hot path, соответствующий деградации.
  • Повторить в момент, когда метрики показывают пик (плохое окно), а не «в среднем по больнице».
  • 3) Направления оптимизации сериализации:

  • Уменьшить размер ответа/запроса (ограничения, более компактные структуры) — риск: влияние на контракт API, нужен контроль backward compatibility.
  • Убрать лишние поля/отладочные данные из ответа — риск: потеря диагностики, нужно компенсировать метриками/логами.
  • Перейти на более эффективные форматы/кодеки внутри системы (например, для внутренних сервисов) — риск: усложнение интеграций и инфраструктуры.
  • Оптимизировать работу с бинарными данными (не перекодировать туда-сюда; не носить base64 там, где можно иначе) — риск: изменения транспорта и требований к клиентам.
  • 4) py-spy vs scalene:

    py-spy предпочтительнее, когда:

  • нужно быстро подключиться к работающему процессу без изменения кода;
  • задача — найти CPU hot path/стек вызовов;
  • важен минимальный overhead и короткая диагностика.
  • scalene предпочтительнее, когда:

  • есть симптомы по памяти (рост RSS, частые аллокации, GC-паузы);
  • нужно разделить время Python vs native и увидеть, где именно «утекает»;
  • вы можете воспроизвести проблему на stage/в тестовом окружении и готовы к более тяжёлому профилированию.
  • </details>

    36. Полный пример сервиса: обзор функционала и API спецификация

    Полный пример сервиса: обзор функционала и API спецификация

    Эта статья — «паспорт» полного референс‑сервиса из курса: что он умеет, какие режимы инференса поддерживает и как выглядит его публичный API‑контракт. Подробные принципы проектирования контрактов, версионирование, единый формат ошибок, observability и режимы sync/async/streaming уже разобраны в соответствующих статьях курса — здесь мы фиксируем конкретную спецификацию примера и как её читать.

    ---

    1) Что делает референс‑сервис (функциональная карта)

    Сервис организован вокруг одной идеи: один production‑контур для инференса, который поддерживает несколько режимов выполнения и одинаковую эксплуатационную дисциплину (request_id, единый error model, ограничения входа, backpressure).

    Функционально сервис включает:

  • Sync inference (REST)
  • 1. эмбеддинги текста (типичный «короткий» endpoint) 2. генерация текста (типичный «длинный» endpoint)
  • Streaming inference (LLM)
  • 1. поток событий с start/delta/end/error
  • Async job‑based inference
  • 1. постановка задачи с idempotency 2. получение статуса/результата 3. отмена (best effort)
  • Health endpoints
  • 1. liveness/readiness/startup

    Ниже — схема поверхности API (все публичные endpoint’ы находятся под /v1).

    ---

    2) Общие правила контракта (единые для всех endpoint’ов)

    2.1. Заголовки

  • Content-Type: application/json для всех POST.
  • X-Request-ID — опционально: если клиент прислал, сервис использует его; если нет — генерирует.
  • Idempotency-Keyобязателен для POST /v1/jobs (если клиент не передаёт, сервис может поддерживать вычисление ключа, но в референсе считаем заголовок основным путём).
  • 2.2. Единый формат успешного ответа (конверт)

    Успешные ответы (sync) используют общий «скелет»:

  • request_id: строка
  • model: строка (идентификатор/версия реально обслужившей модели)
  • result: объект результата
  • timings_ms: опционально (агрегированные тайминги верхнего уровня)
  • 2.3. Единый формат ошибки

    Все ошибки возвращаются в едином формате (подробно — в статье про error model и exception handlers):

  • error_code: стабильный код
  • message: человеко‑читаемое
  • request_id: корреляция
  • details: опционально, структурировано
  • Коды HTTP для типовых ситуаций (сводно):

    | Ситуация | HTTP | Пример error_code | |---|---:|---| | DTO/контракт не прошёл | 422 | validation_error | | Превышен лимит размера | 413 | input_too_large | | Перегруз/лимитер/очередь | 429 (или 503 по политике сервиса) | overloaded | | Неожиданная ошибка | 500 | internal_error |

    ---

    3) API v1: синхронные endpoints

    3.1. POST /v1/embeddings

    Назначение: получить эмбеддинги для набора текстов.

    Семантика: batch‑контракт с per‑item успешностью в рамках одного запроса (практика для «грязных» данных).

    Request (упрощённая схема):

  • inputs: массив строк
  • - длина массива: 1..64 - длина строки: 1..8000 символов
  • profile: fast|quality (по умолчанию fast)
  • normalize: boolean (по умолчанию true)
  • Response:

  • result.embeddings: массив элементов по порядку входа
  • - каждый элемент: - ok: boolean - если ok=truevector: массив чисел - если ok=falseerror: объект в формате error model (без request_id, потому что общий request_id уже есть сверху)

    Особенность контракта:

  • Порядок результата соответствует порядку входов.
  • Частичные ошибки не превращаются в 4xx на весь запрос, если выбран per‑item режим.
  • 3.2. POST /v1/generate

    Назначение: синхронная генерация текста.

    Request (упрощённая схема):

  • text: строка (1..N символов; конкретный N зависит от лимитов сервиса)
  • profile: fast|quality
  • max_tokens: число (жёстко ограничено политикой сервиса)
  • temperature: число
  • top_p: число
  • stop: массив строк (опционально)
  • Response:

  • result.text: строка
  • опционально: result.finish_reason
  • Операционная оговорка: синхронный generate должен укладываться в таймауты всей цепочки (client/ingress/app). Если это не гарантируется, используйте job‑based.

    ---

    4) Streaming endpoint

    4.1. POST /v1/generate:stream

    Назначение: потоковая генерация для снижения perceived latency.

    Контракт потока: это последовательность JSON‑событий (transport‑детали могут отличаться, но смысл фиксируем на уровне типов событий).

    События:

  • start
  • 1. содержит request_id, model, опционально profile
  • delta
  • 1. содержит очередную порцию текста (text_delta) или токены (в зависимости от реализации)
  • end
  • 1. содержит признак нормального завершения, опционально finish_reason
  • error
  • 1. содержит объект ошибки в едином формате (error_code/message/details)

    Ключевое правило для клиента: поток завершён корректно только при получении end (а не просто при разрыве соединения).

    ---

    5) Async job‑based API

    5.1. POST /v1/jobs

    Назначение: поставить инференс‑задачу в очередь и получить job_id.

    Headers:

  • Idempotency-Key: обязателен.
  • Request:

  • kind: embeddings|generate (в референсе два типа задач)
  • payload: объект запроса соответствующего sync endpoint’а
  • callback: опционально (если поддерживается webhook, иначе пропускаем)
  • Response:

  • 202 Accepted
  • result.job_id: строка
  • result.status: queued
  • Повторный запрос с тем же Idempotency-Key: возвращает тот же job_id (не создаёт новую задачу).

    5.2. GET /v1/jobs/{job_id}

    Назначение: получить статус и результат.

    Статусы (контракт):

  • queued
  • running
  • succeeded
  • failed
  • canceled
  • expired (если TTL результата истёк)
  • Response (упрощённо):

  • result.status: один из статусов
  • если succeededresult.output: результат того же формата, что у sync endpoint’а (или ссылка на результат, если он большой)
  • если failedresult.error: единый error model
  • 5.3. DELETE /v1/jobs/{job_id}

    Назначение: отменить задачу.

    Response:

  • 202 Accepted если отмена принята (best effort)
  • 404 если job не существует или уже expired (по политике сервиса)
  • ---

    6) Health endpoints (для эксплуатации)

    Эти endpoint’ы не часть v1‑контракта (они не версионируются вместе с inference API), но считаются публичной поверхностью для оркестратора.

  • GET /health/live — процесс отвечает.
  • GET /health/ready — runtime загружен/прогрет и способен обслуживать запросы.
  • GET /health/startup — стартовая последовательность завершена (важно при долгой инициализации).
  • Точная логика probes и их настройка — в статье про healthchecks.

    ---

    7) Мини‑сводка OpenAPI: что именно фиксируем как контракт

    Чтобы спецификация была «источником правды» (и пригодной для contract‑тестов), в референс‑сервисе принципиально фиксируются:

  • Схемы запросов/ответов, включая ограничения (min/max, maxLength).
  • Структура ошибок и список возможных кодов HTTP.
  • Примеры: один «обычный» и один «на границе лимита» (без гигантских payload прямо в схеме).
  • Явное различие sync vs job‑based семантики.
  • ---

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

    1) Составьте таблицу «endpoint → обещание клиенту» (одна фраза) для:

  • /v1/embeddings
  • /v1/generate
  • /v1/generate:stream
  • /v1/jobs и /v1/jobs/{id}
  • 2) Для batch‑ответа embeddings выберите: all‑or‑nothing или per‑item. Обоснуйте выбор двумя аргументами с точки зрения production.

    3) Придумайте 5 error_code для этого сервиса:

  • 2 кода для ошибок входа
  • 2 кода для перегруза/ресурсов
  • 1 код для неожиданных ошибок
  • 4) Опишите 3 правила, что обязательно должно входить в Idempotency-Key (если бы вы вычисляли его на стороне сервиса) для job‑based генерации.

    <details> <summary> Ответы </summary>

    1) Пример «обещаний»:

  • /v1/embeddings: «верну эмбеддинги для каждого входа; для проблемных элементов верну per‑item ошибку без падения всего запроса».
  • /v1/generate: «верну полный сгенерированный текст в одном ответе, если запрос укладывается в лимиты и таймауты».
  • /v1/generate:stream: «начну отдавать результат частями, сообщая начало, прогресс и явное завершение/ошибку».
  • /v1/jobs + /v1/jobs/{id}: «приму задачу, верну job_id, затем позволю получать статус и результат позже по этому id».
  • 2) Для production чаще выбирают per‑item:

  • Реальные данные часто содержат единичные плохие элементы; all‑or‑nothing заставляет клиента дробить батч и ретраить, увеличивая нагрузку.
  • Per‑item упрощает эксплуатацию и устойчивость: один плохой элемент не портит throughput остальным.
  • 3) Пример error_code:

  • Вход:
  • 1) validation_error 2) input_too_large
  • Перегруз/ресурсы:
  • 3) overloaded 4) timeout
  • Неожиданное:
  • 5) internal_error

    4) Пример 3 правил для Idempotency-Key генерации (если вычислять на сервере):

  • Ключ должен зависеть от нормализованного prompt (после trim/нормализации пробелов) — обычно через хэш.
  • Ключ должен зависеть от версии модели (и/или model_id+model_version), иначе получите ложные совпадения при обновлении модели.
  • Ключ должен зависеть от параметров генерации, влияющих на результат (например, temperature, top_p, max_tokens, stop), иначе два разных запроса будут считаться «одним и тем же».
  • </details>

    37. Полный исходный код сервиса: FastAPI app, роуты, схемы

    Полный исходный код сервиса: FastAPI app, роуты, схемы

    Ниже — сквозной “reference code” для слоя FastAPI: app factory + lifespan, роутеры, Pydantic v2 DTO (request/response/error) и минимальные dependency-функции. Архитектурные принципы (слои/порты/адаптеры), формат ошибок, логирование/метрики, конфигурация и NFR уже подробно разобраны в предыдущих статьях — здесь мы их не повторяем, а приземляем в код.

    1) Структура файлов (то, что вы реально кладёте в репозиторий)

    Важно: ниже код “полный по контуру FastAPI”. ML-runtime внутри use case показан минимально (заглушки), чтобы у вас собирался и запускался пример.

    ---

    2) Settings и сборка приложения

    2.1. app/bootstrap/settings.py

    2.2. app/main.py (app factory)

    2.3. app/bootstrap/lifespan.py

    ---

    3) Domain errors и маппинг в HTTP

    3.1. app/domain/errors.py

    3.2. app/adapters/inbound/http/errors/mapping.py

    3.3. app/adapters/inbound/http/errors/handlers.py

    3.4. app/adapters/inbound/http/errors/setup.py

    ---

    4) Middleware: request_id (минимум)

    app/adapters/inbound/http/middleware/request_id.py

    ---

    5) DTO (Pydantic v2): общие схемы и ошибки

    5.1. app/adapters/inbound/http/api_v1/schemas/errors.py

    5.2. app/adapters/inbound/http/api_v1/schemas/common.py

    > В реальном проекте result лучше сделать generic-типом или отдельными response-моделями на endpoint. Здесь оставляем “скелет”.

    ---

    6) DTO для inference endpoints

    6.1. Embeddings

    app/adapters/inbound/http/api_v1/schemas/embeddings.py

    6.2. Generate

    app/adapters/inbound/http/api_v1/schemas/generate.py

    ---

    7) Dependency-функции и контейнер

    7.1. app/bootstrap/container.py (минимально)

    7.2. app/adapters/inbound/http/api_v1/deps.py

    ---

    8) Роутеры и endpoints

    8.1. app/adapters/inbound/http/api_v1/router.py

    8.2. Health endpoints

    app/adapters/inbound/http/api_v1/endpoints/health.py

    8.3. Embeddings endpoint

    app/adapters/inbound/http/api_v1/endpoints/embeddings.py

    8.4. Generate endpoint

    app/adapters/inbound/http/api_v1/endpoints/generate.py

    8.5. Jobs endpoints (контур)

    app/adapters/inbound/http/api_v1/schemas/jobs.py

    app/adapters/inbound/http/api_v1/endpoints/jobs.py

    ---

    9) Минимальные use cases (заглушки, чтобы сервис был runnable)

    app/use_cases/embeddings.py

    app/use_cases/generate.py

    app/use_cases/jobs.py

    ---

    10) Что проверить руками после сборки кода

  • GET /v1/health/live и GET /v1/health/ready отвечают быстро.
  • POST /v1/embeddings валидирует размер батча и строки (Pydantic), а превышение лимитов в use case даёт доменную ошибку.
  • Любая ошибка возвращается в едином error-формате (через exception handlers).
  • X-Request-ID возвращается в response headers и доступен как request.state.request_id.
  • ---

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

    1) Добавьте к /v1/embeddings per-item ошибки: если конкретная строка пустая (после strip), вернуть ok=false для элемента, но не валить весь запрос.

    2) Сделайте общий базовый класс DTO RequestBase с extra="forbid" и str_strip_whitespace=True, и унаследуйте EmbeddingsRequest и GenerateRequest от него.

    3) Добавьте в ResponseEnvelope поле profile, чтобы оно возвращалось в ответе для inference endpoints (но не для health).

    4) Реализуйте в request_id_middleware поддержку второго заголовка X-Correlation-ID, если X-Request-ID не пришёл.

    <details> <summary>

    Ответы

    </summary>

    1) Per-item ошибки делаются на уровне use case (или адаптера), потому что Pydantic по умолчанию валит весь запрос.

  • Не ставьте min_length=1 на элемент Text, иначе пустая строка будет 422 для всего батча.
  • В use case:
  • 1. для каждого элемента делайте s = s.strip() 2. если not s, добавляйте EmbeddingItemErr(ok=False, error_code="validation_error", message="empty input") 3. иначе добавляйте EmbeddingItemOk(...)

    2) Базовая модель:

    И затем:

    3) Если profile нужен только для inference, есть два варианта:

  • Вариант A (простой): добавить profile: str | None = None в ResponseEnvelope и заполнять только в inference endpoints.
  • Вариант B (строже): сделать два envelope-типа (InferenceEnvelope и ServiceEnvelope), чтобы health не “видел” лишние поля.
  • 4) Логика middleware:

  • Сначала пробуем X-Request-ID.
  • Если нет — пробуем X-Correlation-ID.
  • Если оба отсутствуют/некорректны — генерируем.
  • В ответе всегда возвращаем один канонический заголовок (например, X-Request-ID).
  • </details>

    38. Полный исходный код: ML runtime, batching, кеш, фоновые задачи

    Полный исходный код: ML runtime, batching, кеш, фоновые задачи

    В прошлых статьях мы уже спроектировали слои, контракты, ошибки, конфиги, lifecycle, очереди и idempotency. Здесь — сквозной reference code именно для «тяжёлой части» serving-контура:

  • ML runtime (load/warmup/infer) как outbound-адаптер.
  • Server-side micro-batching (batch window + max batch size) рядом с runtime.
  • Кеш (response cache + in-flight dedupe как опция).
  • Фоновые задачи/job-based (минимальная реализация + куда подключить Redis/RQ/Celery).
  • Код ниже ориентирован на архитектуру «ядро → порты → адаптеры»; FastAPI слой (роуты/DTO/error handlers) уже дан ранее и здесь не повторяется.

    ---

    1) Структура файлов для этого контура

    Принцип: batching/кеш/job-очередь — не в FastAPI handlers, а в outbound контуре, который use case вызывает через порты.

    ---

    2) Порты (interfaces): runtime, cache, jobs

    2.1. RuntimePort

    Здесь runtime возвращает batch результат (list[str]) — это важно для micro-batching.

    2.2. CachePort

    Для embeddings обычно кешируют бинарнее/компактнее, но контракт порта можно расширять позже.

    2.3. JobsPort

    Idempotency уже обсуждали концептуально; здесь закрепляем её в методе enqueue.

    ---

    3) Runtime-адаптер: lifecycle + thread safety (минимум)

    Ниже — «правильная форма» runtime: load/warmup → ready. Реальная модель (PyTorch/ORT) подключается внутрь.

    Что важно:

  • Semaphore — это внутренний limiter конкурентности runtime, а не «настройка ASGI воркеров».
  • warmup() использует тот же путь infer_*, чтобы прогреть реальные буферы/ядра.
  • ---

    4) Micro-batching: агрегатор запросов рядом с runtime

    Micro-batcher — это отдельный объект, который:

  • Принимает одиночные запросы.
  • Собирает их в батч (до max_batch_size или пока не истечёт batch_window_ms).
  • Делает один вызов runtime.infer_text_batch.
  • Раздаёт результаты обратно через Future.
  • Практические замечания:

  • timeout_ms в submit — это внутренний «guardrail», чтобы не копить вечные ожидания.
  • Профили batching (batch_window_ms, max_batch_size) должны быть конфигурируемы и связаны с профилем (fast/quality).
  • ---

    5) Кеш: response cache и ключи

    5.1. In-memory кеш (для reference)

    5.2. Ключ кеша: что обязательно учесть

    Ключ должен зависеть от:

  • нормализованного входа (например, strip()),
  • model_id и/или model_version (что именно возвращаете в API),
  • профиля (fast/quality),
  • параметров, влияющих на результат (max_tokens, temperature, и т.п.).
  • Мини-шаблон ключа:

    Если вам нужен in-flight dedupe (не выполнять один и тот же запрос параллельно), делается отдельной структурой key -> Future рядом с кешом/батчером.

    ---

    6) Use case: Generate через cache + batcher

    Use case остаётся «тонким»: политика → кеш → вызов batcher/runtime.

    Эта версия deliberately упрощает ключ (через hash(text)) — в production замените на стабильный хэш и включите параметры.

    ---

    7) Фоновые задачи (job-based): in-process версия + куда подключить Redis/RQ

    7.1. Минимальный in-process jobs адаптер

    Это учебный вариант: без TTL, без персистентности, без retries. Но интерфейс (JobsPort) уже готов для замены на Redis/RQ или Celery (см. статью про очереди и idempotency).

    7.2. Где «встраивается» RQ/Celery

    В enqueue() вы вместо self._q.put(job_id) публикуете job в брокер (Redis), а get() читает статус/результат из result store. Важно: idempotency mapping (idem_key -> job_id) в Redis должна быть атомарной.

    ---

    8) Wiring в контейнере + lifecycle

    Главное: runtime/batcher/jobs должны стартовать в lifespan.

    Readiness endpoint (см. статью про probes) должен опираться на runtime.ready (или аналогичный флаг), а не делать реальный инференс.

    ---

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

    1) Добавьте in-flight dedupe для GenerateTextUseCase: если два одинаковых запроса пришли одновременно, второй должен ждать результат первого, а не идти в batcher.

    2) Сделайте профили micro-batching для fast и quality:

  • fast: batch_window_ms=2, max_batch_size=4
  • quality: batch_window_ms=10, max_batch_size=16
  • и обеспечьте, что они не смешиваются в одном батче.

    3) Для job-based: реализуйте Redis-совместимое idempotency поведение на уровне интерфейса (без реального Redis):

  • SET idem:{key} NX EX ttl
  • если ключ уже существует — вернуть существующий job_id
  • Опишите, где именно в коде должна быть атомарность.

    4) Опишите минимальный набор метрик, которые вы обязаны добавить вокруг micro-batcher (без реализации экспорта): названия, тип (counter/gauge/histogram) и смысл.

    <details> <summary>

    Ответы

    </summary>

    1) In-flight dedupe удобно реализовать так:

  • В GenerateTextUseCase добавить структуру self._inflight: dict[str, Future[str]] + asyncio.Lock().
  • Алгоритм:
  • 1. Посчитать key. 2. Проверить cache hit — вернуть. 3. Захватить lock, проверить key in inflight. - если да: взять future и отпустить lock. - если нет: создать future, положить в inflight, отпустить lock. 4. Если это «первый» запрос: выполнить batcher, записать в cache, завершить future (result/exception), удалить key из inflight. 5. Если это «второй»: просто await future.

    Критично: удаление из inflight делать в finally, чтобы не оставлять «вечные ожидания».

    2) Не смешивать профили проще всего на уровне объектов:

  • завести два разных MicroBatcher (например, batcher_fast и batcher_quality), каждый со своей очередью.
  • в use case выбирать batcher по profile.
  • Тогда физически невозможно собрать один батч из разных профилей.

    3) Атомарность нужна в момент записи idem_key -> job_id. В нашем in-process коде это «атомарно» внутри одного event loop, но в distributed варианте нужен Redis SET NX:

  • Сначала делаете SET idem:{key} {job_id} NX EX {ttl}.
  • Если SET успешен — публикуете job в очередь и создаёте job:{job_id} запись.
  • Если SET неуспешен — читаете idem:{key} и возвращаете существующий job_id.
  • Если не сделать атомарность, два параллельных запроса создадут две job и нарушат контракт идемпотентности.

    4) Минимальный набор метрик вокруг batcher:

  • batcher_requests_total{profile} — counter: сколько submit пришло.
  • batcher_batch_size{profile} — histogram: распределение размеров батча (понимать, работает ли batching).
  • batcher_batch_wait_seconds{profile} — histogram: сколько ждём набора батча (влияние на p95/p99).
  • batcher_infer_seconds{profile} — histogram: время вызова runtime на батч.
  • batcher_queue_depth{profile} — gauge: размер очереди ожидания (ранний сигнал перегруза).
  • Эти метрики позволяют отличать «модель медленная» от «batch window/очередь копится».

    </details>

    39. Полный исходный код: Docker, Compose, Helm, Kubernetes манифесты

    Полный исходный код: Docker, Compose, Helm, Kubernetes манифесты

    Эта статья — «карта артефактов доставки» референс‑сервиса: какие файлы должны быть в репозитории, как они связаны между собой, и какие параметры считаются обязательными для production‑ML serving.

    Мы не повторяем принципы воспроизводимой сборки, non‑root, probes, ASGI‑настроек, конфигурации/секретов и release‑паттернов — они подробно разобраны в соответствующих статьях. Здесь — конкретный код/шаблоны и дисциплина размещения.

    1) Структура репозитория для артефактов деплоя

    Рекомендуемый минимум:

    Правило: Docker даёт воспроизводимый runtime‑артефакт, Compose — локальный стенд, Helm/K8s — production‑манифесты (либо Helm как «упаковка» тех же ресурсов).

    2) Docker: multi-stage, targets (cpu/gpu), entrypoint, .dockerignore

    2.1. deployments/docker/Dockerfile

    Ниже — шаблон с двумя runtime‑таргетами. Детали “почему так” — в статье про production Dockerfile и pinned deps.

    2.2. deployments/docker/entrypoint.sh

    Checksum‑аннотация — это практический способ гарантировать rollout при изменении ConfigMap.

    6) GPU‑вариант (K8s/Helm): минимальные отличия

    Если вы делаете GPU‑деплой (подробности ресурса/планирования — в статье про GPU):

    Важно: не пытайтесь «одним Deployment покрыть CPU и GPU» условной логикой. Делайте отдельный values‑файл (например, values-gpu.yaml) и отдельный релиз Helm.

    ---

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

    1) В вашем Deployment включили readOnlyRootFilesystem: true, и сервис упал при старте. Назовите 3 наиболее вероятные причины и покажите, какие volumeMounts/volumes вы добавите.

    2) Спроектируйте compose профили для трёх сценариев: dev, integration, gpu. Какие сервисы должны входить в каждый профиль и почему?

    3) В Helm вы обновили values.yaml (лимиты/таймауты), но Pod’ы не перезапустились. Что вы забыли в шаблонах? Напишите фрагмент аннотации, которая исправляет проблему.

    4) Напишите правило тегирования образов для Helm/K8s так, чтобы откат был воспроизводимым. Дайте 3 правила.

    <details> <summary>

    Ответы

    </summary>

    1) Причины падения при readOnlyRootFilesystem: true:

  • Приложение пытается писать во временную директорию по умолчанию (например, /tmp или внутрь /app), но путь не примонтирован как writable.
  • Runtime пытается кешировать артефакты/модель в домашний каталог пользователя или в директорию проекта.
  • Логика библиотеки (например, tokenizers/ображение) создаёт временные файлы в стандартных путях.
  • Исправление (пример):

  • добавить emptyDir и примонтировать в конкретные пути, которые использует сервис:
  • - /app/.tmp - /app/.cache

    2) Профили Compose:

  • dev: app + postgres + redis и проброс портов наружу; нужен hot reload через compose.override.yaml.
  • integration: app + postgres + redis + migrations (+ tests, если есть); без проброса портов наружу; обязателен чистый запуск и healthchecks.
  • gpu: отдельный app-gpu (runtime-gpu target) + (опционально) зависимости, если они реально используются в GPU‑режиме.
  • 3) Забыли checksum‑аннотацию (или аналогичный механизм), который меняет Pod template при изменении ConfigMap.

    Фрагмент:

    4) 3 правила тегирования/идентификации образов:

  • В prod не использовать latest как источник правды.
  • Использовать тег, привязанный к коммиту (например, sha-...) или фиксировать image digest.
  • Один и тот же образ (digest) должен продвигаться между окружениями (dev→stage→prod) без пересборки.
  • </details>

    4. Контракты API: REST, OpenAPI, versioning, backward compatibility

    Контракты API: REST, OpenAPI, versioning, backward compatibility

    Контракт API — это формализованное обещание сервиса клиентам: какие запросы допустимы, какие ответы возможны, как выглядят ошибки и что будет при изменениях. Для ML‑serving это особенно критично, потому что входы часто «грязные» (неожиданные типы/размеры), а изменения модели и препроцессинга происходят регулярно.

    В предыдущих статьях мы уже обсуждали, почему API‑слой должен быть «тонким» и почему версионирование — обязательный элемент эксплуатации. Здесь фокус — как именно фиксировать контракт, как оформлять его в REST/OpenAPI и как менять без поломки клиентов.

    ---

    1) REST для ML‑inference: как проектировать поверхность API

    REST — это не «про красивые URL», а про предсказуемость взаимодействия: ресурсы, методы, коды ответов, идемпотентность.

    1.1. Ресурсы vs “actions”: что считать нормой в инференсе

    ML‑инференс часто выглядит как «действие» (predict/generate). Практически допустимы оба подхода:

  • Action‑endpoint (самый частый):
  • 1. POST /v1/predict 2. POST /v1/embeddings 3. POST /v1/generate
  • Resource‑подход (удобен для async/job‑based):
  • 1. POST /v1/jobs → создаём job 2. GET /v1/jobs/{id} → читаем статус/результат

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

    1.2. Идемпотентность и ретраи

    В high load и при сетевых сбоях ретраи неизбежны. Для контрактов это означает:

  • Для синхронного инференса POST обычно не идемпотентен по определению, но на практике запрос «одинаковый input → одинаковый output» может быть идемпотентным (например, embeddings) и неидемпотентным (LLM генерация с temperature).
  • Для async/job‑based полезно ввести idempotency key (заголовок или поле запроса), чтобы повторный POST /jobs не создавал дубликаты.
  • Контракт должен явно фиксировать:

  • можно ли безопасно ретраить;
  • что считается «тем же» запросом (например, одинаковый input + одинаковые параметры);
  • как долго сервис помнит idempotency key.
  • 1.3. Единый формат ошибок: часть контракта, а не “как получится”

    Стабильный error‑контракт снижает стоимость интеграции и ускоряет отладку. Минимально полезные элементы:

  • error_code: машинно‑читаемый код (стабильный)
  • message: человеко‑читаемое описание
  • details: структурированные поля (например, какой лимит превышен)
  • request_id: для корреляции
  • Важно: не превращать message в контракт. Контракт — это error_code и структура details.

    1.4. Коды ответов: договорённость, которая экономит время

    Для инференса полезно быть скучно‑предсказуемыми:

  • 200 OK — успешный синхронный результат
  • 202 Accepted — запрос принят, результат позже (job‑based)
  • 400 Bad Request — вход невалиден (тип/формат)
  • 413 Payload Too Large — превышен лимит размера
  • 422 Unprocessable Entity — семантически некорректно (например, пустой текст при запрете)
  • 429 Too Many Requests — квоты/rate limiting/backpressure
  • 503 Service Unavailable — перегруз/недоступность зависимостей (контролируемая деградация)
  • Не обязательно использовать все — важно, чтобы выбранные коды были стабильны и отражены в OpenAPI.

    ---

    2) OpenAPI как “источник правды” для контракта

    OpenAPI — это не просто документация. В production его стоит воспринимать как артефакт, который поддерживает:

  • валидацию запросов/ответов;
  • генерацию клиентов и контрактные тесты;
  • контроль изменений (diff спецификации);
  • единый словарь типов (особенно полезно, когда клиентов много).
  • 2.1. Что обязательно описывать в OpenAPI для ML‑serving

  • Схемы входов/выходов:
  • 1. обязательные/необязательные поля 2. ограничения (min/max, maxLength) 3. форматы (например, base64‑строка, либо URL)
  • Ошибки:
  • 1. общий error‑response schema 2. список error_code по endpoint’ам
  • Примеры:
  • 1. примеры валидного запроса 2. примеры типовых ошибок
  • Ограничения на payload (хотя бы как описание):
  • 1. лимиты размера текста/изображения 2. допустимые content‑types

    2.2. Два практических правила, чтобы OpenAPI не расходился с реальностью

  • Спецификация должна обновляться вместе с кодом (в одном PR). Если это не происходит автоматически/процедурно, контракт деградирует.
  • Не прятать важные ограничения “в тексте”: если лимит можно выразить структурно (например, maxLength), лучше выразить структурно.
  • 2.3. Контракт ответа: “конверт” и метаданные

    Для ML‑inference часто полезен ответ‑конверт с метаданными, но он должен быть стабильным и минимальным:

  • result: собственно предсказание/текст/эмбеддинг
  • model: идентификатор/версия модели, которая реально обслужила запрос
  • timings: опционально — агрегированные тайминги (не обязаны совпадать с внутренними трейcами)
  • request_id: корреляция
  • Главное — не смешивать в ответе:

  • API‑контракт (стабильный)
  • отладочные детали (могут быть включаемыми флагом)
  • ---

    3) Versioning: как менять API и не ломать клиентов

    Нужно различать:

  • Версия API — контракт HTTP (поля, форматы, коды)
  • Версия модели — артефакт инференса (веса/токенизатор/правила)
  • Они могут жить с разной скоростью изменений.

    3.1. Стратегии версионирования API

    На практике встречаются три стратегии (выбор уже упоминался ранее, здесь — критерии применения):

  • Версия в пути (/v1/...):
  • 1. проще всего для клиентов 2. легко держать параллельные реализации
  • Версия в заголовке:
  • 1. URL не меняется 2. сложнее кэширование/проксирование и диагностика
  • Версия через content negotiation (Accept: application/vnd...):
  • 1. мощно, но часто избыточно

    Важно зафиксировать правило: мажорная версия меняется только при breaking changes.

    3.2. Политика совместимости: что считается breaking change

    Ниже — практичная таблица для контрактов (в терминах клиента).

    | Изменение | Обычно совместимо? | Комментарий | |---|---:|---| | Добавить новое необязательное поле в ответ | Да | Клиент‑читатель должен быть tolerant (игнорировать неизвестное) | | Добавить обязательное поле в запрос | Нет | Старые клиенты не пришлют поле | | Переименовать поле | Нет | Требует переходного периода или новой версии | | Удалить поле из ответа | Нет | Даже если “никто не использует”, это риск | | Расширить enum новыми значениями | Зависит | Ломает клиентов, которые жёстко матчят enum | | Изменить смысл поля без изменения имени | Нет | Самый опасный вид поломки | | Изменить тип поля (string → object) | Нет | Даже при «логической совместимости» |

    3.3. Деприкация: как убирать старое без хаоса

    Полезная дисциплина:

  • объявить поле/поведение deprecated (в OpenAPI и в описании)
  • держать период совместимости (время или N релизов)
  • добавить телеметрию использования (сколько запросов ещё приходит в старом формате)
  • только потом удалять в новой мажорной версии
  • Контракт должен явно фиксировать:

  • какие версии поддерживаются одновременно;
  • как клиенту мигрировать;
  • что произойдёт после окончания поддержки.
  • ---

    4) Backward compatibility на практике: как проектировать “на вырост”

    4.1. Принцип tolerant reader

    Клиент должен:

  • игнорировать неизвестные поля в ответе;
  • аккуратно обрабатывать неизвестные значения enum;
  • не полагаться на порядок полей в JSON.
  • Сервер, в свою очередь, должен:

  • принимать старые поля (если объявлена совместимость);
  • иметь значения по умолчанию для новых опций;
  • возвращать понятные ошибки с error_code, когда старый формат больше не поддерживается.
  • 4.2. “Расширяемые” структуры вместо жёстких

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

    Два типовых решения:

  • options объект для параметров инференса (вместо множества плоских полей)
  • meta объект для служебных данных
  • Риск: не превращать options в «помойку без схемы». Даже options должен иметь описанные поля и ограничения.

    4.3. Совместимость и кеширование

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

    Практическое правило контракта: любое поле, которое влияет на результат, должно быть явно частью запроса (и описано в OpenAPI). Иначе появятся «магические» изменения поведения без изменения входа.

    ---

    5) Контракт как часть жизненного цикла: проверки и “предохранители”

    Чтобы контракт не ломали случайно, обычно вводят:

  • Contract tests: проверка, что сервис соответствует OpenAPI (минимум — схемы и коды)
  • Diff OpenAPI: в CI сравнивать спецификацию с предыдущей версией и отмечать breaking changes
  • Набор фиксированных примеров (golden requests): несколько эталонных запросов/ответов для защиты от случайных изменений формата
  • Это не заменяет интеграционные/нагрузочные тесты, но защищает именно договорённость между командами.

    ---

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

    1) Придумайте 3 endpoint’а для LLM‑сервиса (sync) и 2 endpoint’а для async/job‑based. Укажите методы и ожидаемые коды ответа.

    2) Опишите единый формат ошибки для вашего сервиса: какие поля обязательны и почему.

    3) Для 6 изменений контракта определите, breaking они или нет (используйте таблицу из статьи): 1. добавили поле model в ответ 2. сделали поле text в запросе обязательным (раньше было optional) 3. добавили новое значение в enum profile: turbo 4. переименовали request_idid 5. изменили тип max_tokens с number на string 6. добавили новый необязательный объект options.safety

    4) Сформулируйте политику поддержки версий: сколько версий поддерживаем параллельно и по какому сигналу можно удалять старую.

    5) Напишите 5 требований к OpenAPI‑спецификации ML‑сервиса, которые помогают эксплуатации (не “чтобы было красиво”).

    <details> <summary> Ответы </summary>

    1) Пример.

    Sync (LLM):

  • POST /v1/generate200, ошибки 400/413/422/429/503
  • POST /v1/embeddings200, ошибки аналогично
  • POST /v1/rerank200
  • Async/job‑based:

  • POST /v1/jobs202 (вернуть job_id)
  • GET /v1/jobs/{job_id}200 (готово) или 202 (ещё в работе) или 404
  • 2) Пример обязательных полей ошибки:

  • error_code — стабильная машинная логика на стороне клиента
  • message — человеко‑читаемая диагностика (не должна быть единственным источником истины)
  • request_id — корреляция в логах/трейсах
  • details — структурированная причина (например, limit=8192, got=12000)
  • 3) Классификация:

  • Добавили model в ответ — обычно не breaking (аддитивно).
  • Сделали text обязательным — breaking.
  • Добавили turbo в enum — зависит от клиентов; формально может быть breaking для строгих валидаторов.
  • request_idid — breaking.
  • Тип max_tokens number → string — breaking.
  • Новый необязательный options.safety — обычно не breaking.
  • 4) Пример политики:

  • Поддерживаем 2 мажорные версии параллельно (v1 и v2).
  • Старую версию можно удалять, когда:
  • 1) доля трафика на неё ниже порога (например, <1%) в течение N дней, 2) есть подтверждение от ключевых потребителей, 3) опубликована дата окончания поддержки и она наступила.

    5) Пример 5 требований к OpenAPI для эксплуатации:

  • Описаны коды ошибок и единая схема error‑response.
  • Явно заданы ограничения на входы (min/max, maxLength), где это возможно.
  • Есть примеры запросов/ответов для основных сценариев и типовых ошибок.
  • Указаны content‑types и формат передачи бинарных данных (если есть изображения).
  • Спецификация обновляется вместе с кодом и проходит проверку на breaking changes.
  • </details>

    40. Полный исходный код: GitHub Actions CI/CD, quality gates, deploy

    Полный исходный код: GitHub Actions CI/CD, quality gates, deploy

    Ниже — референсный набор workflow для GitHub Actions, который закрывает полный цикл:

  • PR quality gates: формат/линт/типы/тесты + сборка контейнера как проверка.
  • Build & publish: сборка и публикация иммутабельного образа (лучше по digest) + артефакты (SBOM, метаданные релиза).
  • Deploy в Kubernetes: Helm‑деплой по окружениям с ожиданием rollout и пост‑деплой проверками.
  • Концептуальные принципы CI/CD, тестовые контуры, SBOM и политика релизов уже разобраны в статьях про CI pipeline и CD pipeline — здесь только конкретный код GitHub Actions и важные инженерные детали, которые обычно «ломают» прод.

    ---

    1) Организация файлов в репозитории

    Рекомендуемое размещение:

    Ключевая дисциплина:

  • PR workflow не пушит образы.
  • Release workflow пушит образ только после всех quality gates.
  • Deploy workflow использует конкретный digest образа, а не «плавающий» тег.
  • ---

    2) PR workflow: quality gates (блокирующий)

    Файл: .github/workflows/pr.yml

    4.2. Environment protection в GitHub

    Смысловой минимум:

  • Создайте Environments: dev, stage, prod.
  • Для prod включите обязательных reviewers (approvals).
  • Храните KUBECONFIG_B64 как environment secret (разный для окружений).
  • ---

    5) Миграции БД как отдельный шаг (не в entrypoint)

    Если вы применяете миграции в CD (см. статьи про миграции и релиз‑паттерны), добавьте отдельный job до деплоя приложения:

    Так вы избегаете гонок и «скрытых» миграций при старте каждого pod.

    ---

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

    1) Выберите 6 блокирующих quality gates для PR workflow именно для ML‑serving сервиса и объясните, какой риск закрывает каждый.

    2) Объясните, почему деплой по digest более воспроизводим, чем деплой по тегу. Назовите 3 конкретных сценария, где тег ломает откат.

    3) Составьте минимальный набор post‑deploy проверок (5 пунктов), который можно запускать за 1–2 минуты в CD.

    4) У вас stage и prod используют один и тот же workflow deploy.yml. Опишите, какие 3 механизма GitHub Environments вы включите, чтобы снизить риск случайного прод‑деплоя.

    <details> <summary>

    Ответы

    </summary>

    1) Пример 6 quality gates:

  • ruff check — ловит ошибки и опасные паттерны до ревью.
  • ruff format --check — предотвращает “шум” диффов и дрейф стиля.
  • mypy app — стабилизирует контракты портов/DTO/settings.
  • pytest -m unit — защищает доменные политики (лимиты, деградация).
  • pytest -m contract — защищает API‑контракт и единый error model.
  • Docker build + smoke‑run — ловит “в контейнере не стартует”, missing OS libs, ошибки entrypoint.
  • 2) Почему digest лучше тега (3 сценария):

  • Тег могли перезаписать (особенно если используется нестрогая политика), и «откат по тегу» укажет уже на другой образ.
  • В разных окружениях под одним тегом может оказаться разный digest (например, при повторной сборке) — promotion ломается.
  • При расследовании инцидента вы не можете доказать, что именно было в образе, если тег плавающий; digest однозначен.
  • 3) Минимальные post‑deploy проверки:

  • kubectl rollout status для Deployment.
  • GET /health/ready с коротким таймаутом.
  • Лёгкий inference smoke (минимальный вход) — если вы можете выполнить безопасно.
  • Проверка, что сервис возвращает ожидаемые обязательные поля (request_id, model).
  • Проверка, что в течение короткого окна нет лавины 5xx (если у вас есть быстрый способ снять метрику/логи).
  • 4) Три механизма защиты через GitHub Environments:

  • Required reviewers для prod (approval gate).
  • Отдельные secrets per environment (prod kubeconfig недоступен stage job’ам).
  • Environment‑scoped variables/правила (например, разные namespace/values файлы), чтобы случайно не деплоить stage values в prod.
  • </details>

    41. Официальные ссылки: FastAPI, Pydantic, OTel, K8s, Helm, GitHub Actions

    Официальные ссылки: FastAPI, Pydantic, OTel, K8s, Helm, GitHub Actions

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

    > Цель: не пересказывать документацию, а дать проверенные точки входа и понятную «карту местности» для ежедневной работы.

    ---

    1) FastAPI (контракты, зависимости, lifecycle)

    Официальная документация FastAPI: FastAPI Documentation

    Что читать в первую очередь (прикладно к курсу)

  • Tutorial (практический ввод)
  • 1) Tutorial - First Steps 2) Tutorial - Body 3) Tutorial - Path Params 4) Tutorial - Query Params

  • Dependency Injection
  • 1) Dependencies 2) Dependencies in path operation decorators

  • Lifecycle / lifespan
  • 1) Events: startup and shutdown

  • Middleware и обработка ошибок
  • 1) Middleware 2) Handling Errors

  • OpenAPI/документация как контракт
  • 1) OpenAPI

    Как применять к production ML serving

  • FastAPI — это inbound‑адаптер и контрактный слой. Все вопросы «как правильно спроектировать эндпоинты (sync/async/streaming), ошибки, версии» мы уже разобрали в статьях про контракты и endpoints; здесь важно помнить: официальная документация FastAPI — источник истинной семантики Depends, middleware, обработчиков исключений и OpenAPI‑генерации.
  • ---

    2) Pydantic v2 и pydantic-settings (DTO и конфигурация)

    Pydantic v2: Pydantic Documentation

    pydantic-settings: pydantic-settings Documentation

    Pydantic v2: страницы, которые реально нужны в serving

  • BaseModel и сериализация/валидация
  • 1) Models 2) Serialization

  • Валидация
  • 1) Validators

  • Annotated/Field и ограничения
  • 1) Fields

  • JSON Schema / OpenAPI‑часть
  • 1) JSON Schema

    pydantic-settings: страницы для typed config и secrets

  • Settings Management
  • Как применять к production ML serving

  • Pydantic v2 — это DTO слой, который должен быть быстрым и предсказуемым; глубокие правила и анти‑паттерны мы уже разобрали в статье про Pydantic схемы.
  • pydantic-settings — это ваш typed config контракт: fail‑fast, структура настроек и источники конфигурации описаны в статье про конфигурацию и 12‑factor.
  • ---

    3) OpenTelemetry (traces/metrics/logs как стандарт)

    Официальный сайт: OpenTelemetry

    Спецификация (важно, если вы проектируете поля/семантику): OpenTelemetry Specification

    Документация по Python SDK: OpenTelemetry Python Documentation

    Что читать в первую очередь

  • Базовые понятия (trace/span/context)
  • 1) Distributed Tracing

  • Context propagation
  • 1) Context Propagation

  • Python
  • 1) Python

    Как применять к production ML serving

  • В наших статьях про трейсинг мы фиксировали «какие span’ы нужны» и «как не взорвать кардинальность/PII». Официальный OTel — источник по семантике контекста, propagation, стандартным атрибутам.
  • ---

    4) Kubernetes (ресурсы, деплой, probes, autoscaling)

    Официальная документация: Kubernetes Documentation

    Ключевые разделы, которые чаще всего нужны в serving

  • Workloads
  • 1) Deployments

  • Service networking
  • 1) Service

  • Ingress (как API объект)
  • 1) Ingress

  • Probes (liveness/readiness/startup)
  • 1) Configure Liveness, Readiness and Startup Probes

  • Resources и планирование
  • 1) Resource Management for Pods and Containers 2) Assign Pods to Nodes

  • Autoscaling
  • 1) Horizontal Pod Autoscaling

    Как применять к production ML serving

  • Kubernetes docs — источник истинных определений: что реально делает readiness, как работают requests/limits, чем отличаются nodeSelector/affinity/taints, как устроен HPA.
  • Практический контур (Deployment/Service/Ingress/HPA + probes) и GPU‑планирование мы уже разобрали в отдельных статьях курса.
  • ---

    5) Helm (упаковка деплоя, values/templates)

    Официальный сайт и документация: Helm Documentation

    Что читать в первую очередь

  • Chart и структура
  • 1) The Chart File Structure

  • Шаблоны и функции
  • 1) Chart Template Guide

  • Values и best practices
  • 1) Values Files

  • Команды релизов (upgrade/rollback)
  • 1) Helm Commands

    Как применять к production ML serving

  • Helm docs — источник по языку темплейтинга, поведению upgrade/rollback, структуре chart.
  • Принципы values/secrets/configmaps и checksum‑аннотаций мы уже закрепили в статье про Helm chart.
  • ---

    6) GitHub Actions (CI/CD workflows, окружения, approvals)

    Официальная документация: GitHub Actions Documentation

    Разделы, которые почти всегда нужны

  • Workflow syntax
  • 1) Workflow syntax for GitHub Actions

  • Contexts и expressions
  • 1) Contexts

  • Secrets и переменные
  • 1) Using secrets in GitHub Actions

  • Environments и protection rules
  • 1) Environments

  • Permissions (важно для публикации образов/артефактов)
  • 1) Workflow permissions

    Как применять к production ML serving

  • GitHub Actions docs — источник по строгой семантике YAML‑синтаксиса, permissions, environment gates и работе с секретами.
  • Архитектуру CI/CD пайплайнов (quality gates, build/push, deploy) мы уже разобрали, а reference workflow — в статье с полным кодом GitHub Actions.
  • ---

    7) Как пользоваться документацией «быстро» (практическая навигация)

    7.1. «У меня странная ошибка в рантайме/под нагрузкой»

  • Сначала проверьте контракт и ошибки (ваш error model + FastAPI error handlers).
  • Затем — процессы/таймауты/воркеры (ASGI сервер и его настройки) и probes (Kubernetes).
  • Далее — observability:
  • 1) histogram‑метрики (Prometheus подход из нашей статьи) 2) traces (OTel)
  • И только потом — оптимизации (batching/cache/runtime), если видно, что узкое место именно там.
  • 7.2. «Хочу понять истинное поведение фичи»

  • Для HTTP/DI/lifespan: FastAPI docs.
  • Для DTO/валидации/JSON schema: Pydantic docs.
  • Для трейсинга/propagation: OpenTelemetry (сайт + спецификация).
  • Для жизненного цикла Pod и ресурсов: Kubernetes docs.
  • Для render/values и поведения upgrade/rollback: Helm docs.
  • Для semantics workflow_dispatch, approvals, permissions: GitHub Actions docs.
  • ---

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

    1) Для каждого инструмента (FastAPI, Pydantic, OpenTelemetry, Kubernetes, Helm, GitHub Actions) укажите по 2 страницы официальной документации, которые вы будете использовать чаще всего в реальном проекте serving.

    2) Сопоставьте проблему и «первый официальный источник», куда вы пойдёте: 1. readiness flapping в Kubernetes 2. странное поведение Depends/lifecycle 3. несоответствие OpenAPI схемы и реального ответа 4. трейс «обрывается» между API и воркером 5. Helm не перезапускает Pod при изменении ConfigMap 6. GitHub Actions не видит secret/не применяет approval

    3) Объясните, почему в production инженерной практике предпочтительнее ссылаться на официальные документы, а не на случайные статьи/ответы. Дайте 3 причины.

    4) Найдите в официальной документации Kubernetes: 1. страницу про probes 2. страницу про requests/limits и объясните по 1–2 предложения: что именно вы там проверите перед тем, как менять прод‑манифест.

    <details> <summary>

    Ответы

    </summary>

    1) Пример частых страниц:

  • FastAPI:
  • 1) Dependencies 2) Events: startup and shutdown

  • Pydantic:
  • 1) Models 2) Validators

  • OpenTelemetry:
  • 1) Distributed Tracing 2) Context Propagation

  • Kubernetes:
  • 1) Configure Liveness, Readiness and Startup Probes 2) Resource Management for Pods and Containers

  • Helm:
  • 1) Chart Template Guide 2) Values Files

  • GitHub Actions:
  • 1) Workflow syntax for GitHub Actions 2) Using environments for deployment

    2) Сопоставление:

  • readiness flapping → Kubernetes docs про probes.
  • Depends/lifecycle → FastAPI docs (Dependencies + Events).
  • OpenAPI схема vs реальный ответ → Pydantic JSON Schema + FastAPI OpenAPI/metadata.
  • trace обрывается между API и воркером → OpenTelemetry (context propagation) + язык/SDK раздел.
  • Helm не перезапускает Pod при изменении ConfigMap → Helm template guide (паттерны) + ваша логика checksum‑аннотаций (в курсе это разобрано в статье про Helm).
  • GitHub Actions secrets/approval → GitHub Actions docs: secrets + environments.
  • 3) Почему официальные источники:

  • Они фиксируют точную семантику и контракт поведения (что особенно важно для lifecycle, таймаутов, security и observability).
  • Они обновляются синхронно с релизами инструмента; неофициальные материалы часто устаревают и вводят в заблуждение.
  • В спорных ситуациях (инциденты, аудит, безопасность) официальный reference проще использовать как «источник правды» для команды.
  • 4) Kubernetes страницы и как применить:

  • Probes: проверить различия liveness/readiness/startup, параметры timeoutSeconds/periodSeconds/failureThreshold, и как они влияют на рестарты и включение pod’а в endpoints.
  • Requests/limits: проверить, как планирование зависит от requests, чем опасен CPU throttling при limits, и как memory limit приводит к OOM kill.
  • </details>

    42. Чеклист production readiness и шаблон для ваших проектов

    Чеклист production readiness и шаблон для ваших проектов

    Эта статья — сборка “в одно место”: компактный production readiness чеклист для ML‑serving сервиса (FastAPI + high load) и шаблон артефактов, которые стоит завести в репозитории. Мы не повторяем детали (они уже разобраны в материалах про контракты, NFR/SLO, runtime lifecycle, observability, безопасность, CI/CD, Docker/K8s/Helm), а даём структуру финальной проверки перед выкладкой.

    1) Как пользоваться чеклистом (практика)

    Используйте два режима:

    1) Gate перед merge: прогоняйте разделы, влияющие на контракт и безопасность (API, ошибки, конфиг, тесты, сканирование). 2) Gate перед релизом/продом: дополнительно прогоняйте нагрузку, rollout/rollback, readiness/warmup, операционные сигналы.

    Важно: чеклист не про «идеально», а про минимально безопасно.

    2) Production readiness чеклист (по контурам)

    2.1. Контракт API и совместимость

    Проверьте:

  • Все публичные endpoint’ы имеют стабильную схему запросов/ответов в OpenAPI.
  • Есть версионирование API (например, /v1).
  • Формат ошибок единый и стабилен:
  • 1) error_code 2) message 3) request_id 4) details (структурно)
  • Breaking change проходит через осознанную процедуру (новая версия / период деприкации).
  • Лимиты входа отражены в контракте там, где возможно (например, maxLength, maxItems).
  • Сигнал “готово”: contract‑тесты защищают коды ответа и форму ошибок для ключевых сценариев.

    2.2. Runtime модели и жизненный цикл (load/warmup/readiness)

    Проверьте:

  • Модель не грузится лениво в handler’е; загрузка и прогрев идут в lifecycle (lifespan/startup).
  • Readiness означает «модель загружена и прошла минимальный self‑test/warmup».
  • Runtime конкурентность ограничена рядом с моделью (семафор/очередь/батчер), а не «на глаз» числом HTTP‑воркеров.
  • Есть политика на случай перегруза (контролируемый отказ вместо накопления в памяти).
  • Для GPU:
  • 1) понятно, сколько процессов живёт на одну GPU; 2) понятно, какие параметры (batch window / max batch / max concurrency) не приведут к OOM.

    Сигнал “готово”: сервис поднимается и становится ready предсказуемо; первые запросы не дают «холодный p99‑шип» сверх ожиданий.

    2.3. Производительность и backpressure

    Проверьте:

  • Для каждого основного endpoint’а есть целевые SLO (latency и успех) и условия нагрузки (профиль, лимиты входа).
  • Есть механизм backpressure:
  • 1) ограничение in‑flight 2) ограничение очереди 3) контролируемый ответ (429/503 по вашей политике)
  • Профили выполнения (например, fast/quality) измеримы и видны в логах/метриках.
  • Для LLM/Streaming отдельно измеряется TTFT и completion time (как минимум концептуально заложено в метриках/трейсинге).
  • Сигнал “готово”: под перегрузом сервис не «умирает молча», а деградирует предсказуемо.

    2.4. Наблюдаемость (logs/metrics/traces)

    Проверьте:

  • Логи структурированные и содержат correlation:
  • 1) request_id 2) событие (event) 3) статус/причину (status_code/error_code)
  • Метрики покрывают минимум:
  • 1) HTTP RPS + latency histogram по endpoint 2) runtime/infer latency histogram 3) queue/batch wait histogram (если есть) 4) счётчики ошибок по error_code 5) явные отказы backpressure
  • Трейсинг:
  • 1) есть root span на HTTP 2) есть спаны стадий (validate → preprocess → queue/batch wait → infer → postprocess) 3) атрибуты не содержат PII и не взрывают кардинальность
  • Есть единый способ связать инцидент: request_id в ответе клиенту.
  • Сигнал “готово”: по одному request_id вы находите логи, видите метрики по ошибкам и (если включено) трейс со стадиями.

    2.5. Безопасность периметра и входов

    Проверьте:

  • Аутентификация/авторизация определены (где gateway, где приложение; какие scopes/права на какие endpoint’ы).
  • Rate limits/quotas/стоимостные лимиты определены хотя бы на уровне политики.
  • CORS настроен осознанно (если есть браузерные клиенты).
  • Секреты не в коде, не в образе и не в логах; ротация предполагает rolling restart.
  • Payload limits на уровне ingress/gateway и на уровне приложения согласованы.
  • Сигнал “готово”: сервис нельзя вызвать «анонимно из интернета», а входы не позволяют легко сжечь ресурсы одним запросом.

    2.6. Конфигурация и управление изменениями

    Проверьте:

  • Все параметры поведения сервиса идут через typed settings.
  • Есть fail‑fast правила старта (некорректный конфиг = pod не становится ready).
  • Конфиг разделён на:
  • 1) не‑секретный (ConfigMap) 2) секреты (Secret/secret manager)
  • Изменение ConfigMap/Secret гарантированно триггерит rollout (checksum‑паттерн в манифестах/Helm).
  • Сигнал “готово”: вы можете безопасно менять лимиты/таймауты без пересборки образа и без «часть реплик на старом конфиге».

    2.7. Тесты и quality gates

    Проверьте:

  • Unit‑тесты покрывают доменные политики (лимиты, профили, backpressure решения, маппинг ошибок).
  • Contract‑тесты блокируют изменения API:
  • 1) схемы 2) коды 3) error model
  • Integration‑smoke проверяет:
  • 1) старт приложения 2) прохождение lifecycle 3) readiness после warmup
  • Есть load smoke (короткий), который ловит грубые регрессии p99/5xx/backpressure.
  • Сигнал “готово”: PR не может сломать контракт или сборку образа без красного CI.

    2.8. CI/CD, артефакты и воспроизводимость

    Проверьте:

  • Dependencies pinned (lockfile) и сборка “frozen”.
  • Образ собирается reproducible, non‑root, без секретов.
  • CI:
  • 1) lint/typecheck/tests 2) build image + smoke run 3) security scanning (SAST, deps, secrets) 4) SBOM как артефакт релиза
  • CD:
  • 1) деплой по digest 2) ожидание rollout по readiness 3) post‑deploy smoke 4) approvals на prod

    Сигнал “готово”: откат воспроизводим, потому что у вас есть конкретный digest + значения деплоя.

    2.9. Kubernetes/Helm эксплуатационная готовность

    Проверьте:

  • Probes разделены:
  • 1) liveness = “процесс жив” 2) readiness = “модель готова обслуживать” 3) startup = “долгий старт допустим”
  • requests/limits соответствуют реальному потреблению; сервис не BestEffort.
  • Для GPU:
  • 1) запрашивается nvidia.com/gpu 2) есть nodeSelector/affinity и tolerations (если GPU‑ноды tainted) 3) нет случайной конкуренции нескольких Pod’ов за одну GPU
  • Rollout стратегия учитывает cold start (maxSurge/maxUnavailable/timeout).
  • Сигнал “готово”: новый pod не получает трафик, пока модель не загружена и не прогрета.

    3) Шаблон “production pack” для репозитория

    Ниже — минимальный набор артефактов, который удобно копировать между проектами.

    Два обязательных “манифеста состояния” (в любом виде):

  • Release manifest: что именно деплоится (image digest, chart version, values revision, model_version).
  • SLO manifest: какие SLO считаются целевыми для ключевых endpoint’ов и профилей (коротко, без простыней).
  • 4) Быстрый pre‑release прогон (10–15 минут)

    Перед выкладкой в stage/prod выполните короткий сценарий:

  • Собрать образ и запустить smoke внутри контейнера.
  • Поднять стенд интеграции и убедиться, что /health/ready становится 200 только после прогрева.
  • Прогнать contract‑golden кейсы.
  • Прогнать load smoke:
  • 1) steady 2–5 минут 2) burst 30–60 секунд
  • Проверить, что метрики/логи дают минимальную диагностику:
  • 1) p95/p99 2) error_code 3) queue/batch wait (если используется)

    Смысл этого прогона — поймать «дорогие» ошибки: сломанный образ, сломанный контракт, отсутствие backpressure, взрыв p99 из‑за очереди.

    ---

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

    1) Возьмите ваш сервис (или референс из курса) и составьте список из 12 пунктов readiness‑чеклиста: по 2 пункта на каждый контур: 1) API контракт 2) runtime lifecycle 3) backpressure 4) observability 5) security 6) CI/CD

    2) Составьте “release manifest” для вашего проекта (8–10 полей), который позволит воспроизвести деплой и откат.

    3) Опишите минимальный post‑deploy smoke (5 проверок), который укладывается в 1–2 минуты и ловит “сервис ready, но инференс сломан”.

    4) Придумайте 3 типовых причины «readiness flapping» в ML‑serving и по одному способу быстро отличить их друг от друга (по логам/метрикам).

    <details> <summary> Ответы </summary>

    1) Пример 12 пунктов (по 2 на контур):

  • API:
  • 1. OpenAPI соответствует DTO и контрактным тестам (golden). 2. Все ошибки возвращаются в одном формате с error_code.

  • Runtime:
  • 1. Модель грузится и прогревается в lifecycle, не в handler. 2. Readiness зависит от флага “warmup ok”.

  • Backpressure:
  • 1. Есть лимит in‑flight/очереди, при переполнении — 429/503. 2. Тяжёлые запросы не могут «обойти» лимиты (max tokens / max image side).

  • Observability:
  • 1. В логах есть request_id, event, status_code/error_code. 2. Есть histogram’ы: http latency + infer latency (+ queue wait при наличии).

  • Security:
  • 1. Endpoint’ы требуют auth (JWT/mTLS/ключ) по принятой политике. 2. Payload limits и rate limits согласованы на ingress и в приложении.

  • CI/CD:
  • 1. PR gates блокируют контракт и сборку образа. 2. Деплой использует digest и ждёт rollout по readiness.

    2) Пример release manifest (поля):

  • service_name
  • git_sha
  • image_ref
  • image_digest
  • chart_name
  • chart_version
  • values_file (или commit/ревизия values)
  • model_id
  • model_version
  • deployed_by / deployed_at
  • 3) Пример post‑deploy smoke (5 проверок):

  • kubectl rollout status завершён.
  • GET /health/ready отвечает 200.
  • Лёгкий sync inference запрос (минимальный вход) с коротким timeout.
  • Ответ содержит обязательные поля (request_id, model, result).
  • Нет всплеска 5xx/internal_error за короткое окно (по метрикам или по лог‑счётчику).
  • 4) Примеры readiness flapping:

  • Runtime не успевает прогреться из‑за too aggressive startupProbe/readinessProbe таймингов.
  • - Отличить: логи старта показывают, что warmup ещё идёт, а probes уже фейлятся.

  • Readiness проверяет внешнюю зависимость (например, БД), и она временно недоступна.
  • - Отличить: метрики/трейсы показывают ошибки подключения к зависимости, при этом сам процесс жив.

  • OOM/pressure: контейнер периодически перезапускается, readiness кратко становится true/false.
  • - Отличить: события Kubernetes + рост рестартов + memory usage уходит в limit.

    </details>

    5. Нефункциональные требования: latency, throughput, SLO, error budget

    Нефункциональные требования: latency, throughput, SLO, error budget

    Нефункциональные требования (NFR) — это то, как сервис работает под нагрузкой и в эксплуатации: задержка, пропускная способность, доступность, устойчивость к ошибкам и предсказуемость поведения. Для ML‑serving (LLM/CV) NFR часто важнее, чем «ещё +0.5% accuracy», потому что именно NFR определяют, можно ли встроить модель в продукт и поддерживать её без постоянных инцидентов.

    В предыдущих статьях мы уже говорили про наблюдаемость, backpressure и контракты API. Здесь фокус — как формализовать требования через измеримые показатели и управлять компромиссами.

    1) Latency: что именно измеряем и чем опасны “хвосты”

    Latency — время от запроса до ответа. Важно различать:

  • Server-side latency: внутри сервиса (от получения запроса до отправки ответа).
  • End-to-end latency: глазами клиента (включает сеть, gateway/ingress, retries).
  • Для production‑требований обычно фиксируют server-side как основной SLI (проще контролировать) и дополнительно мониторят end-to-end как “product metric”.

    1.1. Почему среднее почти бесполезно

    В high load критичны перцентили: p95/p99. Причина проста: пользователи и интеграции страдают не от «среднего запроса», а от редких медленных, которые:

  • вызывают таймауты у клиентов;
  • провоцируют ретраи;
  • увеличивают нагрузку (и тем самым ухудшают ситуацию).
  • Практическое правило формулировки NFR по задержке:

  • задавайте целевой перцентиль (обычно p95 или p99);
  • задавайте окно агрегации (например, 5 минут);
  • фиксируйте условия нагрузки (хотя бы порядок RPS и типы запросов).
  • Пример формулировки: «p95 server-side latency для /v1/embeddings ≤ 120 мс при 200 RPS и payload до 4 KB».

    1.2. Latency‑бюджет по стадиям (без микроменеджмента)

    Чтобы latency можно было улучшать, полезно держать бюджет по стадиям (не обязательно как строгий контракт, но как ориентир):

  • парсинг/валидация;
  • препроцессинг;
  • ожидание очереди/батчера;
  • инференс;
  • постпроцессинг/сериализация.
  • Это не дублирует “observability”, а задаёт целевую структуру задержки: если очередь внезапно “съела” половину бюджета — вы знаете, где искать.

    1.3. Специфика LLM и CV

  • LLM: задержка часто пропорциональна объёму работы (токены входа + токены генерации). Поэтому один общий SLO на latency без указания лимитов обычно бессмысленен.
  • CV: latency часто скачет из-за декодирования и размера изображения; NFR должны учитывать ограничения на разрешение/формат.
  • 2) Throughput: сколько запросов в секунду “по-настоящему” выдерживаем

    Throughput — пропускная способность: сколько запросов/сек (RPS/QPS) сервис может обслужить.

    Ключевое слово: устойчиво. Throughput без привязки к задержке — ловушка. Правильная постановка звучит так:

  • «Сервис обеспечивает 300 RPS при p95 ≤ 200 мс и error rate ≤ 0.1% на профиле fast».
  • 2.1. Saturation point и “нечестный” throughput

    Частая ошибка: замерить максимальный RPS, при котором сервис ещё отвечает, но при этом:

  • p99 растёт в разы;
  • очередь раздувается;
  • процент ошибок увеличивается.
  • Это не production throughput. Production throughput — это максимум, который достигается внутри SLO.

    2.2. Связь throughput, latency и количества “в полёте” (Little’s Law)

    Полезная оценка для планирования мощности и лимитов очереди — закон Литтла:

    Где:

  • — среднее число запросов «в системе» (в обработке + в очереди), то есть in‑flight.
  • — средняя интенсивность входа (RPS).
  • — среднее время пребывания запроса в системе (сек), то есть средняя latency.
  • Как читать формулу: если входной поток растёт, а время обработки не уменьшается, то число запросов «в полёте» растёт линейно — и вы упираетесь в память/очередь/таймауты.

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

    3) SLI/SLO/SLA: как превратить “хочу быстро” в проверяемое обещание

    3.1. Термины

  • SLI (Service Level Indicator) — измеряемый показатель (например, доля запросов быстрее 300 мс).
  • SLO (Service Level Objective) — целевое значение SLI (например, 99% запросов быстрее 300 мс за 30 дней).
  • SLA — внешнее юридическое/коммерческое обещание. Внутренний SLO обычно строже, чем SLA.
  • 3.2. Хорошие SLI для ML‑serving

    Чтобы SLO был управляемым, выбирайте SLI, которые:

  • отражают пользовательский опыт;
  • измеряются автоматически;
  • не ломаются от изменения внутренней реализации.
  • Практичные варианты:

  • Latency SLI: доля запросов, уложившихся в порог (например, ≤ 300 мс).
  • Availability/Success SLI: доля успешных ответов.
  • Correctness-as-contract SLI: доля ответов, соответствующих контракту (валидная схема, не пустое поле, корректный тип) — особенно полезно при частых изменениях моделей.
  • Важный момент для ML: решить, считать ли 429 (backpressure) ошибкой в SLI. Два распространённых подхода:

  • 429 считается “неуспехом” (строгое требование к доступности).
  • 429 считается отдельной метрикой “capacity exceeded” и выводится в отдельный SLO (если сервис честно сигнализирует перегруз и это ожидаемо).
  • Главное — выбрать подход один раз и закрепить его в эксплуатации.

    3.3. Разные SLO на разные режимы

    Смешивать в одном SLO синхронный инференс и async/job‑based — обычно ошибка.

  • Для sync: SLO по latency и success.
  • Для async: SLO по времени до готовности результата (например, p95 “job completion time”) и по доле успешно завершённых jobs.
  • 4) Error budget: как управлять скоростью изменений без саморазрушения

    Error budget — это допустимое количество “неидеальных” запросов в рамках SLO. Он превращает абстрактное «будь надёжным» в управляемый ресурс.

    4.1. Как посчитать budget

    Если SLO по доступности: «99.9% успешных запросов за 30 дней», то допустимая доля ошибок:

    Где:

  • — целевое значение (например, 0.999).
  • — бюджет ошибок (например, 0.001).
  • Дальше переводим в количество запросов. Если за 30 дней пришло запросов, допустимое число “плохих”:

    Где:

  • — общее число запросов в окне.
  • — доля допустимых нарушений.
  • — число запросов, которые могут быть ошибочными/медленными (в зависимости от определения SLI).
  • Важно: в “плохие” могут входить не только 5xx, но и “слишком медленно” — если latency включена в SLI.

    4.2. Burn rate: скорость сгорания бюджета

    Error budget ценен тем, что позволяет принимать решения:

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

    4.3. Как это применять к ML‑сервису

    Типовые “правила движения”, которые реально работают:

  • Если burn rate высокий — запрещаем/ограничиваем релизы модели (или включаем только canary) до стабилизации.
  • Если budget в норме — можно выкатывать оптимизации, менять runtime, вводить кеши.
  • Для LLM: часто вводят отдельные SLO на разные профили (fast vs quality), чтобы дорогие запросы не “съедали” бюджет у всего сервиса.
  • 5) Как ставить реалистичные NFR (и не промахнуться)

    5.1. Начинайте с “контекста нагрузки”, а не с красивых чисел

    Без контекста NFR не проверяемы. Минимум, который стоит фиксировать:

  • тип endpoint (sync vs async);
  • ограничения входа (размер текста/изображения);
  • профиль инференса (параметры генерации/качества);
  • ожидаемый RPS и распределение “тяжёлых/лёгких” запросов.
  • 5.2. Разделяйте “capacity target” и “SLO target”

    Полезно держать две рамки:

  • SLO target — обязательный уровень качества сервиса для пользователей.
  • Capacity target — при каких нагрузках вы гарантируете выполнение SLO.
  • Это снимает иллюзию, что SLO “сам по себе” гарантирует бесконечную масштабируемость.

    5.3. Встраивайте NFR в решения по продукту

    Для ML‑serving почти всегда есть рычаги, которые меняют latency/throughput:

  • лимиты длины/разрешения;
  • параметры генерации/точности;
  • батчинг и политика очереди;
  • деградация в перегруз.
  • Смысл NFR — зафиксировать, какой компромисс допустим. Иначе каждый инцидент превращается в спор “качество vs скорость” без критериев.

    ---

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

    1) Для endpoint’а /v1/embeddings предложите:

  • SLI по latency (как именно измеряем)
  • SLO (число + окно)
  • conditions нагрузки (что обязательно указать, чтобы требование было проверяемым)
  • 2) Пусть SLO по успешным ответам: 99.9% за 30 дней. За 30 дней пришло 80 млн запросов.

  • Посчитайте error budget в запросах.
  • Если за первые 2 дня уже случилось 60 000 “плохих” запросов, оцените, “горит” ли бюджет опасно (качественно: да/нет и почему).
  • 3) Используйте закон Литтла. Пусть средняя нагрузка RPS, а средняя server-side latency секунды.

  • Оцените среднее число in-flight запросов .
  • Как изменится , если latency ухудшится до 0.5 секунды при том же RPS?
  • 4) Вы проектируете LLM endpoint генерации. Какие два ограничения на вход/параметры вы бы обязаны включить в формулировку NFR по latency, чтобы SLO был осмысленным?

    <details> <summary> Ответы </summary>

    1) Пример.

  • SLI latency: доля запросов, у которых server-side latency ≤ 120 мс (или p95 ≤ 120 мс — но тогда SLI фактически “перцентиль”). Важно явно указать, что измеряем на стороне сервера.
  • SLO: 99% запросов укладываются в 120 мс за скользящее окно 30 дней (или 7 дней — зависит от вашей политики).
  • Conditions: лимит размера текста (например, до 2 KB), фиксированный профиль инференса (одинаковые параметры), ожидаемый RPS (например, 200), доля больших запросов (если есть), инфраструктурные условия (CPU/GPU профиль).
  • 2)

  • (доля бюджета). Тогда “плохих” запросов.
  • За 2 дня потратили 60 000 из 80 000 — это 75% месячного бюджета за ~6.7% времени месяца. Качественно: бюджет горит опасно, потому что при сохранении темпа вы исчерпаете бюджет задолго до конца окна.
  • 3)

  • . В среднем около 30 запросов одновременно находятся “в системе” (в обработке/очереди).
  • При : . In-flight вырастет до ~75, то есть сильнее нагрузит память/очередь и повысит риск хвостовых задержек.
  • 4) Пример двух обязательных ограничений для осмысленного NFR по latency в LLM:

  • Ограничение на размер входа (например, максимум токенов/символов в prompt).
  • Ограничение на объём генерации и/или параметры, влияющие на вычисления (например, max_tokens на выходе; иногда также фиксируют режим streaming vs non-streaming или профиль fast/quality).
  • </details>

    6. Проектирование endpoints: sync, async, batch, streaming

    Проектирование endpoints: sync, async, batch, streaming

    Эта статья — про поверхность инференс‑API: какие endpoints нужны, как выбрать режим (sync/async/streaming), как спроектировать batch‑контракты и как сделать поведение предсказуемым под нагрузкой. Общие принципы контрактов, версионирования и кодов ошибок уже зафиксированы в статье про API‑контракты; здесь фокус на семантике endpoints и операционных последствиях.

    1) Четыре режима инференса и когда какой выбирать

    Ниже — практическая карта решений.

    Критерии выбора

  • Sync: выбирайте, когда задержка укладывается в таймауты всей цепочки (ingress/gateway/клиент), а время инференса достаточно стабильно.
  • Async job‑based: выбирайте, когда время выполнения сильно плавает, есть риск превышать таймауты, или нужен контроль очереди/приоритизации на уровне задач.
  • Streaming: выбирайте, когда ценен perceived latency (LLM‑генерация), и клиент готов к более сложному протоколу/обработке.
  • Batch: выбирайте, когда клиенты могут отправлять пачки, либо когда вы хотите повысить throughput за счёт объединения запросов (особенно на GPU).
  • Важно: batch — это не обязательно отдельный endpoint. Часто это режим исполнения runtime (micro‑batching), который может обслуживать и sync/async.

    ---

    2) Sync endpoints: «короткий запрос → быстрый ответ»

    2.1. Что должно быть в sync‑дизайне (помимо базового контракта)

  • Жёстко определённые лимиты входа, иначе один «тяжёлый» запрос будет портить p99 всем. Для LLM это лимит контекста/генерации, для CV — ограничение на размер/разрешение после декодирования.
  • Явный профиль/режим (например, fast/quality) как часть запроса: это упрощает эксплуатацию и SLO, потому что режим становится измеримым и маршрутизируемым.
  • Отделение таймаута клиента от таймаута вычислений:
  • 1. клиентский таймаут может быть больше (учитывает сеть), 2. серверный таймаут вычислений должен быть консервативным, чтобы не копить зависшие запросы.

    2.2. Семантика ответа для production

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

  • request_id (корреляция)
  • model (какая версия реально ответила)
  • (опционально) агрегированные timings верхнего уровня
  • Не превращайте timings в «трейс в ответе» — это отдельный канал наблюдаемости.

    2.3. Отмена запроса (cancellation)

    Для sync критично понимать: клиент может оборвать соединение, но вычисления внутри процесса могут продолжаться.

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

  • сервер должен уметь быстро заметить отмену и прекратить дорогое вычисление, если runtime это поддерживает;
  • если прекратить нельзя — хотя бы не продолжать постобработку/сериализацию.
  • Иначе вы будете «жечь GPU» на запросы, которые уже никому не нужны.

    ---

    3) Async job‑based endpoints: контроль очереди и предсказуемость

    Async‑режим — это не «сделаем 202 и всё». Это мини‑протокол управления задачей.

    3.1. Минимальный набор endpoints и их смысл

  • POST /jobs — создать задачу
  • GET /jobs/{id} — получить статус/результат
  • (опционально) DELETE /jobs/{id} — отменить
  • 3.2. Модель состояний job

    Состояния стоит фиксировать как часть контракта (и логики клиента):

  • queued — принято и стоит в очереди
  • running — выполняется
  • succeeded — результат готов
  • failed — завершилось ошибкой (с нормализованным error_code)
  • canceled — отменено
  • (опционально) expired — результат удалён по TTL
  • Ключевой момент: ошибка job — это не обязательно 5xx на API. API может вернуть 200 со статусом failed и структурой ошибки задачи.

    3.3. Где хранить результат и как долго

    Async почти всегда подразумевает result store и TTL.

    Нужно определить:

  • TTL результата (сколько клиент может забрать после готовности)
  • TTL идемпотентности (если поддерживаете дедупликацию создания job)
  • лимиты на размер результата (особенно для больших выходов)
  • 3.4. Polling vs callback/webhook

    Оба способа имеют стоимость:

  • Polling проще, но генерирует фоновую нагрузку.
  • Webhook уменьшает polling‑нагрузку, но требует:
  • 1. повторных доставок, 2. подписания/аутентификации, 3. хранения статуса доставки.

    Если вы выбираете webhook, это уже отдельный контракт (и отдельный набор SLO по доставке).

    ---

    4) Batch endpoints: как не сделать «быстро, но неправильно»

    Batch нужен в двух формах:

  • Client‑side batch: клиент присылает массив входов.
  • Server‑side micro‑batch: сервис сам агрегирует одиночные запросы в батчи (runtime‑механика).
  • 4.1. Client‑side batch: контракт и ключевые решения

    Главный вопрос контракта: что делать с частичными ошибками?

    Есть два рабочих варианта:

  • All‑or‑nothing (всё либо ничего):
  • 1. проще для клиента, 2. но один плохой элемент валит весь батч, 3. полезно, когда батчи маленькие и данные «чистые».
  • Per‑item results (результат на каждый элемент):
  • 1. лучше для реальных данных, 2. требует стабильной структуры results[i] с полем ok или error.

    Также нужно заранее решить:

  • Сохранение порядка: обычно клиент ожидает results в том же порядке, что вход.
  • Лимит размера батча: выражается явно (и документируется), иначе батч станет способом обойти лимиты.
  • Смешивание параметров: либо параметры общие для всего батча, либо допускаются per‑item (второе заметно усложняет кеширование и исполнение).
  • 4.2. Server‑side micro‑batch: поведение под нагрузкой

    Micro‑batching обычно управляется двумя параметрами:

  • batch window — сколько времени ждём, чтобы собрать батч;
  • max batch size — верхняя граница.
  • Операционное следствие: batch window увеличивает задержку одиночного запроса, но повышает throughput. Поэтому важно:

  • привязать micro‑batch к конкретному профилю (например, fast может иметь маленькое окно, throughput — больше),
  • отделить «очередь ожидания батча» от «очереди перегруза», чтобы не скрывать overload за бесконечным накоплением.
  • 4.3. Batch для LLM vs CV

  • LLM: батчинг выгоден, но сложнее из‑за разной длины последовательностей (padding/pack). Часто нужны ограничения на длину, чтобы батч не деградировал.
  • CV: батчинг обычно проще (тензоры фиксированного размера после препроцессинга), но бутылочным горлышком может стать декодирование изображений, если оно сделано не там и не так.
  • ---

    5) Streaming endpoints: контракт «потока», а не одного ответа

    Streaming в serving чаще всего ассоциируется с LLM‑генерацией. Главное — воспринимать streaming как сессию событий, а не «кусочки JSON как получится».

    5.1. Что является контрактом в streaming

  • Типы событий (минимум):
  • 1. start (метаданные: request_id, model) 2. delta (очередная порция результата) 3. end (признак завершения) 4. error (структурированная ошибка)
  • Порядок: start → 0..N deltaend или error.
  • Гарантии: может ли быть частичный результат при ошибке; является ли поток «ровно один раз» или допускает повторы чанков при ретраях транспортного уровня.
  • 5.2. Backpressure и разрыв соединения

    Streaming добавляет два новых фактора перегруза:

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

  • лимит на максимальную длительность стрима;
  • лимит на объём выдачи (например, max tokens уже ограничивает это);
  • политика при медленном клиенте: либо блокируемся (рискуя накоплением), либо принудительно завершаем поток с контролируемой ошибкой.
  • 5.3. Наблюдаемость streaming

    В отличие от sync, у streaming есть два вида latency:

  • TTFT (time to first token/first chunk) — насколько быстро клиент увидел начало полезного вывода;
  • completion time — когда поток завершился.
  • Эти два показателя надо измерять отдельно, иначе вы не поймёте, где проблема: «долго стартуем» или «долго генерируем».

    ---

    6) Практическая матрица дизайна endpoints

    | Режим | Что обещаем клиенту | Главный риск под нагрузкой | Что обязательно зафиксировать в контракте | |---|---|---|---| | Sync | результат сразу | таймауты/рост p99 | лимиты входа, профиль, предсказуемые ошибки | | Async job | результат позже, по id | очереди/TTL/дедуп | состояния job, TTL, формат ошибки job | | Batch | массив результатов | частичные ошибки и лимиты | порядок, per‑item error, max batch size | | Streaming | последовательность событий | медленные клиенты, долгие соединения | типы событий, завершение, политика при разрыве |

    ---

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

    1) Для LLM‑сервиса предложите набор endpoints, который поддерживает:

  • sync генерацию
  • async генерацию как job
  • streaming генерацию
  • Укажите для каждого: «что обещает клиенту» одной фразой.

    2) Вы проектируете batch endpoint для эмбеддингов. Выберите стратегию ошибок (all‑or‑nothing или per‑item) и объясните, почему она лучше для production.

    3) Придумайте минимальную модель состояний job (5–6 статусов) и опишите, какие два статуса нельзя объединять в один, иначе будет больно в эксплуатации.

    4) Для streaming перечислите 4 типа событий и объясните, почему нельзя обойтись только delta.

    5) Опишите два разных SLI для streaming‑эндпойнта и что каждый из них диагностирует.

    <details> <summary> Ответы </summary>

    1) Пример набора:

  • Sync: POST /v1/generate — «верну полный текст в одном ответе, если уложимся в лимиты и таймауты».
  • Async: POST /v1/jobs + GET /v1/jobs/{id} — «приму задачу и позволю получить статус/результат позже по идентификатору».
  • Streaming: POST /v1/generate:stream (или отдельный /v1/stream) — «отдам последовательность событий с частями ответа до завершения или ошибки».
  • 2) Для production чаще удобнее per‑item:

  • реальные данные часто содержат единичные плохие элементы;
  • all‑or‑nothing увеличивает число повторов и нагрузку (клиент будет дробить батч и ретраить);
  • per‑item позволяет валидировать/отказывать точечно, сохраняя throughput.
  • 3) Пример статусов: queued, running, succeeded, failed, canceled, expired.

    Два статуса, которые не стоит объединять:

  • failed и canceled: отмена — управляемое действие пользователя/системы, это другой класс событий, другие метрики и другие алерты.
  • succeeded и expired: «успешно» означает, что результат доступен; «expired» означает, что job завершён, но результат больше нельзя получить (важно для клиентов и поддержки).
  • 4) Пример типов событий:

  • start — позволяет клиенту сразу узнать request_id/model и начать корреляцию.
  • delta — собственно куски результата.
  • end — явный маркер нормального завершения (иначе клиент вынужден гадать: оборвалось или закончилось).
  • error — структурированная ошибка (код/детали), а не просто «соединение закрылось».
  • Только delta недостаточно, потому что без start/end/error нет надёжной семантики сессии и диагностики.

    5) Два SLI для streaming:

  • TTFT (время до первого чанка): диагностирует проблемы старта — очередь, прогрев, токенизация/препроцессинг, ожидание слота runtime.
  • Completion time (время до end): диагностирует скорость генерации и общую длительность выполнения (например, слишком большие max_tokens, перегруз GPU, медленный клиент, backpressure).
  • </details>

    7. Структура репозитория и политика качества кода

    Структура репозитория и политика качества кода

    Production ML‑serving на FastAPI (LLM/CV) быстро перестаёт быть «приложением с парой роутов»: появляются профили инференса, лимиты, очередь/батчинг, observability, несколько runtime’ов, инфраструктурные манифесты и CI/CD. Чтобы это не превратилось в неуправляемую смесь, нужны:

  • предсказуемая структура репозитория (где что лежит и почему);
  • политика качества кода (какие автоматические «ворота» не дают деградировать базе).
  • Архитектурные границы (слои/порты/адаптеры) уже обсуждались в статье про Clean/Hexagonal/DDD‑lite, а контракт API — в статье про OpenAPI и backward compatibility. Здесь фокус — как это приземлить в репозиторий и в ежедневную инженерную дисциплину.

    ---

    1) Принципы структуры репозитория для ML‑serving

    Хорошая структура отвечает на три вопроса:

  • Где ядро логики инференса? (не зависит от FastAPI, Redis, конкретного runtime).
  • Где инфраструктура? (Docker/K8s/CI, конфиги окружений, скрипты).
  • Где «границы продукта»? (API‑контракт, версии, миграции форматов, тестовые фикстуры).
  • Практические правила:

  • Код приложения не должен «тащить» инфраструктуру внутрь (например, манифесты K8s не лежат рядом с use case).
  • Вся «склейка зависимостей» делается в одном месте (bootstrap/wiring), чтобы не разводить синглтоны по проекту.
  • Контракт API живёт рядом с inbound‑адаптером, а доменные типы — отдельно.
  • Тестовые данные и golden‑кейсы — версиями в репозитории (иначе регрессии в формате/поведении не ловятся).
  • ---

    2) Референс‑структура репозитория

    Ниже — вариант, который хорошо масштабируется под high load serving и соответствует идее «ядро + адаптеры».

    Почему именно так

  • app/domain — «истина» о предметной области инференса: типы запросов/ответов, политики лимитов, доменные ошибки (не HTTP‑коды).
  • app/ports фиксирует контракты зависимостей. Если завтра меняется runtime (PyTorch → ONNX), меняется адаптер, а не use cases.
  • app/use_cases — место, где реально живёт orchestration инференса: проверка политики → опциональный кеш → вызов runtime → постобработка.
  • app/adapters разделяет inbound (HTTP/worker) и outbound (runtime/кеш/артефакты/observability). Это снижает риск «протечек» фреймворка в ядро.
  • deployments и ci позволяют держать инфраструктуру рядом, но не смешивать с приложением.
  • ---

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

    Политика качества — это не «рекомендации», а набор проверок, которые выполняются одинаково локально и в CI.

    3.1. Минимальный набор quality gates

  • Форматирование: единый формат кода, без споров в PR.
  • Линтинг: стиль, потенциальные ошибки, запреты на опасные конструкции.
  • Type checking: защита интерфейсов портов/DTO/настроек.
  • Тесты: минимум — unit + contract, интеграционные по необходимости.
  • Сборка: образ/пакет должен собираться воспроизводимо.
  • Важно: нагрузочные тесты обычно не «блокируют» каждый PR, но должны быть стандартизированы и запускаться регулярно/перед релизом.

    3.2. Что считаем «Definition of Done» для production serving

  • Любое изменение, затрагивающее формат входов/выходов, обновляет contract‑тесты и golden‑кейсы.
  • Любое изменение runtime/батчинга/лимитов добавляет или обновляет тест на соответствующую политику (например, «слишком большой input → контролируемая ошибка»).
  • Любая новая конфигурация имеет typed‑представление (settings) и дефолты/валидацию.
  • Любая новая метрика/лог‑поле не тащит PII и не ломает структуру логов.
  • ---

    4) Стандарты тестирования именно для ML‑serving

    Чтобы тесты реально помогали, их нужно разделить по назначению.

    4.1. Unit: быстрые и «жёсткие»

  • Тестируем доменные политики: лимиты, выбор профиля, маппинг ошибок.
  • Use case тестируем через заглушки портов (runtime/cache/artifacts).
  • Никакого GPU и тяжёлых моделей: иначе тесты перестают быть быстрыми.
  • 4.2. Contract: защита API‑контракта

  • Проверяем коды ответов и форму ошибок.
  • Проверяем стабильность ключевых полей ответа (например, наличие request_id, model).
  • Golden‑кейсы фиксируют формат (не производительность).
  • 4.3. Integration: «сквозняк» без нагрузки

  • Прогон сервиса целиком с тестовыми зависимостями.
  • Проверка, что wiring зависимостей корректен (bootstrap).
  • Smoke‑тест модели/рантайма на малых входах.
  • 4.4. Load: отдельно от CI по PR

  • Сценарии нагрузки хранятся как код/конфиги.
  • Результаты сравниваются с базовой линией (хотя бы вручную по отчётам).
  • ---

    5) Правила кодовой базы: чтобы она не «расползалась»

    5.1. Правила импортов и зависимостей

  • domain не импортирует FastAPI/Starlette/Pydantic DTO, клиенты Redis/брокера, ML‑библиотеки.
  • use_cases зависят только от domain и ports.
  • adapters могут зависеть от внешних библиотек, но не тащат их внутрь domain.
  • Практический приём: ввести простую проверку «слоёв» (скрипт/линтер‑правило), чтобы запрещённые импорты ловились автоматически.

    5.2. Стандарты ошибок

  • Внутри ядра используем доменные ошибки (типы/коды), без HTTP‑семантики.
  • В HTTP‑адаптере делаем единый маппинг в формат ошибок API.
  • Логи и метрики используют стабильные error_code, а не текстовые сообщения.
  • 5.3. Управление конфигурацией

  • Настройки идут через typed settings (из env), без чтения env «где попало».
  • Любой параметр, влияющий на результат (профиль, лимиты, параметры генерации), должен быть явно отражён на уровне use case и/или DTO (согласно вашему контракту).
  • ---

    6) Процесс разработки: pre-commit, PR и релизные «предохранители»

    6.1. Локальные проверки (до PR)

  • Форматирование + линт + typecheck запускаются автоматически на коммите.
  • Быстрые unit/contract тесты запускаются локально одной командой.
  • Цель: разработчик не тратит время CI на очевидные ошибки.

    6.2. Правила PR

  • PR маленькие по размеру (иначе review превращается в формальность).
  • В PR описывается причина изменения и риск (например, «меняем политику лимитов», «меняем runtime»).
  • Изменения контракта помечаются явно и сопровождаются обновлением contract/golden.
  • 6.3. Политика «не деградировать производительность молча»

  • Критичные параметры (лимиты, batch window, max batch size) не меняются без явного упоминания в PR.
  • Для рискованных изменений используйте feature flag/конфиг‑переключатель, чтобы уметь быстро откатить поведение без пересборки.
  • ---

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

    1) Предложите свою структуру tests/ для serving‑сервиса и объясните, какие тесты обязаны быть быстрыми и почему.

    2) Укажите 6 «quality gates», которые вы сделаете блокирующими в CI для merge в main.

    3) Сформулируйте 5 запретов на импорты/зависимости для domain и use_cases (в стиле «слой X не импортирует Y»).

    4) Опишите минимальный набор артефактов, которые должны жить в репозитории в папке deployments/ для воспроизводимого деплоя.

    <details> <summary> Ответы </summary>

    1) Пример структуры tests/:

  • unit/: доменные политики (лимиты, профили) и use cases через заглушки портов — должны быть быстрыми, потому что это основной цикл разработки.
  • contract/: проверки схем/ошибок/стабильных полей ответа и golden‑кейсы — быстрые, потому что изменения API происходят часто и их нужно ловить рано.
  • integration/: поднятие приложения целиком и smoke‑прогон рантайма — могут быть медленнее, но должны быть стабильными.
  • load/: нагрузочные сценарии — отдельно от PR, потому что они долгие и зависят от окружения.
  • 2) Пример 6 блокирующих gates в CI:

  • форматирование (код не должен расходиться со стандартом),
  • линтинг (ошибки/антипаттерны),
  • typecheck (контракты портов и DTO),
  • unit‑тесты,
  • contract‑тесты,
  • сборка артефакта (например, Docker image build) — чтобы не было «в main не собирается».
  • 3) Пример запретов:

  • domain не импортирует FastAPI/Starlette типы.
  • domain не импортирует Pydantic DTO из inbound‑HTTP слоя.
  • domain не импортирует клиентов внешних систем (Redis/S3/брокер).
  • domain не импортирует конкретные ML‑библиотеки (torch/onnxruntime).
  • use_cases не должны создавать конкретные адаптеры напрямую (никаких Redis(...) внутри use case), только работать через порты.
  • 4) Пример минимального набора в deployments/:

  • Dockerfile(ы) и entrypoint/скрипты запуска.
  • K8s манифесты: Deployment/Service/Ingress (или аналоги), ресурсы и probes.
  • Настройки окружений (например, base + overlays), чтобы dev/stage/prod отличались конфигом, а не ручными правками.
  • </details>

    8. Python toolchain: uv/poetry, pinned deps, reproducible builds

    Python toolchain: uv/poetry, pinned deps, reproducible builds

    Production ML‑serving отличается от «просто приложения на FastAPI» тем, что любой дрейф окружения (версии Python, transitive‑зависимости, системные библиотеки, колёса под платформу) быстро превращается в непредсказуемые ошибки: от внезапных ImportError до деградации latency.

    В статье про структуру репозитория и quality gates мы зафиксировали, что сборка должна быть воспроизводимой и проверяемой в CI. Здесь — практическая дисциплина toolchain и зависимостей, которая делает это реальностью.

    ---

    1) Что значит «воспроизводимая сборка» для ML‑serving

    В контексте сервиса инференса «reproducible build» — это не академическая «бит‑в‑бит одинаковость», а гарантия:

  • Одинаковый набор Python‑пакетов (включая транзитивные зависимости) при сборке сегодня и через месяц.
  • Одинаковая совместимость по платформе: Python minor version, CPU/GPU варианты, ABI, системные библиотеки.
  • Одинаковая процедура установки (без “магии” вроде pip install -U или подтягивания свежих версий во время билда).
  • Практический критерий: если вы пересобрали образ из того же git‑коммита, он должен ставиться и запускаться одинаково (а поведение различается только из‑за внешних факторов, а не из‑за «уплывших» зависимостей).

    ---

    2) uv и Poetry: роли, отличия, когда что выбирать

    2.1. Две модели управления зависимостями

    Poetry исторически решает две задачи одновременно:

  • управление зависимостями и lockfile;
  • упаковка проекта (package metadata, версии, публикация).
  • uv чаще используют как быстрый и строгий инструмент для:

  • установки зависимостей;
  • создания/использования lockfile;
  • синхронизации окружения в режиме “frozen”.
  • Оба подхода валидны. Важно не название инструмента, а то, какой артефакт является «источником правды» и как вы гарантируете неизменность зависимостей.

    2.2. Практические плюсы/минусы в ML‑serving

    uv удобно, когда вам критичны скорость и повторяемость сборки контейнера:

  • быстрые установки в CI;
  • удобный режим строгой синхронизации по lockfile;
  • меньше «скрытых» шагов.
  • Poetry удобно, когда:

  • вам важно вести проект как пакет (версионирование, группы зависимостей, единый workflow команды);
  • вы хотите единый интерфейс для dev‑окружения (линтеры/тесты) и production‑зависимостей.
  • 2.3. Главное правило выбора

    Выберите один из двух «источников правды» и сделайте его обязательным:

  • либо poetry.lock и установка строго из него;
  • либо uv‑lock (например, uv.lock) и установка строго из него.
  • Смешивание возможно (например, Poetry для управления проектом, а uv для быстрой установки), но тогда нужно явно описать, какой lockfile главный, и запретить «обновлять зависимости случайно».

    ---

    3) Pinned deps: что именно надо фиксировать

    3.1. Фиксация прямых и транзитивных зависимостей

    Запись в стиле fastapi>=0.100 — это не production‑pinning. В production важно зафиксировать:

  • direct dependencies (то, что вы явно объявили);
  • transitive dependencies (то, что подтянулось как зависимости зависимостей).
  • Lockfile как раз и фиксирует полный граф.

    3.2. Python minor version тоже часть «зависимостей»

    Для воспроизводимости в serving стоит фиксировать:

  • версию Python в контейнере (например, 3.11. vs 3.12. — это не мелочь);
  • локально (в команде) — через .python-version/настройки окружения или явное требование в проекте.
  • Иначе вы получаете ситуацию: «на ноутбуке ставится, в CI падает» или наоборот.

    3.3. Hashes и защита от «подмены» артефактов

    Для строгой повторяемости и безопасности полезно, чтобы установка зависимостей могла проверять хэши артефактов (колёс/тарболов). Смысл простой:

  • lock фиксирует не только версии;
  • но и какие именно файлы допустимы к установке.
  • В production‑контейнерах это снижает вероятность неприятных сюрпризов и делает сборку более детерминированной.

    3.4. Отдельная боль ML: CPU/GPU варианты

    ML‑зависимости часто имеют разные сборки под разные окружения (CPU‑only, CUDA версии, разные платформы). Поэтому важно:

  • не «надеяться» на автоматический выбор правильного варианта;
  • закреплять совместимые версии на уровне lock/constraints;
  • собирать образы под конкретный runtime (CPU образ отдельно, GPU образ отдельно), а не пытаться одним образом покрыть всё.
  • ---

    4) Reproducible builds в Docker: практический шаблон

    В production чаще всего воспроизводимость достигается не одной настройкой, а цепочкой решений.

    4.1. Детерминированный пайплайн установки

    Минимальная схема для контейнера:

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

  • кеш Docker слоёв работал предсказуемо;
  • изменение кода не триггерило «переустановку мира».
  • 4.2. “Frozen” режим — обязательный

    Независимо от uv/Poetry, вам нужен режим:

  • который падает, если lockfile не соответствует декларациям;
  • который не обновляет версии сам.
  • Это защищает от сценария: «в main смержили изменение, а сборка вдруг подтянула свежие транзитивные версии и поведение изменилось».

    4.3. Wheels‑стратегия (ускорение и стабильность)

    Для high load сервиса вы почти неизбежно захотите ускорять сборку и снижать риск “build from source”:

  • предпочитать колёса вместо сборки из исходников;
  • по возможности использовать wheel‑cache в CI;
  • разделять этап “build wheels” и “install wheels” (особенно если есть тяжёлые зависимости).
  • Это важно, потому что сборка из исходников зависит от системного toolchain и может стать источником недетерминизма.

    ---

    5) Системные зависимости: скрытый источник «невоспроизводимости»

    Python‑lockfile не фиксирует версии пакетов ОС. А именно они часто ломают ML‑образы (libstdc++, glibc, libjpeg, libglib и т.д.).

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

  • Не использовать “latest” для базового образа; фиксировать конкретный тег.
  • Если ставите пакеты через пакетный менеджер ОС — фиксируйте версии там, где это реалистично, и минимум фиксируйте базовый образ.
  • Старайтесь избегать сборки C/C++ зависимостей «на лету» в production‑образе: лучше колёса или отдельный builder stage.
  • ---

    6) Группы зависимостей: dev/test/prod без дрейфа

    Чтобы не тащить в production линтеры/pytest и при этом не терять воспроизводимость:

  • держите явное разделение групп (prod vs dev/test);
  • следите, чтобы lockfile покрывал все группы, но установка в runtime‑образе происходила только для prod‑части;
  • запрещайте «случайный импорт dev‑зависимостей» в production‑коде (это уже относится к quality gates из предыдущих материалов).
  • Типичный запах проблем: сервис локально работает, а в контейнере падает, потому что “случайно” использовал пакет, который есть только в dev‑группе.

    ---

    7) Политика обновления зависимостей (чтобы не разрушить стабильность)

    Pinned deps неизбежно устаревают. В production подход обычно такой:

  • обновления делаются пакетно и осознанно (не «по пути»);
  • обновление lockfile — отдельный вид изменения, который проходит тесты и (желательно) базовую нагрузку;
  • для ML‑runtime и численных библиотек обновления планируются аккуратнее, чем для «обычных» web‑пакетов.
  • Хорошая практика: любая правка lockfile в PR явно помечается как изменение окружения.

    ---

    8) Частые ошибки, которые ломают reproducibility

  • pip install -U ... в Dockerfile или скриптах — обновляет мир непредсказуемо.
  • Отсутствие lockfile в репозитории или его «нестрогое» использование.
  • Разные версии Python локально и в контейнере.
  • Сборка пакетов из исходников без фиксированного системного toolchain.
  • Один образ “и для CPU, и для GPU” с условной логикой — почти всегда ведёт к конфликтам версий.
  • ---

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

    1) Опишите, какие артефакты вы считаете «источником правды» для зависимостей в вашем сервисе: pyproject/lockfile/constraints. Почему?

    2) Вам нужно обеспечить, чтобы Docker‑сборка падала, если кто-то изменил pyproject, но забыл обновить lockfile. Какие два механизма вы введёте (инструментальные или процессные)?

    3) Перечислите 5 параметров окружения, которые влияют на воспроизводимость ML‑serving (не только Python‑пакеты).

    4) В команде спорят: «хранить dev‑зависимости вместе с prod или разделять?». Дайте решение для production‑serving и два аргумента.

    5) Придумайте правило PR‑ревью для изменений lockfile, которое снижает риск регрессий в latency/стабильности.

    <details> <summary> Ответы </summary>

    1) Источник правды обычно один из вариантов:

  • poetry.lock (или аналог lockfile) как полный граф зависимостей + pyproject.toml как декларация. Почему: декларация описывает намерения, а lock фиксирует конкретику для повторяемой установки.
  • uv.lock (или аналог) + pyproject.toml по той же логике.
  • Constraints (например, отдельный файл ограничений) — как дополнительный механизм, но не как единственный источник правды, потому что constraints обычно не фиксируют полный граф автоматически.
  • 2) Два механизма:

  • Инструментальный: установка/синхронизация в режиме “frozen”, который проверяет соответствие lockfile декларации и завершается ошибкой при расхождении.
  • Процессный/CI: отдельная проверка в пайплайне, что после команды генерации lockfile репозиторий остаётся “clean” (нет изменений в lockfile). Это ловит ситуацию «забыли закоммитить обновлённый lockfile».
  • 3) Примеры факторов воспроизводимости:

  • версия Python (minor version);
  • базовый Docker image и его OS‑библиотеки;
  • архитектура (x86_64 vs aarch64);
  • CPU/GPU стек (наличие CUDA и её версия);
  • наличие/версия системного компилятора и build‑инструментов (если что-то собирается из исходников).
  • 4) Решение: разделять группы зависимостей, но управлять ими через один инструмент и единый lockfile.

    Два аргумента:

  • production‑образ меньше и безопаснее (меньше лишних пакетов и потенциальных уязвимостей);
  • ниже риск скрытой зависимости от dev‑пакетов (то, что случайно установлено у разработчика, не должно быть условием работоспособности сервиса).
  • 5) Пример правила:

  • Любой PR с изменением lockfile должен явно описывать причину (обновление безопасности, обновление конкретной библиотеки, смена Python версии и т.д.).
  • Для PR с тяжёлыми ML‑зависимостями (runtime, numpy/torch/onnxruntime и т.п.) требуется приложить результат хотя бы одного воспроизводимого smoke‑прогона (и/или базового latency‑замера), чтобы изменения окружения не прошли «втихую».
  • </details>

    9. FastAPI проект: app factory, routers, dependency injection

    FastAPI проект: app factory, routers, dependency injection

    В production ML‑serving (LLM/CV) FastAPI‑код должен быть предсказуемо собираемым, тестируемым и управляемым по жизненному циклу. Ранее мы уже обсуждали архитектурные границы (ядро/порты/адаптеры) и требования к production. Здесь приземлим это на практику FastAPI‑проекта: app factory, роутеры и dependency injection (DI).

    1) App factory: зачем и что это такое

    App factory — это функция, которая создаёт экземпляр FastAPI и регистрирует всё необходимое (роутеры, middleware, обработчики ошибок, зависимости, lifecycle‑хуки).

    Почему это важно в production:

  • Позволяет создавать приложение с разной конфигурацией (dev/stage/prod) без копипасты.
  • Упрощает тесты: вы создаёте app, подменяете зависимости и изолируете окружение.
  • Избавляет от «магических» глобальных синглтонов, которые плохо контролируются (особенно при нескольких воркерах).
  • 1.1. Минимальный скелет app factory

    Ключевая идея: create_app() — единственная точка, где вы собираете приложение.

    1.2. Где хранить зависимости: app.state или глобальные переменные

    В production‑коде избегают «глобальных» объектов вида model = load() на уровне модуля, потому что:

  • трудно управлять порядком инициализации;
  • сложно тестировать и подменять;
  • неочевидно, что происходит при импортах.
  • Практичный вариант — держать контейнер зависимостей в app.state и предоставлять доступ через dependency‑функции.

    2) Lifecycle: startup/shutdown и lifespan

    ML‑serving почти всегда имеет тяжёлую инициализацию: загрузка модели, прогрев, подготовка кэшей/клиентов и т.д. Делайте это не в обработчиках, а в lifecycle.

    2.1. Предпочтительно: lifespan контекст

    lifespan удобен тем, что централизует инициализацию/закрытие ресурсов.

    И подключение:

    Зачем это нужно:

  • Вы отделяете «сервис готов» от «процесс запустился».
  • У вас появляется естественное место для clean shutdown.
  • 3) Routers: как дробить API без хаоса

    3.1. Один файл с роутами — плохо масштабируется

    Когда всё лежит в main.py, быстро появляется смесь:

  • DTO (Pydantic)
  • правила валидации/лимитов
  • вызовы use case
  • маппинг ошибок
  • Это мешает тестам и изменяемости.

    3.2. Референс‑паттерн: APIRouter по версии и по “bounded context”

    Плюсы:

  • Версия API фиксируется структурой (api_v1), а не размазана по коду.
  • Отдельные группы endpoint’ов легко тестировать и сопровождать.
  • 3.3. Router‑level dependencies

    FastAPI позволяет навесить зависимости на весь роутер (например, корреляцию, проверку API‑ключа, лимиты размера запроса):

    Это снижает риск забыть “обязательные” политики на одном из endpoint’ов.

    4) Dependency Injection в FastAPI: как сделать правильно для production

    FastAPI DI построен вокруг Depends(...). В production важно понимать: DI — это не магия, а способ управлять:

  • временем жизни объектов;
  • подменяемостью (тесты/окружения);
  • границами слоёв.
  • 4.1. Разделите зависимости на уровни

    Практичная классификация:

  • Request‑scope: данные запроса, request_id, текущий пользователь/класс обслуживания.
  • Application‑scope: use case, порты (runtime/cache/observability), клиенты внешних систем.
  • Config‑scope: настройки и профили.
  • Принцип: дорогие объекты создаются один раз на процесс, а не на каждый запрос.

    4.2. Доступ к контейнеру через Request

    Endpoint вызывает use case через Depends(get_generate_use_case).

    Плюсы:

  • endpoint остаётся тонким;
  • легко подменять use case/порты в тестах;
  • DI не «протекает» в домен.
  • 4.3. Не делайте тяжёлую работу внутри dependencies

    Антипаттерн для high load:

  • dependency, которая в каждом запросе создаёт клиента Redis/S3;
  • dependency, которая лениво грузит модель;
  • dependency, которая делает сложный препроцессинг.
  • Dependencies должны быть либо:

  • лёгкими (получить объект из контейнера);
  • чисто request‑уровня (достать заголовок, провалидировать лимит).
  • 4.4. DI и типичные ошибки конкурентности

    Если вы кладёте runtime/клиенты в application‑scope, убедитесь, что:

  • объект потокобезопасен (если используется многопоточность);
  • объект корректно ведёт себя при нескольких воркерах (каждый воркер — отдельный процесс и отдельная память).
  • Это не «ошибка FastAPI», а следствие модели запуска ASGI‑сервера.

    5) Wiring: как соединить порты/адаптеры в контейнере

    Контейнер — это место, где вы собираете конкретные реализации портов (runtime/cache/observability) и создаёте use case.

    Важный дисциплинарный момент: use case получает интерфейсы/порты, а не “импортирует Redis/torch напрямую”.

    6) Тестирование: dependency overrides как ключевой инструмент

    FastAPI позволяет подменять зависимости через app.dependency_overrides. Это идеально ложится на подход «тонкие endpoint’ы + use case через Depends».

    В итоге вы тестируете:

  • HTTP контракт и маппинг ошибок — без реальной модели;
  • бизнес‑логику use case — отдельно через unit‑тесты (без FastAPI).
  • Это напрямую поддерживает архитектурные границы из прошлых материалов.

    ---

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

    1) Опишите, какие 5 вещей вы регистрируете внутри create_app() (по категориям), и какие 3 вещи вы сознательно не делаете в create_app().

    2) У вас есть тяжёлая модель (инициализация 30 секунд). Где именно вы делаете загрузку и прогрев: в обработчике, в dependency или в lifespan? Почему?

    3) Придумайте структуру api_v1/ для сервиса, который имеет:

  • /health
  • /generate
  • /embeddings
  • Укажите, какие роутеры будут и где будут лежать DTO.

    4) Приведите пример dependency, которая должна быть request‑scope, и пример dependency, которая должна быть application‑scope. Объясните, что сломается, если перепутать.

    5) В тестах вы хотите:

  • отключить реальный runtime модели;
  • оставить настоящие Pydantic‑схемы и формат ошибок.
  • Какие зависимости вы будете override’ить и почему именно их?

    <details> <summary> Ответы </summary>

    1) Пример.

    Что регистрировать в create_app():

  • Роутеры (include_router) и префиксы версий.
  • Middleware (request_id, логирование, лимиты/защита входов).
  • Обработчики исключений (единый error‑формат).
  • Контейнер/зависимости приложения (wiring портов и use case).
  • Lifecycle (через lifespan) для инициализации/закрытия ресурсов.
  • Что не делать в create_app():

  • Не выполнять долгую загрузку модели прямо в теле create_app() (это усложняет управление readiness и перезапуски).
  • Не писать бизнес‑логику инференса (она должна быть в use case/ядре).
  • Не создавать внешние клиенты “по месту” в роутерах (создаём в контейнере).
  • 2) В lifespan: потому что это централизованная точка жизненного цикла, где корректно отделяются стадии «процесс запущен» и «сервис готов». В обработчике — получите хвостовые задержки и гонки инициализации; в dependency — рискуете скрыть тяжёлую работу и повторять её.

    3) Пример структуры:

  • api_v1/router.py — собирает подроутеры.
  • api_v1/endpoints/health.py, api_v1/endpoints/generate.py, api_v1/endpoints/embeddings.py — обработчики.
  • api_v1/dto.py или api_v1/schemas/*.py — Pydantic DTO (в inbound слое, рядом с HTTP).
  • 4) Примеры.

  • Request‑scope: get_request_id() (берёт заголовок/генерирует). Если сделать application‑scope — будет один request_id на все запросы.
  • Application‑scope: get_runtime() (возвращает уже созданный runtime из контейнера). Если сделать request‑scope и создавать runtime на каждый запрос — получите огромную деградацию latency и риск утечек/перегрузки.
  • 5) Обычно override’ят dependency, которая выдаёт use case (или порт runtime), например get_generate_use_case или get_runtime_port.

    Почему:

  • вы сохраняете реальный HTTP слой (роутинг, DTO‑валидация, формат ошибок);
  • но заменяете тяжёлую/нестабильную часть (инференс) на контролируемую заглушку.
  • </details>