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