Production RecSys
~14 мин

Serving и инфраструктура

Real-time inference, кэширование, feature serving, масштабирование.

Serving и инфраструктура — как рекомендации работают на проде

Загрузка интерактивного виджета...

Модель обучена. А дальше что? 100 миллионов пользователей, каждый запрос за 100мс, тысячи RPS. Serving — это про то, как перевести модель из Jupyter в систему, которая работает 24/7 под нагрузкой. И это не «завернул в Flask» — это архитектура из 5-6 компонентов, каждый со своим SLA.

Архитектура serving-сервиса

Архитектура serving-сервиса рекомендаций
Рекомендательный сервис координирует Feature Store, Model Serving и ANN-индекс

Типичная архитектура: API Gateway → рекомендательный сервис → Feature Store (Redis) + Model Serving (Triton) + ANN-индекс (FAISS). Всё за nginx/envoy с балансировкой и circuit breaker.

  • Model serving: TorchServe, Triton (NVIDIA), vLLM. GPU-батчинг — накапливаем запросы и прогоняем батчом
  • Feature serving: Redis/Memcached для онлайн-фич, <1мс на запрос. Пользовательские статистики, CTR по категориям
  • ANN-индекс: FAISS, Milvus, Qdrant. Хранит item-эмбеддинги, обновляется каждый час. Поиск по 100М векторов за 5мс
  • Кэширование: рекомендации для активных пользователей — в кэш на 5-15 минут. Экономит 60-80% compute
# Минимальный serving рекомендаций на FastAPI
from fastapi import FastAPI
import faiss, redis, numpy as np

app = FastAPI()
r = redis.Redis(host="redis", port=6379)
index = faiss.read_index("item_embeddings.faiss")  # ANN-индекс

@app.get("/recommend/{user_id}")
async def recommend(user_id: str, k: int = 20):
    # 1. Проверяем кэш (5мс вместо 100мс)
    cached = r.get(f"recs:{user_id}")
    if cached:
        return {"items": json.loads(cached), "source": "cache"}

    # 2. Получаем user embedding из Feature Store
    user_emb = np.frombuffer(r.get(f"emb:{user_id}"), dtype="float32").reshape(1, -1)

    # 3. ANN-поиск кандидатов (~5мс на 100М айтемов)
    scores, item_ids = index.search(user_emb, k=200)

    # 4. Ранжирование (тяжёлая модель на top-200)
    ranked = ranker.predict(user_id, item_ids[0][:200])
    top_k = ranked[:k]

    # 5. Кэшируем на 10 минут
    r.setex(f"recs:{user_id}", 600, json.dumps(top_k))
    return {"items": top_k, "source": "model"}

Задержка — бюджет по миллисекундам

Пользователь открывает ленту — и через 200мс должен увидеть рекомендации. Если дольше — уходит. Бюджет распределяется по этапам:

  • Feature fetch: 5мс — Redis за user/item фичами
  • Candidate generation: 20мс — ANN-поиск по FAISS/Milvus
  • Ranking: 50мс — нейросеть/бустинг на top-200 кандидатов, GPU batch
  • Re-ranking: 5мс — diversity, бизнес-правила, exploration slots
  • Сеть + сериализация: 20мс — от сервиса до клиента
  • Итого: ~100мс p50, <200мс p99. Каждый компонент оптимизируется до последней миллисекунды

Масштабирование

  • Горизонтальное: добавляем реплики stateless-сервиса, балансируем через nginx/envoy
  • GPU батчинг: Triton с динамическим батчингом — накапливает запросы 1-5мс, прогоняет батчом. 10x throughput
  • Шардирование: ANN-индекс по 100М+ айтемов — шардим по хэшу item_id. Каждый шард на своём сервере
  • Предрасчёт: для неактивных пользователей (не заходили 7+ дней) рекомендации считаем офлайн и кладём в Redis

Обновление моделей и индексов

Модель устаревает — новые товары, изменившиеся вкусы. Новые айтемы не попадают в ANN-индекс. Нужен цикл: переобучение (раз в день/неделю) → офлайн-валидация (NDCG не упал) → канареечный деплой (5% трафика) → полный выкат.

Blue-green deployment — две полные копии сервиса: Blue (текущая) и Green (новая). Переключение мгновенное через балансер. Если Green сломалась — откат за секунды. Этот подход широко используется для рекомендательных сервисов.

# Graceful degradation — fallback chain для рекомендаций
import asyncio
import logging

logger = logging.getLogger("recsys")

class FallbackChain:
    """Цепочка fallback-ов: если основная модель упала — не показываем пустоту."""
    
    def __init__(self):
        self.strategies = []
    
    def add(self, name: str, fn, timeout_ms: float = 100):
        self.strategies.append({"name": name, "fn": fn, "timeout": timeout_ms})
        return self
    
    async def execute(self, user_id: str, k: int = 10):
        for strategy in self.strategies:
            try:
                result = await asyncio.wait_for(
                    strategy["fn"](user_id, k),
                    timeout=strategy["timeout"] / 1000
                )
                if result:
                    metrics.inc("recsys.strategy", tags={"name": strategy["name"]})
                    return result
            except (asyncio.TimeoutError, Exception) as e:
                logger.warning(f"{strategy['name']} failed: {e}")
                continue
        return await get_global_popular(k)  # последний рубеж

# Использование: каждый уровень — всё проще и надёжнее
chain = FallbackChain()
chain.add("full_pipeline", full_ranking, timeout_ms=200)     # полный пайплайн
chain.add("light_ranking", light_model_only, timeout_ms=100)  # без тяжёлой модели
chain.add("cached", get_cached_recs, timeout_ms=10)           # кэш
chain.add("popular", get_popular, timeout_ms=5)                # популярное

Graceful degradation — когда что-то сломалось

Пользователь не должен видеть пустую страницу. Каждый компонент должен иметь fallback:

  • Модель ранжирования упала → отдаём результаты candidate generation без ранжирования
  • Redis недоступен → используем in-memory кэш (LRU, последние 10K пользователей)
  • ANN-индекс не обновился → работаем со старой версией (лучше вчерашние рекомендации, чем никакие)
  • Всё сломалось → popularity fallback: топ-100 айтемов за последние 24 часа

System design на собесе

Спрашивают: как задеплоить рекомендательную систему на 10М DAU? Считай конкретно: 10M DAU × 5 сессий × 3 запроса = 150M запросов/день = ~1700 RPS. Нужно: 3-5 stateless инстансов по 400-500 RPS, Redis для фич (~50ГБ), FAISS-индекс (~10ГБ в RAM), модель ранжирования на GPU (batch=64, ~50мс). Latency budget: p99 < 200мс.

🎯 Суть для собеса

• Latency budget — как распределить? (retrieval <50ms, ranking <100ms, total <200ms. 99p, не среднее!) • Как кэшировать рекомендации? (user-level cache 5-15 min, item embeddings в памяти, feature store online) • Масштабирование: 10K → 10M RPS? (горизонтальное: stateless сервисы, батчинг, GPU inference, ANN индексы) • Graceful degradation — зачем? (если модель упала → popularity fallback, а не 500 ошибка) • В production: SLA 99.9%, latency p99 < 200ms, fallback на popularity при сбоях