Референс-архитектура: 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>