外观
推荐系统在线推理:延迟预算下的多模型串联
一句话定义:推荐系统的在线推理是"召回→粗排→精排→重排"四个阶段、多个模型在百毫秒延迟预算内级联执行的部署工程——它同时挑战了特征工程、模型服务、级联调度与容量规划四件事。
为什么值得动手:推荐在线链路是全站延迟预算最紧、模型最多、最考验部署架构的场景——一个 100ms 的端到端预算里要塞下特征读取、向量检索、粗排几千条、精排几百条、重排混排;任何一个环节慢 20ms 都会挤爆预算。本文拆解这条链路每个阶段怎么部署、特征怎么在线取、模型怎么串,并给出可落地的延迟预算分配表。推荐的部署决策离不开模型网关与灰度发布的流量编排与性能优化与容量规划的预算分配,本文是它们在一线场景的合体。
一、推荐链路全景与延迟预算
text
用户请求
│
▼
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 召回 │──▶│ 粗排 │──▶│ 精排 │──▶│ 重排 │──▶ 结果
│ 千万→千 │ │ 千→百 │ │ 百→几十 │ │ 去重/多样性 │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
向量检索 双塔/小模型 深度排序模型 规则+小模型| 阶段 | 候选规模 | 典型模型 | 延迟预算 |
|---|---|---|---|
| 召回 | 千万 → 1~2 千 | 向量检索(Faiss/ANN)+ 双塔模型 | 15~20ms |
| 粗排 | 千 → 100~300 | 双塔/小 DNN(特征精简版) | 10~15ms |
| 精排 | 百 → 几十 | 深度 CTR 模型(大特征集) | 30~50ms |
| 重排 | 几十 → 展示 | 规则 + 小模型(多样性、新鲜度) | 5~10ms |
| 特征/上下文读取 | — | Feature Store / KV | 15~25ms |
| 合计 | ≤ 100ms |
先给结论:预算分配的比例原则是"越靠前的阶段越便宜"——召回阶段候选多但模型轻,精排阶段候选少但模型重,把贵的计算留给最值得精排的几百条。具体数字按业务调,但总预算必须留 20% 的余量给网络抖动和不可预知开销。
二、特征工程在线化
精排模型吃的是特征,特征的一致性是推荐在线的第一事故源。三件事:
- 特征分实时/离线两路:离线特征(用户长期画像、物品属性)由批处理推理管道每天灌入 Feature Store;实时特征(最近点击序列、上下文)在线实时计算并写 Redis 类存储,TTL 控制;
- 特征一致性:训练时用"该请求时间点之前"的特征,在线也必须如此,不能偷看未来。最常见的事故是"训练用 T 时刻特征、在线用 T+1 时刻特征",模型直接崩。一致性治理方法见常见陷阱与反模式的特征泄漏条目;
- 特征存储选型:读多写少、毫秒级延迟 → Redis/Faiss 这类 KV;要回放与血缘 → Feature Store(Feast/阿里云 FeatureStore 等)。在线只读、离线批量写,避免在线侧写库引入链路耦合。
三、精排模型在线服务
精排是延迟大头,部署上三个关键动作:
1. embedding 检索与模型输入组装:特征分两半——直接数值特征进模型,稀疏 ID 特征先查 embedding(KV/向量库)再拼成张量。组装要在模型服务内做还是外层做,先给结论:组装放服务内、输入 schema 传业务语义(user_id、item_ids),模型服务把"查表→组装→前向→后处理"封装成一个 predict 类——与 FastAPI 实战的封装原则一致,但对延迟敏感,通常不用 HTTP 而是 gRPC(见服务化与推理 API)。
2. batch 推理:精排一次收到上百条候选,务必一次前向出全部分数,而不是逐条推理。深度 CTR 模型把 item_ids 拼成 batch 维度 [N, feature_dim],一次 forward 拿到 N 个分数。这在性能优化与容量规划里就是"批处理"打法;模型量大、要更高吞吐可上 Triton 的动态批处理。
python
# 精排服务核心逻辑(示意)
import torch
@torch.inference_mode()
def rank(user_vector, item_ids, item_emb_cache):
# item_emb_cache: 从 KV 查出的 [N, d] embedding 矩阵
item_emb = torch.from_numpy(item_emb_cache) # [N, d]
user = user_vector.unsqueeze(0).expand(len(item_ids), -1) # [N, d]
x = torch.cat([user, item_emb, cross_features], dim=1) # 组装输入
scores = ctr_model(x).squeeze(-1) # 一次前向出 N 个分数
return scores3. 模型体积与显存:精排模型大、显存贵,常见的降本手段是量化(FP16/INT8,见量化)和 embedding 表按热程度分层(热 ID 全量驻留、冷 ID 查外存)。
精排接口契约:一次组装、一次前向
精排服务对外的 schema 用业务语义而不是原始张量,模型服务内部负责"查表→组装→前向":
json
// 请求:一条用户 + N 个候选
{
"request_id": "req_8f21",
"user": { "user_id": "u_10086", "scene": "home_feed" },
"items": ["i_101", "i_102", "i_103"],
"context": { "hour": 21, "device": "android" }
}
// 响应:与请求 items 一一对应的分数
{
"request_id": "req_8f21",
"scores": [0.91, 0.82, 0.76]
}契约里带上 request_id,是为了整条链路对账——召回/粗排/精排各阶段日志都带同一个 id,排查"某条候选为什么没进精排"时直接串起来。这是监控与可观测性里链路追踪在推荐场景的标准用法。
特征组装的延迟账本
精排 40ms 预算里,特征取数是容易被低估的隐形开销。一组参考分解(KV 全部命中、单机 Redis):
| 动作 | 耗时 | 说明 |
|---|---|---|
| 用户画像批量取(1 次批量 GET) | 约 0.5~1ms | 特征存储批量读 |
| N 条候选 item 特征批量取 | 约 1~2ms | N=300,批量而非逐条 |
| embedding 查表 | 约 2~5ms | 向量库/内存 KV |
| 输入组装(concat/numpy) | 约 1ms | 别用 Python 逐字段拼 |
| 模型前向(batch=300) | 约 10~25ms | 精排模型主体 |
先给结论:特征取数通常只占精排 10~20% 预算,但"逐条取"会把它放大 10 倍——任何阶段都做批量读取,不做逐条 N 次往返。这也是推荐链路优化的第一优先级:先修"没批量化"的笨取数,再谈调模型。
四、多模型串联:级联、超时与降级
四个阶段四个服务,串联的核心是超时与降级:
| 风险 | 对策 |
|---|---|
| 上游慢,拖垮下游 | 每阶段独立超时(如召回 20ms),超时用上一批缓存结果 |
| 召回挂了 | 降级:只推热门池/首页固定池,保可用性 |
| 精排慢 | 降级:跳过精排,粗排结果直接进重排 |
| 单点打满 | 每阶段独立扩缩 + 限流,见模型网关与灰度发布 |
实现方式三种,先给结论:业务逻辑复杂(多分支、个性化降级)→ 应用层编排(网关/服务内调用);链路固定简单 → Triton ensemble 或 KServe 推理图。超时设置参考:端到端 100ms,每阶段预留独立超时(召回 20ms、粗排 15ms、精排 40ms、重排 10ms),用 Hystrix/Sentinel 类熔断器保护下游——某阶段连续失败快速熔断,避免雪崩。
五、缓存与热点处理
推荐在线延迟优化的两个常用手段:
- 结果缓存:同一用户+同一场景短期内重复请求,直接命中缓存。Key =
user_id + scene + model_version。注意缓存必须带模型版本号,否则模型更新后仍在返回旧结果——这是模型网关与灰度发布里"缓存污染"问题的同款坑; - 热点请求:头部用户贡献大部分流量。做法:头部用户的推荐结果预热进缓存(上一轮全量算好)、热点 Item 的 embedding 提前驻留内存。热点治理见部署架构模式的"热点与缓存"讨论。
六、架构图与工具选型
text
客户端 ─▶ 推荐网关(路由/限流/灰度/A-B)
│
▼
推荐编排服务(级联 + 超时 + 降级)
│ │ │
▼ ▼ ▼
召回服务 粗排服务 精排服务 ──▶ 重排服务 ──▶ 输出
(Faiss + (小DNN) (深度CTR) (规则/小模型)
双塔)
│ │ │
▼ ▼ ▼
Feature Store(KV/Redis + 离线画像管道) + 向量库(Milvus/Faiss)| 组件 | 选型参考 | 一句话理由 |
|---|---|---|
| 特征存储 | Redis / Feast | 毫秒读、批量写、TTL |
| 向量检索 | Faiss / Milvus | 召回主战场,ANN 检索 |
| 模型服务 | FastAPI(小模型)/ Triton(大模型、多模型) | 延迟与吞吐按需选 |
| 推荐网关 | Envoy / 自研 | 路由、限流、灰度、A/B |
| 编排 | 自研编排服务 / Triton ensemble | 级联、超时、降级 |
延迟预算与容量规划的落地方法见性能优化与容量规划;灰度上线新推荐模型要走模型网关与灰度发布的完整流程(本文的网关就是它的入口);模型版本与特征的对齐关系靠MLOps 部署流水线管起来。
常见坑与排查
| 坑 | 现象 | 排查 |
|---|---|---|
| 特征泄漏 | 在线效果比离线差一大截 | 训练/在线特征时间对齐,见第二节 |
| 级联超时没设 | 某阶段慢 → 整条链路超时 | 每阶段独立超时 + 熔断 |
| 精排逐条推理 | 精排耗时 200ms | 候选拼 batch 一次前向 |
| 缓存不带版本号 | 模型更新后结果不变 | 缓存 key 加 model_version |
| 召回池全量扫描 | 召回超预算 | 用 ANN(HNSW/IVF),别暴力全量 |
| 头部用户打满服务 | 热点用户请求拖垮整体 | 热点结果预热缓存 |
| 降级策略缺失 | 精排一挂整站推荐白屏 | 预置降级链(跳过精排/热门兜底) |
延伸阅读
- 模型网关与灰度发布 —— 推荐链路的流量入口:路由、限流、A/B 与回滚
- 性能优化与容量规划 —— 延迟预算分配与各阶段容量估算的方法
- 部署架构模式 —— 级联调用、热点、缓存等架构模式的理论
- 常见陷阱与反模式 —— 特征泄漏、缓存污染等推荐场景高频坑
- 服务化与推理 API —— gRPC、批处理、接口契约的设计原则
- FastAPI + Docker 在线服务 —— 小模型/粗排的朴素部署实现
- NVIDIA Triton 多模型服务 —— 精排等重模型的动态批处理托管
- 批处理推理管道 —— 离线画像/特征产线,在线链路的左膀
参考资料
- Facebook 论文:Faiss:https://arxiv.org/abs/1702.08734
- Faiss GitHub:https://github.com/facebookresearch/faiss
- Milvus 官方文档:https://milvus.io/docs
- Redis 官方文档:https://redis.io/docs/latest/
- Feast(Feature Store):https://docs.feast.dev/