Netflix 模型服务路由笔记:把路由器从请求路径上拆掉
- 约 13 分钟读完 - 全文 5019 字Netflix 模型服务路由笔记:把路由器从请求路径上拆掉
原文:State of Routing in Model Serving(Nipun Kumar, Rajat Shah, Peter Chng,Netflix Technology Blog,2026-05-01) 目标读者:做 ML 平台、推理服务、内部网关的工程师;以及所有面对"要不要在请求路径上加一层集中代理"这个问题的人。 阅读目标:搞清楚 Netflix 为什么自建路由层、这层路由怎么和工作流级模型抽象配合、它规模化后踩了哪三个坑,以及 Lightbulb 是怎么在不丢能力的前提下把这层路由从请求路径上拆掉的。
一、先澄清前提:Netflix 的「模型」不是模型
这篇文章是一个系列的第一篇,讲 Netflix 中央 ML 模型服务平台的路由设计。截至 2025 年,这个平台承载数百种模型类型和版本,合计每秒一百万次请求。但要理解它的路由,得先理解它路由的对象。
业界说的 model inference 通常就是 infer(features) 到 score 这一条函数调用。Netflix 的「模型」是另一个东西:一个自包含的工作流,封装了前处理、后处理、特征计算逻辑,外加一个可选的、真正训练出来的 ML 组件,全部打成标准格式。这个工作流的端到端执行才叫 model serving。
这个定义直接决定了 API 的形状。调用方只需要给两类东西:
- 标准请求上下文:userId、国家、设备;
- 领域上下文:要排序的候选片单,或者一笔待检测的支付交易。
特征不用客户端算——模型定义里声明了它需要哪些 facts(原始未加工的观测数据),serving 平台负责在运行时调其他微服务把 facts 取来,模型自己完成特征计算和推理。离线训练时则由 fact store 提供快照做批量特征计算。
这个设计的收益是双向的:客户端一次接入之后,模型架构和数据输入再怎么演化都不需要客户端配合;模型研究员也只需要和平台这一方打交道。标题推荐和支付欺诈检测用的是同一套接入方式——这是后面一切路由抽象的地基。
二、平台的三条设计原则
原文列的原则里和路由直接相关的有三条,每一条都对应一个具体的工程约束:
- 模型创新独立于客户端应用。新用例只做一次性接入,之后几乎所有模型迭代——包括中间的 A/B 实验——对调用方透明。模型选择、实验分流、额外数据拉取、训练日志,全是平台的事。
- 客户端与模型分片解耦。模型分布在多个 serving 集群分片上,每个分片有自己的 VIP 地址。流量模式、SLA、模型架构、CPU 和内存余量都会影响模型到分片的映射,映射一变 VIP 就变。客户端不应该感知这种变化。
- 灵活的流量路由规则。支持按 A/B 实验路由、缓慢向新模型和新 VIP 切流、客户端覆写。
注意第二条:在 Netflix 这个设计里,「模型在哪」是个高频变动的运维事实,而不是部署时定死的静态配置。这决定了路由必须是个动态系统。
三、Switchboard:自建的路由层
现成的 API 网关(AWS API Gateway、独立的 Service Mesh 代理)为什么不行?原文给了四个理由:需要和 Netflix 实验平台一等公民集成、需要对客户端暴露 gRPC、需要用富领域上下文做路由定制、需要定制模型生命周期阶段(影子模式、金丝雀、回滚)。通用代理不为此设计,于是自研了 Switchboard——所有流量的弹性代理层,扛一百万 QPS。
Objective:用例的名字
整个抽象的核心是一个叫 Objective 的概念:serving 平台定义的一个枚举,每个进系统的请求都必须携带。它就是业务用例的名字,比如 ContinueWatchingRanking。它干三件事:让客户端只声明「我要什么」而不说「用哪个模型」;作为路由和模型选择的决策依据;把客户端和具体模型彻底解耦。
Switchboard 的四项能力
- 统一客户端抽象:所有客户端的模型请求只有一个联络点,没有新依赖、没有新 client 要管;平台侧则拿到跨模型版本的限流和并发控制旋钮,能对付行为恶劣的客户端。
- 上下文感知路由:按设备、地区、排序场景(首页还是搜索结果)、用户所在的 A/B 实验来路由。
- 动态流量切分:金丝雀和实验的实时切流,新模型先放给受控的小比例用户。
- 版本与生命周期管理:同一模型多版本并发承接流量,支撑影子模式测试(生产流量发给新模型但不影响用户,只做性能对比)和瞬时回滚。
Switchboard Rules:研究员的入口
Objective 是客户端和平台之间的契约,那研究员怎么把模型变体、实验、流量配比挂到 Objective 上?答案是一份 JavaScript 配置,叫 Switchboard Rules,编译产出一份 JSON 规则文件,规定三件事:某 Objective 的默认模型、一组 Objective 上要开哪些 A/B 实验及对应模型、向新模型渐进切流的定制。
原文给了一个 Continue Watching 场景的规则示例:定义 A/B 实验 12345,cell 1 走当前线上模型,cell 2 和 cell 3 走两个候选模型,绑定到 ContinueWatchingRanking 这个 Objective。
规则的分发是这次设计里最值得抄的部分:规则有独立于代码部署的发布周期,走 Netflix 的 Gutenberg 系统做 pub-sub,带版本管理、动态加载和回滚。Switchboard 和 serving 集群主机订阅同一份规则配置。控制面拿到规则后做三件事:产出模型到分片的分配方案、把规则里的模型都加载进分片验证依赖能跑通、把模型到分片 VIP 的映射交给 Switchboard。数据面则是四步:
flowchart TD
C["客户端微服务<br/>携带 Objective 和标准上下文"] -->|"gRPC 请求"| SB["Switchboard<br/>集中路由层"]
SB -->|"第一步 分配<br/>查实验平台<br/>userId 在哪个 cell"| EXP["实验平台"]
EXP --> SB
SB -->|"第二步 选模型<br/>按 cell 和 A B 规则<br/>确定模型版本"| SB
SB -->|"第三步 路由<br/>查模型到 VIP 映射"| SHARD["Serving 集群分片<br/>对应 VIP"]
SHARD -->|"第四步 执行<br/>跑完整模型工作流"| RESP["返回推理结果"]
RESP --> SB
SB --> RESP2["响应给客户端"]
style SB fill:#fff3cd
到这一步故事很顺:30 多个客户端服务靠这层抽象获得了模型迭代的完全自由。然后规模化开始收账。
四、规模化的三笔账
Switchboard 跑大之后,三个问题浮出来,每一个都很典型:
- 单点故障。Switchboard 在所有请求的关键路径上。无意的 bug、 noisy neighbor 打过来的超额流量,极端情况下可能切断对所有 serving 主机的访问。它是 30 多个 ML 体验的共享依赖,它倒了大家一起倒。
- 额外延迟。请求路径上多一跳,序列化加反序列化带来 10-20ms 的延迟(取决于 payload 大小),还放大了尾延迟。对延迟敏感的客户端,这直接表现为服务超时、终端用户受影响。
- 客户端灵活性受损。Switchboard 挡在中间,serving 集群看不到请求的真实来源。区分真实流量和人为测试流量——这对模型训练数据至关重要——变得困难,需要持续的定制开发,MLOps 开销一直涨。
第三条值得停一下。集中代理的一个隐性代价是信息损耗:代理层为了抽象抹掉了来源信息,而下游恰恰需要这个信息做租户隔离和训练数据清洗。抽象不是免费的,它吃掉的信息总得有地方补回来。
五、Lightbulb:拆路由,而不是删路由
团队的结论不是推翻 Switchboard,而是重新摆放它职责的执行位置。要保留的能力列得很清楚:统一客户端抽象、分片解耦、灵活路由规则、轻量客户端、单一配置入口、配置快速传播、故障时的 fallback 和客户端缓存。要解决的是三件事:
- 把路由服务从直接请求路径上移走。路由规则变化频率很低,为它维持强一致性的可用性代价不划算;
- 把模型输入和请求元数据分开。payload 可能很大,Switchboard 为了做路由决策要把整个 payload 反序列化再序列化,这是延迟和成本的大头;
- 给路由层更好的隔离。多租户挤在一个路由集群里,一个租户的异常流量会级联影响其他租户,而不同用例的延迟要求差异又很大。
做法是把原来一整份规则拆成两套配置:模型服务配置(请求时该用哪个模型及所需元数据)和路由规则(定了模型之后,请求该去哪个 VIP)。数据面也跟着拆开——路由本身交给 Envoy 做:
flowchart LR
C["客户端服务"] -->|"最小请求上下文"| LB["Lightbulb<br/>只做配置解析"]
LB -->|"返回 routingKey<br/>和 ObjectiveConfig"| C
C -->|"完整请求<br/>头部带 routingKey<br/>body 带 ObjectiveConfig"| ENVOY["Envoy 代理<br/>客户端侧出口"]
ENVOY -->|"按 routingKey 映射到 VIP<br/>头部判断 开销极小"| SHARD["Serving 集群分片"]
SHARD --> ENVOY
ENVOY --> C
style LB fill:#e8f5e9
style ENVOY fill:#fff3cd
几个关键设计决策:
- Envoy 在 Netflix 本来就把所有应用间出口流量都接管了,它能按控制面发布的路由规则把请求发到不同集群(VIP),但它没有路由决策所需的信息,也不会给请求体补充 A/B 变体所需的 serving 参数。Lightbulb 就是补这个缺口的。
- Lightbulb 只吃最小的请求上下文(用例信息),解析出两样东西:routingKey 和 ObjectiveConfig。前者是给 Envoy 的路由元数据,后者放模型 id 和模型执行所需的其他请求级配置。把配置解析和「怎么到达推理集群」彻底分开了。
- routingKey 放请求头给 Envoy 消费;ObjectiveConfig 由客户端放进请求体。刻意不把所有东西都塞进头部,避免头部膨胀。
- 真正的路由由 Envoy 执行:routingKey 在头部,映射到 VIP 的判定开销极小。
Switchboard 时代的核心收益——单一接入点、模型 id 对用例的抽象、上下文感知路由——全部保留;单点和额外跳数没了。
六、工程实践联想
这篇架构演进史里有几条可以直接搬进日常:
- 先问「这层代理吃掉了什么信息」。加一层集中代理时,大家习惯算延迟账,很少有人算信息账。Switchboard 的第三个坑就是信息账:来源身份被抽象抹掉,训练数据分不清真假流量。设计代理层时,把「下游需要但代理会吞掉的字段」列出来,和延迟预算一起评审。
- 配置和代码分开发布,是实验平台的生命线。Switchboard Rules 走 Gutenberg pub-sub、有独立发布周期、能动态加载能回滚——模型实验的迭代速度取决于配置的流转速度,不取决于服务的部署速度。凡是「业务方天天要改、改了就要生效」的东西,都不应该长在代码发布列车里。
- 控制面集中、数据面分散,是规模化的标准答案。Lightbulb 的本质是把「做决策」(低频、需要一致性、可以集中)和「执行决策」(高频、要求低延迟、必须分散)拆开。Envoy 执行、Lightbulb 决策,各归各位。同一个套路在服务网格、CDN、feature flag 系统里反复出现。
- 低频变化的路径上不要维持强一致。路由规则变化频率很低,为它维护一个高可用的在线路由服务是过度支付——推到客户端侧缓存,故障时还能 fallback。「这个信息多久变一次」应该直接决定它放在请求路径上还是路径外。
- 给模型起「用例名」而不是暴露「模型名」。Objective 这个枚举是整个抽象的关键:客户端说的是意图,平台负责兑现。这个套路在任何平台工程里都成立——暴露稳定的能力枚举,藏住易变的实现标识,实现侧的迭代自由度就是这么来的。
本篇小结
- 前提:Netflix 的「模型」是自包含工作流(前处理、特征计算、可选的 ML 组件、后处理),serving 而非 inference 是抽象单位;客户端只给标准上下文加领域上下文,facts 由平台在运行时取。
- 第一版架构:Switchboard 集中路由层,配 Objective 枚举(用例名)和 Switchboard Rules(JS 配置编译成 JSON,Gutenberg pub-sub 分发,独立于代码部署);提供统一接入、上下文路由、动态切流、影子模式和瞬时回滚。
- 规模化的三笔账:单点故障、10-20ms 额外延迟加尾延迟放大、请求来源信息被代理吞掉导致租户隔离和训练数据清洗困难。
- 第二版架构:Lightbulb 把模型选择从路由里拆出来——Lightbulb 解析最小上下文产出 routingKey(进头部)加 ObjectiveConfig(进请求体),Envoy 在客户端侧按 routingKey 路由到 VIP;请求路径上不再有集中路由器,能力一项没丢。
- 元教训:集中式代理解决抽象问题,规模化之后要把决策和执行拆开——决策可以集中(低频、要一致性),执行必须分散(高频、要低延迟)。
参考资料
- State of Routing in Model Serving,Netflix Technology Blog,2026-05-01(本系列第一篇,后续会讲推理与特征拉取)。
- 原文引用的内部系统:Netflix 实验平台(A/B 分配)、Gutenberg(配置 pub-sub 与版本管理)、ML fact store(离线训练特征快照)。
- Envoy Proxy(Lightbulb 架构中承担实际路由的客户端侧代理)。