Классическая задача, на которой видно, умеет ли инженер считать бюджет, а не только перебирать модели: качество ранжирования здесь ограничено не архитектурой сети, а миллисекундами и памятью.
Маркетплейс: главная страница показывает блок «вам может понравиться». Сейчас там правило «популярное в категории», продукт хочет персонализацию.
Блок рендерится вместе со страницей, поэтому у рекомендаций свой бюджет — 50 мс на p99, иначе фронтенд рисует заглушку и персонализация не доезжает до пользователя.
Требования и ограничения
Что
Значение
Откуда
Задержка
p99 ≤ 50 мс
Считается от входа в сервис до ответа, вместе с походом за признаками.
Нагрузка
12 000 RPS в пике
Вечерний пик втрое выше дневного плато.
Каталог
30 млн товаров
В наличии одновременно около 8 млн: остальные скрыты фильтром доступности.
Аудитория
5 млн DAU
Треть — незалогиненные, истории нет.
Свежесть
Новый товар виден за 15 мин
Требование каталога: акции живут часами.
Доступность
99.9 %
Отказ рекомендаций не должен ронять страницу.
Прежде чем читать разбор, попробуйте спроектировать систему сами — по этим требованиям. Дальше будет видно, что вы учли, а что нет.
Разбор по шагам
1Разложить 50 мс по статьям
Первое действие — не выбор модели, а бюджет задержки: он определяет всё остальное.
Пока бюджет не разложен, любое обсуждение архитектуры беспредметно. Раскладка сразу показывает, что на само ранжирование остаётся меньше половины времени.
Статья
Бюджет p99
Что в ней происходит
Сеть и сериализация
5 мс
Запрос от фронтенда, ответ обратно, JSON.
пользователя
8 мс
Один батч-запрос в онлайн-хранилище.
Отбор кандидатов
10 мс
ANN-поиск плюс фильтры доступности.
Ранжирование
15 мс
Скоринг 300–500 кандидатов.
Бизнес-правила и сборка
7 мс
Дедупликация, разнообразие, пины.
Запас
5 мс
GC, всплески, повторные попытки.
На практике
Бюджет назначается на p99, а не на среднее. Средняя в 12 мс отлично уживается с хвостом в 200 мс — а видит пользователь именно хвост.
Отсюда сразу следует главное архитектурное ограничение: скорить 30 млн товаров нельзя. При 15 мс на ранжирование и разумном железе бюджет — сотни кандидатов, а не миллионы.
2Двухэтапная схема: отбор и ранжирование
Дешёвая модель сокращает 30 млн до сотен, дорогая расставляет их по местам.
Отбор кандидатов (retrieval): несколько источников по 100–200 позиций — ANN-поиск по , «похожие на просмотренные», популярное в категории, свежие поступления.
Слияние: объединение с дедупликацией даёт 300–500 кандидатов.
Ранжирование (ranking): одна модель на всех кандидатов, пользователя, товара и пары.
Пост-обработка: разнообразие по категориям, бизнес-пины, фильтр «уже куплено».
Стоимость этапов различается на порядки: ANN-поиск по 8 млн размерности 128 в HNSW укладывается в 1–3 мс при @100 около 0.95, а полный проход модели по каталогу — секунды. Именно поэтому каскад, а не одна модель.
Tранж≈Nкандидатов×tнакандидата
Что означает каждый компонент
Tранжбюджет этапа ранжирования — 15 мс из общих 50
Nкандидатовсколько объектов пришло с этапа отбора: 300–500
tнакандидатастоимость скоринга одного объекта: около 20 мкс у бустинга на одном ядре
500 кандидатов × 20 мкс ≈ 10 мс — в бюджет укладывается, 30 млн объектов — нет
На практике
Фильтр доступности вешается на этап отбора, а не на этап показа. Иначе ранжирование честно потратит бюджет на товары, которых нет на складе, и блок приедет полупустым.
3Признаки: где они лежат и когда считаются
Главная причина промахов по бюджету — не модель, а поход за признаками.
Группа
Где считается
Где лежит
Профиль пользователя (, агрегаты за 30 дней)
Офлайн, раз в сутки
Онлайн-хранилище
1–3 мс
Сессия (последние 20 действий)
Онлайн, при событии
То же хранилище, TTL 30 мин
1–3 мс
Товар (, статистика продаж)
Офлайн, каждые 15 мин
Память процесса, обновление фоном
0 мс
Пара «пользователь — товар»
На лету
Не хранится
≈1 мс
товара держат прямо в памяти сервиса: 8 млн товаров × 128 float16 — это около 2 ГБ, что реально для одной реплики и убирает целый сетевой поход из бюджета.
Все ключи забираются одним батч-запросом. Пятьдесят последовательных обращений по 1 мс — это 50 мс и мгновенный выход за бюджет; те же пятьдесят ключей в одном MGET — 2–3 мс.
На практике
Тот же код, что считает в обучении, должен считать их в проде — иначе появляется training/serving skew: офлайн-метрика растёт, онлайн-метрика стоит на месте, и найти причину почти невозможно.
4Деградация: что показать, когда не успели
Система с жёстким SLA обязана иметь ответ на случай «не уложились».
Таймаут на каждый этап, а не на запрос целиком: не пришли за 8 мс — ранжируем на том, что есть, и помечаем ответ как деградированный.
Фолбэк-цепочка: персональные → популярное в категории → популярное вообще. Последний уровень считается заранее и лежит в памяти, поэтому не может отказать.
Circuit breaker на онлайн-хранилище: если оно легло, сервис не должен ждать таймаута на каждом запросе.
Кеш готовых блоков на 5–10 минут для незалогиненных: треть аудитории без истории получает один и тот же набор, и держать под неё полный незачем.
На практике
Доля деградированных ответов — обязательная метрика на дашборде. Без неё система «работает нормально» ровно до разбора, почему упала конверсия: кеш и фолбэки маскируют отказ, отдавая пользователю правдоподобный мусор.
5Выкатка и проверка
Офлайн-метрика ранжирования не доказывает ничего, пока модель не проверена на живом трафике.
Shadow-режим: новая модель считает ответ на реальном трафике, ответ не показывается. Проверяются и стабильность, а не качество.
A/B на 5 % трафика с фиксацией guardrail-метрик: p99, доля фолбэков, ошибки, выручка на сессию.
Раскатка до 50/50 при неухудшении guardrail; автооткат срабатывает без человека.
Стабильные группы обязательны: пользователь распределяется по хешу идентификатора и не должен прыгать между вариантами между запросами, иначе эксперимент меряет шум.
Развилки и выбор
На собеседовании ценится не сам выбор, а объяснение, почему отклонены остальные варианты.
Чем ранжировать: градиентным бустингом на CPU или нейросетью на GPU?
✓Бустинг на CPU для первой версии
500 кандидатов × 200 признаков укладываются в 10 мс на CPU, горизонтальное масштабирование тривиально, а стоимость реплики в разы ниже. Нейросеть окупается позже — когда упрёмся в потолок качества, а не в старте.
Что отклонено и почему
DLRM на GPU сразу — Даёт прирост качества, но добавляет GPU в критический путь, батчинг ради утилизации карты и +10–20 мс задержки на очередь.
Скоринг всего каталога — 30 млн объектов на запрос — это секунды на любом железе. Каскад существует именно поэтому.
Где хранить эмбеддинги товаров?
✓В памяти каждой реплики, обновление фоном каждые 15 минут
Убирает сетевой поход из бюджета, а 2 ГБ на реплику — приемлемая цена. Требование свежести в 15 минут при этом выполняется.
Что отклонено и почему
Внешнее векторное хранилище — Ещё один сетевой хоп и ещё одна зависимость, которая может лечь; оправдано, когда индекс не влезает в память.
Пересчёт эмбеддингов на лету — Прямой выход за бюджет: инференс энкодера на каждый запрос дороже всего остального пайплайна.
Кешировать ли персональные рекомендации?
✓Кешировать только для незалогиненных и только на 5–10 минут
Незалогиненные дают треть трафика при почти одинаковом ответе — это самый дешёвый способ снять нагрузку. Персональный кеш быстро протухает: после каждого действия пользователя ответ должен меняться.
Что отклонено и почему
Кешировать всё на час — Рекомендации перестают реагировать на действия пользователя — ровно то, ради чего строилась персонализация.
Не кешировать ничего — Треть трафика обслуживается полным пайплайном без единого шанса на другой ответ.
Что ломается в проде
Хвост задержки живёт своей жизнью
Среднее в 12 мс и p99 в 180 мс — обычная картина: сборка мусора, промахи кеша, ретраи. Мерить и алертить нужно по p99, а бюджет закладывать с запасом на всплески.
Расхождение признаков между обучением и продом
Офлайн агрегат «покупок за 30 дней» считается по полной таблице, а онлайн — по данным, доехавшим до хранилища. Разница в пару часов даёт систематический сдвиг, который не видно ни в одной офлайн-метрике.
Утечка через будущее
При сборке обучающей выборки легко взять признак, которого в момент показа ещё не существовало (например, итоговый рейтинг товара). Метрика на валидации взлетает, в A/B прироста нет.
Кеш маскирует деградацию
Когда часть трафика обслуживается кешем и фолбэками, отказ модели виден не в ошибках, а только в продуктовых метриках — через сутки. Долю фолбэков надо считать явно.
Обратная связь замыкается на себя
Модель учится на кликах, которые сама же и собрала: показанное получает клики, непоказанное не имеет шанса. Без доли случайного трафика для сбора непредвзятых данных выборка вырождается.
По чему видно, что система здорова
Метрика
Значение
Зачем
Задержка
p50, p95, p99 по этапам
Отдельно по каждому этапу — иначе непонятно, кто съел бюджет.
Доля деградаций
< 1 %
Ответы, отданные фолбэком или без части признаков.
Покрытие каталога
доля товаров, показанных хотя бы раз
Падение — признак схлопывания рекомендаций в популярное.
Продуктовая метрика
выручка и конверсия на сессию
Решение о раскатке принимается по ней, а не по NDCG.
Офлайн-качество
NDCG@10, Recall@100 по этапам
Recall отбора ограничивает всё, что может сделать ранжирование.
Что спрашивают по этому кейсу
Разложите бюджет в 50 мс по статьям и скажите, какая из них рискованнее всего.
Почему нельзя обойтись одной моделью вместо каскада? На каком размере каталога граница?
Что показывать пользователю, если хранилище признаков не ответило за 8 мс?
Как поймать расхождение признаков между обучением и продом до выката?
Модель выиграла офлайн, но проиграла в A/B. Ваши гипотезы по порядку проверки.