<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Netflix on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/tags/netflix/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Mon, 19 Oct 2026 10:00:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/netflix/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>C 端 IPTV 产品的 Java 高并发架构设计：高可用、高性能、零信任与缓存一致性</title>
      <link>https://ftmao.com/blog/posts/iptv-java-high-concurrency-architecture/</link>
      <pubDate>Mon, 19 Oct 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/iptv-java-high-concurrency-architecture/</guid>
      <description>&lt;h1 id=&#34;c-端-iptv-产品的-java-高并发架构设计高可用高性能零信任与缓存一致性&#34;&gt;C 端 IPTV 产品的 Java 高并发架构设计：高可用、高性能、零信任与缓存一致性&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;过去几年我一直在做一个面向海外的 C 端 IPTV 产品（STB 机顶盒 + 移动端，多品牌多租户，跑在 AWS 上）。这篇文章是我对这套系统架构的复盘：高并发、高可用、高性能、高安全、缓存一致性这几件事我们分别是怎么做的、踩过什么坑。过程中我们落地了不少 Netflix 的开源组件和公开实践（Hystrix、EVCache、Zuul 的网关思想、混沌工程等），但每一个决策都是经过我们自己生产环境验证或修正的，不是照搬。文中方案细节来自真实的系统设计文档（Video Cloud 目标模型、vod-auth、CloudRouter、DRM、网盘整改等），脱敏后分享。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一先搞清楚对手c-端-iptv-的流量模型&#34;&gt;一、先搞清楚对手：C 端 IPTV 的流量模型&lt;/h2&gt;
&lt;p&gt;设计之前先看清流量的形状。这是我做这套系统第一天就逼自己回答的问题：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;特征&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
					&lt;th&gt;架构含义&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;潮汐效应极强&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;晚 8 点 - 11 点是高峰，大促、球赛、爆款剧上线时流量成倍脉冲&lt;/td&gt;
					&lt;td&gt;弹性伸缩 + 削峰，按高峰容量做预案&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;读极多、写极少&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;EPG 节目单、Meta 元数据、Layout 下发占了 9 成以上流量&lt;/td&gt;
					&lt;td&gt;缓存优先，读路径尽量不打 DB&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;播放鉴权是超级热点&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;每次起播、切台都要鉴权取播放链接，QPS 随在线用户数线性涨&lt;/td&gt;
					&lt;td&gt;热点接口单独优化（缓存 + 短 TTL + 滑动续期）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;设备在线心跳&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;app 轮询 session，百万级设备定时上报&lt;/td&gt;
					&lt;td&gt;心跳服务独立部署，写路径异步化&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;多品牌多租户&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;同一套系统服务多个品牌（巴西、俄语区等）&lt;/td&gt;
					&lt;td&gt;分库隔离爆炸半径，account_id 前缀区分品牌&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这个流量形状——读多写少、晚间高峰、播放路径是生命线——决定了后面所有的架构取舍。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二总体架构一张图看清全貌&#34;&gt;二、总体架构：一张图看清全貌&lt;/h2&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;graph TB
    subgraph 终端
        STB[&amp;#34;STB 机顶盒&amp;#34;]
        APP[&amp;#34;移动 App&amp;#34;]
        WEB[&amp;#34;Web / Portal&amp;#34;]
    end

    subgraph 接入层
        ELB[&amp;#34;AWS ELB&amp;#34;]
        APISIX[&amp;#34;APISIX 网关&amp;lt;br/&amp;gt;限流 Sentinel / WAF / 鉴权前置&amp;#34;]
    end

    subgraph 微服务层
        AUTH[&amp;#34;vod-auth&amp;lt;br/&amp;gt;登录注册 / 账号 / 子账号&amp;#34;]
        SESS[&amp;#34;common-session&amp;lt;br/&amp;gt;会话管理&amp;#34;]
        META[&amp;#34;vod-meta-api&amp;lt;br/&amp;gt;元数据 / Layout / EPG&amp;#34;]
        PROF[&amp;#34;profile&amp;lt;br/&amp;gt;观看历史 / 收藏 / 偏好&amp;#34;]
        CLOUD[&amp;#34;vod-cloud&amp;lt;br/&amp;gt;个人网盘&amp;#34;]
        ROUTER[&amp;#34;CloudRouter&amp;lt;br/&amp;gt;播放链接路由&amp;#34;]
        DRM[&amp;#34;drmlicense&amp;lt;br/&amp;gt;DRM 鉴权与 license 下发&amp;#34;]
    end

    subgraph 数据层
        MYSQL[&amp;#34;MySQL RDS&amp;lt;br/&amp;gt;按品牌分库&amp;#34;]
        REDIS[&amp;#34;Redis Cluster&amp;lt;br/&amp;gt;Session / 验证码 / Meta 缓存&amp;#34;]
        METAREDIS[&amp;#34;Meta 专用 Redis Cluster&amp;#34;]
        ES[&amp;#34;Elasticsearch&amp;lt;br/&amp;gt;检索字段&amp;#34;]
        KAFKA[&amp;#34;Kafka MSK&amp;lt;br/&amp;gt;Meta 同步 / 网盘同步&amp;#34;]
    end

    subgraph 内容侧
        STREAM[&amp;#34;Streaming 流媒体&amp;#34;]
        CDN[&amp;#34;CDN / Squid 边缘缓存&amp;#34;]
        CP[&amp;#34;cloudProxy&amp;lt;br/&amp;gt;第三方网盘适配&amp;#34;]
    end

    STB --&amp;gt; ELB
    APP --&amp;gt; ELB
    WEB --&amp;gt; ELB
    ELB --&amp;gt; APISIX
    APISIX --&amp;gt; AUTH
    APISIX --&amp;gt; SESS
    APISIX --&amp;gt; META
    APISIX --&amp;gt; PROF
    APISIX --&amp;gt; CLOUD
    APISIX --&amp;gt; ROUTER
    ROUTER --&amp;gt; DRM
    ROUTER --&amp;gt; STREAM
    STREAM --&amp;gt; CDN
    STREAM --&amp;gt; CP
    META --&amp;gt; METAREDIS
    META --&amp;gt; ES
    AUTH --&amp;gt; MYSQL
    AUTH --&amp;gt; REDIS
    SESS --&amp;gt; REDIS
    CLOUD --&amp;gt; KAFKA
    META --&amp;gt; KAFKA&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;几个关键决策，后面逐章展开：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;接入层&lt;/strong&gt;：链路是 &lt;code&gt;app -&amp;gt; ELB -&amp;gt; APISIX -&amp;gt; vod 微服务&lt;/code&gt;，限流用 Sentinel，网关做鉴权前置和 WAF。网关选型时我认真研究过 Netflix Zuul 2 从同步阻塞改异步非阻塞的历程——结论是网关本身绝不能成为瓶颈。我们体量没那么大，APISIX（底层同样是 Nginx + Lua 的高性能异步模型）已经够用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务拆分&lt;/strong&gt;：按业务域拆（Auth / Profile / Meta / Cloud / Session），不是按技术层拆。领域拆分的完整过程在&lt;a href=&#34;https://ftmao.com/blog/blog/posts/iptv/&#34;&gt;视频系统 DDD 实战&lt;/a&gt;里写过。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据层&lt;/strong&gt;：MySQL 按品牌分库（&lt;code&gt;{brand}_account_db&lt;/code&gt; 等）；Redis 承载 Session、验证码、热点数据；Meta 用&lt;strong&gt;独立的 Redis Cluster&lt;/strong&gt; 全量缓存，与业务缓存物理隔离；检索字段入 ES。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步化&lt;/strong&gt;：Kafka（MSK）只干两件事——Meta 同步管道和网盘同步，不滥用。这是我给自己立的规矩：每加一个 MQ topic 都要回答「同步调用为什么不行」。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三高并发把流量挡在离用户最近的地方&#34;&gt;三、高并发：把流量挡在离用户最近的地方&lt;/h2&gt;
&lt;p&gt;高并发的核心心法就一句：&lt;strong&gt;每一层都尽量把请求消化掉，别让它穿透到下一层&lt;/strong&gt;。越靠近 DB 越贵。&lt;/p&gt;
&lt;h3 id=&#34;31-网关层第一道削峰闸&#34;&gt;3.1 网关层：第一道削峰闸&lt;/h3&gt;
&lt;p&gt;我们在网关层做了三件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;限流&lt;/strong&gt;：APISIX 自带 limit-req / limit-count 插件做粗粒度限流，Sentinel 做服务级细粒度熔断限流（现网组合就是「ELK + Skywalking + Sentinel（APISIX）」）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;鉴权前置&lt;/strong&gt;：明显非法的请求（无 token、签名错误、UA 黑名单）在网关直接 401/403，不放行到业务服务。被攻击时这一层挡掉的流量经常比业务层处理的还多。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;静态配置下发走 CDN&lt;/strong&gt;：&lt;code&gt;/ui/cfg/v1&lt;/code&gt;、&lt;code&gt;/layout/v1&lt;/code&gt; 这类终端配置接口，内容变化频率低，直接走 CDN 缓存，源站只承担回源请求。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;32-热点接口播放鉴权链接的缓存设计&#34;&gt;3.2 热点接口：播放鉴权链接的缓存设计&lt;/h3&gt;
&lt;p&gt;CloudRouter（播放源路由服务）是我手里最热的路径：每次起播都要按 &lt;code&gt;logicStreamId&lt;/code&gt; 换真实的下载/播放链接。我做的设计非常直接：&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;graph LR
    A[&amp;#34;App 起播请求&amp;lt;br/&amp;gt;logicStreamId&amp;#34;] --&amp;gt; B{&amp;#34;查 Redis&amp;#34;}
    B -- &amp;#34;命中&amp;#34; --&amp;gt; C[&amp;#34;刷新 TTL 滑动续期&amp;lt;br/&amp;gt;直接返回链接&amp;#34;]
    B -- &amp;#34;未命中&amp;#34; --&amp;gt; D[&amp;#34;查 MySQL 两表&amp;lt;br/&amp;gt;logic_stream + file_source&amp;#34;]
    D --&amp;gt; E[&amp;#34;构建下载链接 DownloadBO&amp;lt;br/&amp;gt;link / fileSize / expired&amp;#34;]
    E --&amp;gt; F[&amp;#34;写回 Redis&amp;#34;]
    F --&amp;gt; G[&amp;#34;返回&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;三个细节值得说：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;命中即续期（滑动 TTL）&lt;/strong&gt;：热播内容的链接永不过期，冷内容自然淘汰——不需要人工预热。这个设计替我省掉了整个「热点内容运营」的麻烦。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存对象自带过期时间&lt;/strong&gt;：链接本身有 &lt;code&gt;expired&lt;/code&gt; 字段，客户端在链接到期前会重新请求，和缓存 TTL 是两层独立的过期机制，互不相信。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;失败快速抛出&lt;/strong&gt;：查不到直接抛业务异常，不做 fallback 到第三方存储的慢路径——播放链路每一毫秒都值钱。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;33-多级缓存l1--l2&#34;&gt;3.3 多级缓存：L1 + L2&lt;/h3&gt;
&lt;p&gt;纯 Redis 方案在超热点 key 上依然会打满单个分片（新片上线当晚的 metaId 我真实遇到过）。为了解决这个问题我研究了 Netflix 的 EVCache——它的多副本加客户端本地缓存思路给了我启发，但我们体量用不上 EVCache 全套，于是我基于 Caffeine（L1 进程内缓存）+ Redis（L2）自己实现了一个两级缓存框架并开源，见&lt;a href=&#34;https://ftmao.com/blog/blog/posts/kecache-intro/&#34;&gt;我开发的 keCache：面向 Spring Boot 3 的 L1 + L2 两级缓存框架&lt;/a&gt;，要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;L1 短 TTL（秒级），扛住瞬时洪峰；&lt;/li&gt;
&lt;li&gt;L1 失效消息走 Redis Pub/Sub 广播，保证多实例间 L1 不一致的窗口可控；&lt;/li&gt;
&lt;li&gt;序列化用 Kryo，比 JDK 序列化小 3-5 倍。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;34-写路径异步化mq-削峰&#34;&gt;3.4 写路径异步化：MQ 削峰&lt;/h3&gt;
&lt;p&gt;写少但集中的场景（新片上线批量导入 Meta、网盘大规模刷新）我全部走 Kafka：&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;graph LR
    A[&amp;#34;vod-sync-dispatcher&amp;lt;br/&amp;gt;每 30 秒轮询上游片源&amp;#34;] --&amp;gt; B[&amp;#34;Kafka MSK&amp;#34;]
    B --&amp;gt; C[&amp;#34;vod-imdb-meta-sync&amp;lt;br/&amp;gt;元数据入 Redis + ES&amp;#34;]
    D[&amp;#34;vod-cloud-sync-dispatcher&amp;#34;] --&amp;gt; B
    B --&amp;gt; E[&amp;#34;vod-cloud-sync-worker&amp;lt;br/&amp;gt;刷新网盘&amp;#34;]
    F[&amp;#34;http2Msk&amp;lt;br/&amp;gt;大数据 HTTP 回调转 MQ&amp;#34;] --&amp;gt; B
    B --&amp;gt; G[&amp;#34;偏好计算&amp;lt;br/&amp;gt;you may like&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;生产者-消费者模型 + MQ 缓冲，让上游脉冲（片源方一次推几万条 meta）被摊平成消费者能吃得动的匀速流。当时的 MQ 选型过程见&lt;a href=&#34;https://ftmao.com/blog/blog/posts/mq-comparison-rabbitmq-kafka-rocketmq/&#34;&gt;消息队列总结：RabbitMQ、Kafka、RocketMQ 与 Redis MQ&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;另一个异步化的例子是我设计的转码调度：&lt;strong&gt;Quartz + DB 状态机&lt;/strong&gt;驱动，每个片源转出 1-3 个分辨率、每个分辨率一个任务，用「动态归一化权重调度算法」分配执行器（priority = 客户权重 / 活跃任务数，兼顾多客户公平与并发槽位控制），30 分钟轮询任务进度。慢不要紧，稳和公平才要紧——这是典型的「离线任务用状态机换可靠性」。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四高可用假设一切依赖都会挂&#34;&gt;四、高可用：假设一切依赖都会挂&lt;/h2&gt;
&lt;p&gt;我做高可用的两条心法，是从 Netflix 的工程实践里学来并在我们系统里反复验证过的：&lt;strong&gt;Design for failure&lt;/strong&gt;（为失败而设计）和 &lt;strong&gt;Blast radius&lt;/strong&gt;（控制爆炸半径）。落地到我们系统：&lt;/p&gt;
&lt;h3 id=&#34;41-熔断降级与兜底重试&#34;&gt;4.1 熔断、降级与兜底重试&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;熔断&lt;/strong&gt;：我们早期直接用 Netflix 开源的 Hystrix 做熔断隔离，Hystrix 停更后迁移到 Resilience4j（网关层用 Sentinel）。工具换了，规则不变：下游超时率/错误率超阈值就熔断，快速失败，绝不线程排队等死。Hystrix 的线程池隔离思想对我们影响很大——核心链路（登录、起播）和非核心链路（埋点上报）的资源池必须分开。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;降级而非失败&lt;/strong&gt;：Meta 自动匹配失败时我定的策略是&lt;strong&gt;降级创建默认元数据&lt;/strong&gt;，不影响文件源状态——内容能播永远比信息完美重要。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;兜底重试&lt;/strong&gt;：CloudAsset 的异常处理是我给全系统定的统一模式——&lt;strong&gt;3 次以内本轮调度重试，超过 3 次任务进失败状态，由独立的失败任务调度器兜底&lt;/strong&gt;。关键设计点：主流程不被重试拖死，失败任务有独立通道排队消化，互不阻塞。&lt;/li&gt;
&lt;/ul&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;graph TB
    A[&amp;#34;任务执行&amp;#34;] --&amp;gt; B{&amp;#34;成功?&amp;#34;}
    B -- &amp;#34;是&amp;#34; --&amp;gt; C[&amp;#34;完成&amp;#34;]
    B -- &amp;#34;否, failCount+1&amp;#34;] --&amp;gt; D{&amp;#34;failCount ＞ 3?&amp;#34;}
    D -- &amp;#34;否&amp;#34; --&amp;gt; A
    D -- &amp;#34;是&amp;#34; --&amp;gt; E[&amp;#34;status = fail&amp;#34;]
    E --&amp;gt; F[&amp;#34;独立失败任务调度器&amp;lt;br/&amp;gt;定时扫描兜底重试&amp;#34;]
    F --&amp;gt; A&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;后来我把这类「一定要跑到」的可靠执行逻辑抽成了通用组件开源，见&lt;a href=&#34;https://ftmao.com/blog/blog/posts/ke-reliable-executor/&#34;&gt;我开发的 KE Reliable Executor&lt;/a&gt;。&lt;/p&gt;
&lt;h3 id=&#34;42-多租户隔离分库就是隔舱&#34;&gt;4.2 多租户隔离：分库就是隔舱&lt;/h3&gt;
&lt;p&gt;多品牌共享一套代码，但我在数据层做了物理隔离：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MySQL 按品牌分库（&lt;code&gt;{brand}_account_db&lt;/code&gt;、&lt;code&gt;{brand}_profile_db&lt;/code&gt;），公共数据在 &lt;code&gt;vod_meta_db&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;account_id&lt;/code&gt; 前缀区分品牌（MFC:11、TVELA:12、WAKA:13），应用层强制路由；&lt;/li&gt;
&lt;li&gt;实体普遍带 &lt;code&gt;env&lt;/code&gt; / &lt;code&gt;vnoId&lt;/code&gt; 字段，&lt;code&gt;logicStreamId&lt;/code&gt; 内嵌环境标识。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;某品牌数据被打爆、误删、需要回滚时，影响面天然被限制在单库内。这就是水密隔舱——后来某品牌做数据订正时我真切体会到这个设计的价值。&lt;/p&gt;
&lt;h3 id=&#34;43-依赖解耦订阅模式&#34;&gt;4.3 依赖解耦：订阅模式&lt;/h3&gt;
&lt;p&gt;DRM 链路里我有个坚持：drmlicense 不直接依赖 Headend 和 drmencryptor 的在线接口，而是以&lt;strong&gt;订阅模式&lt;/strong&gt;收 &lt;code&gt;DrmContentInfoBO&lt;/code&gt; 和 &lt;code&gt;ContentKeyBO&lt;/code&gt;，自己组装 logicalStream 与 PSSH 的关系。上游挂了，license 下发用已订阅到的数据照常工作。原因是算术：&lt;strong&gt;同步调用是可用性的乘法，异步订阅是加法&lt;/strong&gt;——一个可用性 99.9% 的服务同步串依赖十个同样 99.9% 的服务，链路可用性就只剩 99% 了。&lt;/p&gt;
&lt;h3 id=&#34;44-混沌工程轻量版就够&#34;&gt;4.4 混沌工程：轻量版就够&lt;/h3&gt;
&lt;p&gt;Netflix 的 Chaos Monkey 是随机杀生产实例，逼所有服务习惯「邻居随时消失」。我评估后没敢直接上生产，但保留了这个思想的两个轻量实践：预发环境定期随机关停单个实例观察告警与自愈；每个季度做一次依赖演练（手动切断 Redis 从库、Kafka 单 broker），验证降级路径真的能用。我的原则：&lt;strong&gt;降级方案不演练就等于没有&lt;/strong&gt;——每次演练几乎都能发现某条降级路径已经悄悄失效。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五高性能把钱花在读路径上&#34;&gt;五、高性能：把钱花在读路径上&lt;/h2&gt;
&lt;p&gt;高性能章节不长，因为大部分事前面已经做了。补几个我做过的针对性的点：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;优化点&lt;/th&gt;
					&lt;th&gt;做法&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Meta 全量入内存&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;元数据全量进专用 Redis Cluster，终端 &lt;code&gt;/root/v1&lt;/code&gt;、&lt;code&gt;/tree/v1&lt;/code&gt; 等接口零 DB 访问&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;检索走 ES&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;只有关键检索字段入 Elasticsearch，不全量双写&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;播放内容边缘缓存&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Streaming 前置 Squid + CDN，热点片源命中边缘节点，源站只承担回源（对应 Netflix Open Connect 的角色，我们用商用 CDN 实现）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;大表治理&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;观看历史表现网 2-3 亿行，是我一直盯着的最大风险点：每天定时任务删 90 天前历史 + 按账号双表拆分（设备账号/普通账号）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;JVM 与连接池&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;G1/ZGC、HikariCP 连接池按「连接数 = (核数 × 2) + 磁盘数」起步压测调优，慢 SQL 进 Skywalking 告警&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;原则：&lt;strong&gt;性能优化的 80% 是让请求根本到不了慢的地方&lt;/strong&gt;，剩下 20% 才是调参数。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六缓存一致性与缓存高可用&#34;&gt;六、缓存一致性与缓存高可用&lt;/h2&gt;
&lt;p&gt;这是全文我最想细说的一章。缓存是性能的来源，也是我事故最多的地方。&lt;/p&gt;
&lt;h3 id=&#34;61-一致性策略先写-db后删缓存&#34;&gt;6.1 一致性策略：先写 DB，后删缓存&lt;/h3&gt;
&lt;p&gt;vod-auth 的账号数据我用的是标准的 &lt;strong&gt;Cache Aside + 写后删除&lt;/strong&gt;：先写 DB，成功后再删缓存，下次读时回源重建。为什么选「删除」而不是「更新」？我想清楚过：「先写 DB 再删缓存」只有在「读线程恰好踩着写间隙回源了旧值」的极端窗口才会短暂不一致，而「更新缓存」在并发双写下会乱序覆盖出脏数据。删除的失败模式是 miss（回源一次，便宜），更新的失败模式是脏数据（播错内容，贵）。&lt;/p&gt;
&lt;p&gt;对一致性要求更高的场景（套餐权益、付费状态），我在删除之上再加一层：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;延迟双删&lt;/strong&gt;：写 DB → 删缓存 → 延迟 N 秒 → 再删一次，兜住间隙回源；&lt;/li&gt;
&lt;li&gt;或用 MQ 发缓存失效消息，由消费者负责删除 + 失败重试，把「删缓存失败」这个尾巴也闭环掉。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;62-缓存三大经典问题的组合拳&#34;&gt;6.2 缓存三大经典问题的组合拳&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;问题&lt;/th&gt;
					&lt;th&gt;我遇到的场景&lt;/th&gt;
					&lt;th&gt;解法&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;穿透&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;恶意刷不存在的 metaId / accountId&lt;/td&gt;
					&lt;td&gt;布隆过滤器拦截 + 空值短 TTL 缓存（30 秒）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;击穿&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;爆款剧上线，热点 key 过期瞬间&lt;/td&gt;
					&lt;td&gt;逻辑过期 + 互斥重建：value 里带逻辑过期时间，过期后只放一个线程回源，其余返回旧值&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;雪崩&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;大量 key 同 TTL 同时到期&lt;/td&gt;
					&lt;td&gt;TTL 加随机抖动（基础值 ± 20%）+ Redis Cluster 多主分摊&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;逻辑过期方案有个工程细节：回源重建要用分布式锁（Meta 绑定我用的就是分布式锁，获取失败快速失败、重新调度），且锁的 TTL 必须大于回源耗时，finally 里释放——我在 CloudAsset 方案里专门写了「任何异常都会释放分布式锁、清除 ThreadLocal」，这是血泪经验，锁泄漏一次就够排查一晚上。&lt;/p&gt;
&lt;h3 id=&#34;63-缓存自身的高可用&#34;&gt;6.3 缓存自身的高可用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Redis Cluster 多主 + 从&lt;/strong&gt;：Meta 缓存我用独立集群，和 Session/验证码的集群物理隔离——Meta 集群被打挂不影响用户登录。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多级缓存兜底&lt;/strong&gt;：Redis 整体不可用时，L1（Caffeine）还能扛住读过期窗口，服务降级而不是全灭。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;从 EVCache 学到的运营观&lt;/strong&gt;：我研究 Netflix EVCache 时最大的收获不是 memcached 本身，而是&lt;strong&gt;他们把缓存当成 tier-0 服务运营&lt;/strong&gt;——独立容量规划、独立监控、独立 SLO。我照这个思路给缓存集群配了和业务服务同级别的告警与容量评审。缓存在高并发系统里不是优化项，是一级基础设施——这点很多团队要到出事才懂，我庆幸自己学得早。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七零信任安全与必要防御&#34;&gt;七、零信任安全与必要防御&lt;/h2&gt;
&lt;h3 id=&#34;71-零信任永不信任始终验证&#34;&gt;7.1 零信任：永不信任，始终验证&lt;/h3&gt;
&lt;p&gt;我们的安全体系是在几次整改后逐步按零信任思路重建的——内网不再等于可信，&lt;strong&gt;每一次调用都要验证身份，不论它来自哪里&lt;/strong&gt;。落地到我的系统，是这几件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用户侧&lt;/strong&gt;：accessToken（JWT，&lt;code&gt;sub = accountId&lt;/code&gt;）+ 播放票据 tk12（内含 &lt;code&gt;expireTime / accountId / streamId&lt;/code&gt;，由平台密钥签发）。App 起播时两个都要验，token 验「你是谁」，tk12 验「你能不能播这条流、能播到什么时候」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设备侧&lt;/strong&gt;：账号卡与设备绑定（&lt;code&gt;ac_device_binding&lt;/code&gt;），&lt;code&gt;device_limit&lt;/code&gt; 限制并发设备数，挤掉盗号共享的盈利空间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务间&lt;/strong&gt;：内部接口（&lt;code&gt;/internal-api/...&lt;/code&gt;）不走公网网关，服务间调用带服务身份凭证。我评估过 mTLS + SPIFFE 这套工作负载身份方案（Netflix 零信任改造用的就是这条路），对我们体量偏重，最终落地为「内部接口不暴露 + 调用方白名单」，等规模上来再升级。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最小权限&lt;/strong&gt;：cloudProxy 是我专门做的收敛层——把对第三方网盘（Mega 等）的所有访问收敛成一个适配层，只暴露 &lt;code&gt;checkPassword&lt;/code&gt; 和 &lt;code&gt;getDownloadLink&lt;/code&gt; 两个接口，业务服务根本不接触第三方凭证。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;72-一次真实的安全整改网盘密码泄露&#34;&gt;7.2 一次真实的安全整改：网盘密码泄露&lt;/h3&gt;
&lt;p&gt;这是我亲手推动的整改，值得完整复盘。背景：网盘打包工具生成的加密网盘包里&lt;strong&gt;含账号 + 加密后的登录密码&lt;/strong&gt;，加解密密钥一旦泄露，所有网盘账号密码全部泄露。&lt;/p&gt;
&lt;p&gt;整改分两步走：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一步，密码不落地。&lt;/strong&gt; 新网盘包不再写密码，改为写入与网盘账号一一对应的 &lt;code&gt;shareKey&lt;/code&gt;（UUID）。导入时只校验 shareKey 一致性，通过后把网盘与用户关联。按网盘包 &lt;code&gt;version&lt;/code&gt; 字段区分新老包走新旧两条校验逻辑，平滑迁移——&lt;strong&gt;存量兼容是安全改造里最容易被低估的工作量&lt;/strong&gt;，这一步我花的精力比新逻辑本身还多。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二步，密钥体系升级。&lt;/strong&gt; 对称加密改 RSA 非对称加密：&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;graph TB
    A[&amp;#34;CP / Reseller&amp;lt;br/&amp;gt;只拿到 RSA 公钥&amp;#34;] --&amp;gt; B[&amp;#34;公钥加密网盘包&amp;lt;br/&amp;gt;公钥泄露也无法解包&amp;#34;]
    C[&amp;#34;VNO 上传私钥&amp;lt;br/&amp;gt;加密保存于平台 pkg_rsa_key 表&amp;#34;] --&amp;gt; D[&amp;#34;导入时按 VNO 查私钥&amp;#34;]
    B --&amp;gt; E[&amp;#34;网盘包带签名&amp;lt;br/&amp;gt;含 pkgId + keyId&amp;#34;]
    E --&amp;gt; D
    D --&amp;gt; F[&amp;#34;先验签再解密&amp;lt;br/&amp;gt;解密成功才关联用户&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;配套我设计的&lt;strong&gt;密钥轮换机制&lt;/strong&gt;：平台同时最多两个 Key 生效；更新时旧密钥先进入最长 30 天缓冲期，再生成新密钥对；存在生效中的旧密钥则不允许再次更新——防止轮换频率失控导致历史包无法解密。&lt;/p&gt;
&lt;p&gt;另外扫码登录链路我也推动做过同类加固：扫码站点从 S3 静态托管（明文登录）迁移到托管服务并接入加固方案，鉴权统一收口到 vod-auth。&lt;/p&gt;
&lt;h3 id=&#34;73-接口消息体加密https-之上的第二道锁&#34;&gt;7.3 接口消息体加密：HTTPS 之上的第二道锁&lt;/h3&gt;
&lt;p&gt;有人会问：全站都上 HTTPS 了，为什么还要在应用层再加密一次消息体？我的理由有三个，都来自实际教训：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;HTTPS 防不住终端侧的抓包&lt;/strong&gt;。Charles/Fiddler 挂个代理证书，App 和网关之间的明文报文一览无余——协议字段、业务参数、token 用法全暴露。C 端 App 发出去的那一刻起，客户端环境就不可信。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTPS 到网关就终止了&lt;/strong&gt;。TLS 在 ELB/APISIX 卸载后，内网转发的是明文；零信任的原则是内网也不可信，敏感报文在网关之后依然应该处于加密状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IPTV 场景的特殊性&lt;/strong&gt;：播放鉴权、充值、账号接口一旦被逆向出报文格式，黑产就可以脱离 App 直接伪造请求刷接口。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我设计的方案是 &lt;strong&gt;TLS 为底线 + 应用层 AES 加密消息体&lt;/strong&gt;，核心链路如下：&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;graph LR
    A[&amp;#34;App 启动&amp;lt;br/&amp;gt;ECDH 密钥协商&amp;lt;br/&amp;gt;网关公钥经 SSL Pinning 校验&amp;#34;] --&amp;gt; B[&amp;#34;得到会话密钥 sessionKey&amp;lt;br/&amp;gt;按 session 绑定&amp;#34;]
    B --&amp;gt; C[&amp;#34;业务请求&amp;lt;br/&amp;gt;AES-256-GCM 加密 body&amp;#34;]
    C --&amp;gt; D[&amp;#34;明文头带 token + 时间戳&amp;lt;br/&amp;gt;+ nonce + HMAC 签名&amp;#34;]
    D --&amp;gt; E[&amp;#34;网关统一解密过滤器&amp;lt;br/&amp;gt;验签名 / 验时间窗 / 验 nonce&amp;#34;]
    E --&amp;gt; F[&amp;#34;业务服务收到明文&amp;lt;br/&amp;gt;响应同样加密回传&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;工程上的几个取舍，都是试出来的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;密钥协商用 ECDH，不用硬编码密钥&lt;/strong&gt;。早期图省事在 App 里内置过 AES 密钥，App 被反编译后全线失守，改成登录/启动时 ECDH 动态协商、密钥绑定 session、服务端定期强制轮换。SSL Pinning（证书固定）挡住大部分中间人代理，让 ECDH 协商本身不被劫持。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AES-256-GCM 而不是 CBC&lt;/strong&gt;：GCM 自带完整性校验（AEAD），加密和防篡改一次完成；密钥 nonce 绝不复用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;头部明文、body 加密&lt;/strong&gt;：token、时间戳、nonce、签名放明文头里，网关不解密就能先做限流和鉴权前置；body 整体加密，解密收敛在网关的统一过滤器里，业务服务无感知——加解密逻辑散落进每个 Controller 是维护灾难。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不是所有接口都加密&lt;/strong&gt;。EPG、Meta 这类公开内容接口只走 HTTPS + 签名；登录、充值、播放鉴权、账号操作这些敏感接口才上消息体加密。全量加密的 CPU 成本和排查问题的痛苦不成比例——加密后的日志排障要靠专门的脱密工具，这也是成本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;绝不自研算法&lt;/strong&gt;。所有原语都用成熟实现（AES-GCM、ECDH、HMAC-SHA256），自己拼协议可以，自己造算法不行。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;74-必要防御防攻击清单&#34;&gt;7.4 必要防御：防攻击清单&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;攻击面&lt;/th&gt;
					&lt;th&gt;我们的防御手段&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;接口刷量 / CC 攻击&lt;/td&gt;
					&lt;td&gt;网关限流 + Sentinel 熔断；登录、验证码接口按 IP + 账号双维度计数&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;短信/邮件轰炸机&lt;/td&gt;
					&lt;td&gt;验证码存 Redis 带 TTL，&lt;strong&gt;验证成功后立即删除&lt;/strong&gt;防重放；单账号/单 IP 频次限制&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;重放攻击&lt;/td&gt;
					&lt;td&gt;请求带时间戳 + nonce，token 短过期&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;抓包 / 中间人&lt;/td&gt;
					&lt;td&gt;HTTPS + SSL Pinning + 敏感接口消息体加密（见 7.3）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;越权访问&lt;/td&gt;
					&lt;td&gt;tk12 绑定 &lt;code&gt;accountId + streamId&lt;/code&gt;，换账号播放直接鉴权失败&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;篡改网盘包&lt;/td&gt;
					&lt;td&gt;RSA 签名验签，验签不过不解密&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;撞库 / 盗号&lt;/td&gt;
					&lt;td&gt;登录失败计数锁定（&lt;code&gt;account_black_list&lt;/code&gt;）、设备绑定、子账号体系隔离&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;敏感信息外泄&lt;/td&gt;
					&lt;td&gt;交接文档脱敏（我们的交接文档里曾出现明文环境密码，是我抓出来的典型反面教材）；日志脱敏；DB 密码字段只存哈希&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;75-drm内容安全的最后一道门&#34;&gt;7.5 DRM：内容安全的最后一道门&lt;/h3&gt;
&lt;p&gt;DRM 链路的安全设计我同样按零信任思路做：ContentKey 由独立密钥服务（drmencryptor）管理，&lt;strong&gt;先查缓存没有再向外部 DRM 厂商申请并落库缓存&lt;/strong&gt;；播放优先走离线 license（有缓存用缓存），PSSH 异常才降级在线 license。license 本身带过期时间，App 本地不持久化密钥。系统支持 Goose DRM 与 Multi-DRM（Widevine）切换，切换动作就是删旧流加新流——密钥体系与业务流解耦，这个设计让后来的 DRM 厂商迁移几乎没动业务代码。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;八可观测性没有度量就没有高可用&#34;&gt;八、可观测性：没有度量就没有高可用&lt;/h2&gt;
&lt;p&gt;我们的现网组合：&lt;strong&gt;ELK 日志 + Skywalking 链路追踪 + Prometheus/Grafana 指标 + Nacos 配置中心&lt;/strong&gt;，每个子系统独立 ES index。&lt;/p&gt;
&lt;p&gt;我压测过的容量数据（v7.16 集群）：只写日志约 11 万条/秒，只写 Skywalking 约 2 万条/秒，混合写时日志 6 万 + 链路 1 万/秒。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;血泪教训&lt;/strong&gt;：周一高峰 Skywalking 曾出现 4 小时写入延迟，日志量增长也让 Logstash 排队。事后我立了两条规矩：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;日志与链路采样分离&lt;/strong&gt;：高峰期链路数据降采样，保住日志；两者绝不能互相拖死。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本地日志保留 30 天&lt;/strong&gt;，crontab 定时清理——磁盘写满导致的 ELK 雪崩比业务故障还常见。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我还坚持一点：告警要打在&lt;strong&gt;用户可感知的指标&lt;/strong&gt;上（起播成功率、鉴权失败率、链接过期率），而不是只打 CPU 内存——机器好好的但用户看不了，是最尴尬的监控盲区。服务间的依赖关系复杂到一定程度后，「告警来了不知道谁影响了谁」会成为新问题，Netflix 为此专门做过一套服务拓扑系统，我研究过它的实现并整理了笔记：&lt;a href=&#34;https://ftmao.com/blog/blog/posts/netflix-service-topology-at-scale/&#34;&gt;Netflix 服务拓扑系统笔记&lt;/a&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;九总结一张-checklist&#34;&gt;九、总结：一张 checklist&lt;/h2&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;我采用的核心手段&lt;/th&gt;
					&lt;th&gt;用到的 Netflix 开源组件 / 实践&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;高并发&lt;/td&gt;
					&lt;td&gt;网关限流、缓存优先、MQ 削峰、多级缓存&lt;/td&gt;
					&lt;td&gt;Zuul 2 异步化思想、EVCache 启发 keCache&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;高可用&lt;/td&gt;
					&lt;td&gt;熔断降级、失败兜底调度器、多租户分库隔离、订阅模式解耦&lt;/td&gt;
					&lt;td&gt;Hystrix（后迁 Resilience4j/Sentinel）、Chaos Monkey 轻量版&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;高性能&lt;/td&gt;
					&lt;td&gt;Meta 全量入内存、ES 检索、CDN/Squid 边缘缓存、大表治理&lt;/td&gt;
					&lt;td&gt;Open Connect 的边缘缓存角色&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;缓存一致性&lt;/td&gt;
					&lt;td&gt;先写 DB 后删缓存 + 延迟双删、逻辑过期防击穿、随机 TTL 防雪崩&lt;/td&gt;
					&lt;td&gt;EVCache 的缓存运营观&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;零信任&lt;/td&gt;
					&lt;td&gt;双票据鉴权、设备绑定、内部接口白名单、最小权限适配层&lt;/td&gt;
					&lt;td&gt;零信任改造思路（mTLS/SPIFFE 评估后暂缓）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;攻击防御&lt;/td&gt;
					&lt;td&gt;网关 WAF/限流、验证码防重放、RSA 验签、黑名单、敏感接口消息体加密&lt;/td&gt;
					&lt;td&gt;—&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;可观测&lt;/td&gt;
					&lt;td&gt;ELK + Skywalking + Prometheus，日志链路采样分离&lt;/td&gt;
					&lt;td&gt;服务拓扑系统的思路&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;最后一句实话：&lt;strong&gt;这套架构没有一样是发明出来的，全部是踩坑踩出来的&lt;/strong&gt;。网盘密码泄露、Skywalking 四小时延迟、2-3 亿行的历史大表——每个设计决策背后都有一次事故或一次险些事故。Netflix 的开源组件和博客帮我少踩了很多坑，但终究要自己踩过才知道坑有多深。架构师的职责不是画出完美的图，而是让每个坑只踩一次。&lt;/p&gt;
</description>
    </item>
    <item>
      <title>三万个 Flink 作业怎么自动扩缩容：Netflix 两代 Autoscaler 的教训</title>
      <link>https://ftmao.com/blog/posts/flink-autoscaling-netflix-two-autoscalers/</link>
      <pubDate>Fri, 02 Oct 2026 17:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/flink-autoscaling-netflix-two-autoscalers/</guid>
      <description>&lt;h1 id=&#34;三万个-flink-作业怎么自动扩缩容netflix-两代-autoscaler-的教训&#34;&gt;三万个 Flink 作业怎么自动扩缩容：Netflix 两代 Autoscaler 的教训&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;目标读者：三类人——&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;平台/数据工程师&lt;/strong&gt;：正在或准备给 Flink 作业做自动扩缩容，想知道选哪个方案、坑在哪（看&amp;quot;背景二&amp;quot;和&amp;quot;三个工程缺口&amp;quot;）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;架构师&lt;/strong&gt;：在给实时计算平台做选型，需要&amp;quot;外部指标 vs 内部推理&amp;quot;两条路线的完整对比（看两代方案的对比表）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CTO/技术管理者&lt;/strong&gt;：关心自研和开源的边界，Netflix 从自建收敛到开源的过程是一个现成的决策样本（看&amp;quot;三条可以带走的教训&amp;quot;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;没跑过 Flink 也能读：正文前两节是背景知识；跑过的可以直接跳到&amp;quot;第一代&amp;quot;开始看。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;之前那篇&lt;a href=&#34;https://ftmao.com/blog/blog/posts/how-to-get-reliable-tech-knowledge-in-ai-era/&#34;&gt;《软件知识的分层与获取地图》&lt;/a&gt;说过：方案层的知识没有教材，只有案例，大厂技术博客是主力来源。最近去读了 Netflix 技术博客的 &lt;a href=&#34;https://netflixtechblog.com/a-tale-of-two-flink-autoscalers-e9f6a1b1492b&#34;&gt;A Tale of Two Flink Autoscalers&lt;/a&gt;，讲的是他们怎么做&lt;strong&gt;流处理作业的自动扩缩容&lt;/strong&gt;。这篇是我的学习笔记，把里面的架构知识点整理出来。&lt;/p&gt;
&lt;p&gt;问题的形状很普遍：作业负载随昼夜、活动、故障转移大幅波动，按峰值配资源浪费，按均值配资源高峰期 lag 爆炸。Netflix 的答案是先后造了两代 autoscaler，现在正在从自研收敛到开源。两代的设计差异和迁移教训，比算法本身更有价值。&lt;/p&gt;
&lt;p&gt;不过进入正题前，先补两段背景：Flink 是什么、扩缩容在这上面为什么天然就贵；以及开源社区现在有哪些现成方案、谁更好。&lt;/p&gt;
&lt;h2 id=&#34;背景一flink-是什么扩缩容为什么贵&#34;&gt;背景一：Flink 是什么，扩缩容为什么贵&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Apache Flink 是开源的流处理框架&lt;/strong&gt;，核心能力是&amp;quot;对无界数据流做有状态计算&amp;quot;：数据像水管里的水一样源源不断地来（Kafka 消息、日志、点击流），Flink 一边流一边算，算出实时结果（实时报表、实时推荐、风控告警）。&lt;/p&gt;
&lt;p&gt;一个 Flink 作业（job）在逻辑上是一张&lt;strong&gt;算子组成的有向无环图（DAG）&lt;/strong&gt;：数据从 source 进来，经过 map、keyBy、window、join 这些算子的处理，最后写到 sink。&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
    A[&amp;#34;source&amp;lt;br/&amp;gt;读 Kafka&amp;#34;] --&amp;gt; B[&amp;#34;map / filter&amp;lt;br/&amp;gt;转换清洗&amp;#34;]
    B --&amp;gt; C[&amp;#34;keyBy + window&amp;lt;br/&amp;gt;分组聚合 带状态&amp;#34;]
    C --&amp;gt; D[&amp;#34;sink&amp;lt;br/&amp;gt;写下游&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;要理解扩缩容为什么难，需要知道四个机制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;并行度（parallelism）&lt;/strong&gt;：每个算子可以拆成多个并行实例（叫 subtask），分摊在集群的 TaskManager 进程里跑。所谓&amp;quot;扩缩容&amp;quot;，本质就是调整各算子的并行度、增减 TaskManager。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态（state）&lt;/strong&gt;：keyBy 之后的窗口、聚合结果都存在状态里，大作业的状态可以到 TB 级。状态按 key 分布在各个并行实例上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;checkpoint / savepoint&lt;/strong&gt;：Flink 定期把状态整体快照下来，用于故障恢复。&lt;strong&gt;改并行度意味着状态要在新数量的实例间重新分配&lt;/strong&gt;——所以扩缩容的标准动作是&amp;quot;做 savepoint → 停作业 → 按新并行度重启 → 重新加载状态&amp;quot;，状态越大恢复越慢。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;背压（backpressure）&lt;/strong&gt;：下游算子处理不过来，会沿着数据流反向&amp;quot;顶住&amp;quot;上游。背压是判断&amp;quot;瓶颈在哪个算子&amp;quot;的关键信号，也是第二代 autoscaler 的数据基础。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一句话：&lt;strong&gt;无状态服务扩缩容是加台机器的事；有状态流处理扩缩容是一次&amp;quot;带着全部家当搬家&amp;quot;&lt;/strong&gt;。这就是为什么&amp;quot;自动扩缩容&amp;quot;在无状态微服务上早就普及，在 Flink 上却直到这两年才有成熟方案。&lt;/p&gt;
&lt;h2 id=&#34;背景二开源社区有什么方案谁更好&#34;&gt;背景二：开源社区有什么方案，谁更好&lt;/h2&gt;
&lt;p&gt;给流处理作业做自动扩缩容，社区里前后出现过几条路线：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;方案&lt;/th&gt;
					&lt;th&gt;原理&lt;/th&gt;
					&lt;th&gt;粒度&lt;/th&gt;
					&lt;th&gt;现状&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Flink Reactive Mode（1.13 起）&lt;/td&gt;
					&lt;td&gt;作业自适应集群里现有的 TaskManager 数量，配合 K8s HPA 按 CPU 伸缩容器&lt;/td&gt;
					&lt;td&gt;整个作业一刀切&lt;/td&gt;
					&lt;td&gt;简单可用，但按容器指标决策，看不见作业内部瓶颈&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Apache Flink Autoscaler（Flink Kubernetes Operator 内置）&lt;/td&gt;
					&lt;td&gt;从作业内部逐算子算&amp;quot;真实处理速率&amp;quot;，按需逐顶点调并行度&lt;/td&gt;
					&lt;td&gt;逐算子&lt;/td&gt;
					&lt;td&gt;社区主力方向，本文的主角&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;云厂商托管服务（如 AWS Managed Service for Apache Flink）&lt;/td&gt;
					&lt;td&gt;平台按容器利用率自动伸缩&lt;/td&gt;
					&lt;td&gt;整个作业一刀切&lt;/td&gt;
					&lt;td&gt;零运维但粒度粗，复杂有状态作业效果有限&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;各家自研（Netflix 第一代、当年 Twitter Heron 的 Dhalion 等）&lt;/td&gt;
					&lt;td&gt;外部指标（CPU、lag、背压）+ 自研策略&lt;/td&gt;
					&lt;td&gt;整个作业一刀切&lt;/td&gt;
					&lt;td&gt;大多是&amp;quot;社区没有成熟方案时&amp;quot;的历史产物&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;谁更好？&lt;strong&gt;对 2026 年刚开始做这件事的团队，答案基本收敛了&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;作业跑在 K8s 上 → 直接用 Flink Kubernetes Operator 自带的 autoscaler，顶点级、背压感知，是当前最通用的最优解；&lt;/li&gt;
&lt;li&gt;平台是自建的（不在 K8s 上、有自己的控制面）→ 核心算法库已经独立出来，可以像 Netflix 一样只接四个接口（下文详述），不必自建；&lt;/li&gt;
&lt;li&gt;作业简单、无状态、量不大 → Reactive Mode 或云托管的粗粒度伸缩就够用，别上重武器；&lt;/li&gt;
&lt;li&gt;从零自研外部指标 autoscaler → 2019 年的合理选择，2026 年不太成立——Netflix 自己正在从这条路迁走，这就是用脚投票。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有了这个背景，Netflix 这两代方案的故事就好读了：它几乎把上面每条路线都趟了一遍。&lt;/p&gt;
&lt;h2 id=&#34;为什么这个问题在-netflix-躲不掉&#34;&gt;为什么这个问题在 Netflix 躲不掉&lt;/h2&gt;
&lt;p&gt;先看规模：2026 年，Netflix 在多个 AWS 区域跑着 &lt;strong&gt;3 万多个 Flink 作业&lt;/strong&gt;（2017 年开始用 Flink）。这决定了&amp;quot;人肉运维&amp;quot;这条路根本不存在。&lt;/p&gt;
&lt;p&gt;这三万个作业到底在干什么业务？按原文的分类可以拆成两大块：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;大头是托管平台 Data Mesh 自动生成的数据管道&lt;/strong&gt;——所以这三万个作业的主体确实是&amp;quot;大数据相关&amp;quot;，不是在跑在线业务服务。Data Mesh 是 Netflix 的托管数据搬运/处理平台（2023 年他们在博客公开过架构）：用户在界面上声明&amp;quot;数据从哪来、到哪去、中间做什么转换&amp;quot;，平台自动生成 Flink 作业——典型场景是 Kafka 之间的数据搬运、数据库变更（CDC）实时同步到数仓（Iceberg）、事件路由分发。用户大多根本不直接接触 Flink，自然也不会去管扩缩容。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;少数但增长快的是各团队自建自运维的自定义作业&lt;/strong&gt;，承载真正的实时业务逻辑：个性化（实时推荐特征计算）、广告（2022 年广告套餐上线后的曝光与计费遥测）、直播活动（体育赛事直播的实时监控）等。原文里靠 autoscaler 省下 110 万美元的案例，就来自做客户端遥测和日志的团队。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;形态差异也很大：从 Kafka 搬运工式的单算子作业，到带分支、join、TB 级状态的复杂流水线都有——记住这个差异，后面两代 autoscaler 的能力边界正好卡在这里。&lt;/p&gt;
&lt;p&gt;顺带澄清一个容易误解的点：&lt;strong&gt;首页千人千面的推荐，不是每个用户打开时都走一遍 Flink&lt;/strong&gt;。Flink 是流处理引擎，干的是&amp;quot;事件来了就算&amp;quot;，不在用户请求链路上。个性化系统是经典的三层结构（Netflix 早在 2013 年就公开过这套架构）：&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
    A[&amp;#34;离线 offline&amp;lt;br/&amp;gt;批量训练模型&amp;lt;br/&amp;gt;计算全量候选集&amp;#34;] --&amp;gt; D[&amp;#34;在线存储&amp;lt;br/&amp;gt;画像 特征 候选集&amp;#34;]
    B[&amp;#34;近线 nearline&amp;lt;br/&amp;gt;行为事件进 Kafka&amp;lt;br/&amp;gt;Flink 实时更新画像特征&amp;#34;] --&amp;gt; D
    D --&amp;gt; C[&amp;#34;在线 online&amp;lt;br/&amp;gt;打开首页时排序组装&amp;lt;br/&amp;gt;几十毫秒返回&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;你在观看、暂停、评分时产生的行为事件，由近线层的 Flink 作业实时消化、更新画像；你打开首页那一瞬间，在线服务只做&amp;quot;查 + 排&amp;quot;，几十毫秒返回。&lt;strong&gt;把重计算从请求路径挪到事件流上&lt;/strong&gt;，正是个性化系统扛住全球用户规模的关键设计——本文说的&amp;quot;个性化&amp;quot;自定义作业，干的就是近线这一层的活。&lt;/p&gt;
&lt;p&gt;更麻烦的是约束条件：&lt;strong&gt;在这个平台上，扩缩容不是免费的&lt;/strong&gt;。默认动作是&amp;quot;做 savepoint → 优雅停掉作业 → 按新规模重启&amp;quot;，大状态作业一次要几分钟。这意味着扩缩容策略不能冲动——每次动作都是分钟级的中断。&lt;/p&gt;
&lt;p&gt;于是一个真正难的问题摆在面前：&lt;strong&gt;如何在没有人的情况下，让每个作业在需要时拿到需要的资源，同时不把任何东西搞挂？&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;第一代从外面看的-autoscaler2019&#34;&gt;第一代：从外面看的 autoscaler（2019）&lt;/h2&gt;
&lt;p&gt;Netflix 的第一个答案本身就是一条流处理作业：跑在自家 Mantis 平台上，消费遥测系统 Atlas 吐出的集群级实时指标——每个作业的 CPU、网络、Kafka lag、摄入速率、消费速率。&lt;/p&gt;
&lt;p&gt;决策逻辑是几种信号的组合：lag 推导出的追赶时间、CPU/网络利用率阈值、历史性能基线、对近期摄入速率做回归预测，然后决定扩容，或者判断&amp;quot;更小的集群也扛得住未来窗口&amp;quot;从而缩容。&lt;/p&gt;
&lt;p&gt;这个设计有两个当时看来很聪明、事后定义了天花板的选择：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;它独立于 Flink 平台运行&lt;/strong&gt;。Flink 自己出问题时扩缩容不受牵连；而且按&amp;quot;流处理作业&amp;quot;的形态来构建，天然好扩展——每个 autoscaler 节点负责一部分 Flink 作业的指标，从没写过什么分片协调逻辑。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;成绩单不差&lt;/strong&gt;：在几千个托管流水线上稳定砍下 25%-45% 的资源用量（Flink Forward 2020 分享过）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但&amp;quot;从外面看&amp;quot;是有代价的，原文叫它 ceiling（天花板）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;粗粒度 + 单一旋钮&lt;/strong&gt;。它通过容器级指标推理整个集群，而且只能调一个数——TaskManager 总数，也就是作业里所有算子同比例缩放。对单算子简单流水线够用，对多算子、有状态的 DAG（广告、推荐、游戏团队越来越多拿来的正是这种）就无能为力——而这恰恰是它推理不了的作业类型。每接一种新 case 就得加定制逻辑，形不成通用能力。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;指标会骗你&lt;/strong&gt;。autoscaler 的天花板就是外部指标系统的天花板。有的作业明明忙得团团转，CPU 利用率上却毫无体现，作业卡在降级状态里，scaler 根本看不见。更狠的一次：一次网络迁移悄悄改变了部分流量的上报口径，Atlas 里 scaler 依赖的一组指标开始漏报——&lt;strong&gt;这个缺口在生产环境潜伏了很久才暴露&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;到这一步，build vs buy 被重新摆上了桌面。&lt;/p&gt;
&lt;h2 id=&#34;第二代从里面推理的-autoscaler开源方案&#34;&gt;第二代：从里面推理的 autoscaler（开源方案）&lt;/h2&gt;
&lt;p&gt;2019 年自建时，Flink 社区没有成熟的 autoscaler；等 Netflix 重新评估时，已经有了：Apache Flink Autoscaler。思路发生了根本变化——&lt;strong&gt;不从外面看容器，从作业内部推理&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;核心概念是&lt;strong&gt;真实处理速率（TPR, True Processing Rate）&lt;/strong&gt;：一个算子如果满负荷运转，能扛住的吞吐是多少。&lt;/p&gt;
&lt;p&gt;Flink 会按 subtask 上报&amp;quot;每秒真正干活的时间占比&amp;quot;（和背压时间、空闲时间分开统计）。用观测吞吐除以忙碌占比，就能把容量外推到满负荷：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一个算子实测每秒处理 700 条记录、忙碌占比 70%，TPR = 700 / 0.7 = 1000 条/秒。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;有了这个数，autoscaler 从 source 开始遍历整个作业 DAG，结合每个算子的 TPR、输入输出比率和目标利用率，&lt;strong&gt;逐顶点计算需要的并行度&lt;/strong&gt;——保证没有任何一个算子成为瓶颈，而不是把整个集群当整体伸缩。&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
    A[&amp;#34;Flink 上报&amp;lt;br/&amp;gt;每个 subtask 的忙碌占比&amp;#34;] --&amp;gt; B[&amp;#34;TPR = 观测吞吐 / 忙碌占比&amp;lt;br/&amp;gt;外推满负荷容量&amp;#34;]
    B --&amp;gt; C[&amp;#34;从 source 遍历作业 DAG&amp;lt;br/&amp;gt;结合输入输出比率&amp;#34;]
    C --&amp;gt; D[&amp;#34;逐顶点计算目标并行度&amp;lt;br/&amp;gt;没有算子成为瓶颈&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;两代方案的契约差异：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;自研（外部观察）&lt;/th&gt;
					&lt;th&gt;开源（内部推理）&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;信号来源&lt;/td&gt;
					&lt;td&gt;容器级指标（Atlas）&lt;/td&gt;
					&lt;td&gt;作业内 per-vertex 指标&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;决策单元&lt;/td&gt;
					&lt;td&gt;整个集群一个旋钮&lt;/td&gt;
					&lt;td&gt;DAG 逐顶点并行度&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;适用作业&lt;/td&gt;
					&lt;td&gt;单算子简单流水线&lt;/td&gt;
					&lt;td&gt;多算子有状态 DAG&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;配置粒度&lt;/td&gt;
					&lt;td&gt;全局统一&lt;/td&gt;
					&lt;td&gt;每个作业可自定义&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;对 Netflix 来说，决定性的就是后两行：开源方案能扩缩的恰好是自研系统天生处理不了的那类作业，而且每个作业可以带自己的配置（稳定期、阈值等扩缩行为参数）——这正是那些一直被人工扩缩容的自定义作业需要的。&lt;/p&gt;
&lt;h2 id=&#34;落到三万个作业三个工程缺口&#34;&gt;落到三万个作业：三个工程缺口&lt;/h2&gt;
&lt;p&gt;算法层面拿来即用，社区已经把难的部分做完了。Netflix 的工作是让它在自己的体系里可靠运转——这才是&amp;quot;社区能跑&amp;quot;和&amp;quot;Netflix 规模能跑&amp;quot;之间的距离。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;接入方式：独立库 + 四个接口。&lt;/strong&gt; 开源 autoscaler 最初架构在 Flink Kubernetes Operator 里，而 Netflix 的 Flink 平台跑在自己的控制面上。社区后来做了一个关键决策：把核心逻辑抽成独立库，只留四个通用接口——携带作业元数据和 REST 信息的 context、状态存储、事件处理器、执行扩缩动作的 realizer。Netflix 的接入服务是一个 Spring Boot 应用，编排跑在 Temporal（持久化工作流引擎）上：一个编排工作流每分钟轮询一次控制面，为每个开启扩缩容的作业启动一个独立的长运行工作流。&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
    A[&amp;#34;Temporal 编排工作流&amp;lt;br/&amp;gt;每分钟轮询控制面&amp;#34;] --&amp;gt; B[&amp;#34;每个作业一个&amp;lt;br/&amp;gt;独立长运行工作流&amp;#34;]
    B --&amp;gt; C[&amp;#34;从 JobManager 拉&amp;lt;br/&amp;gt;per-vertex 指标&amp;#34;]
    C --&amp;gt; D[&amp;#34;跑开源评估算法&amp;#34;]
    D --&amp;gt; E[&amp;#34;realizer 通过控制面&amp;lt;br/&amp;gt;执行扩缩&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&amp;ldquo;每个作业一个工作流&amp;quot;是直接被 pain 教育出来的设计：最初用单一批处理循环评估所有作业，&lt;strong&gt;一个慢作业或异常作业能卡住它后面所有作业的指标采集和扩缩&lt;/strong&gt;。拆开之后，单个作业出问题只在自己的工作流里失败重试，爆炸半径被隔离，接入更多作业时运行时水平扩展即可。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;三个必须自己补的工程缺口：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;高并行度下的指标采集&lt;/strong&gt;。大作业上从 JobManager 拉指标成为瓶颈，部分原因在 Flink 运行时本身。Netflix 改了 JobManager：缓存瞬时指标名、统一清理而不是每次拉取都重扫，并加了服务端过滤让 autoscaler 只拉需要的指标。效果：可用规模从约 1000 个 subtask 提到 3000 个。这些改动在内部 fork 里，部分已贡献回上游（如 FLINK-36172）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保住 forward chaining&lt;/strong&gt;。由 forward 边连接的两个顶点必须同并行度——记录是在内存里走固定本地通道交接的。如果只扩一个，Flink 不会报错，&lt;strong&gt;它会悄悄把这条边转成网络 shuffle&lt;/strong&gt;（性能完全另一回事）。Netflix 的 fork 会检测 forward 连接的子图，把它们当整体扩缩。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;尊重 sink 的容量上限&lt;/strong&gt;。有些 sink 写入能力有限，把上游作业扩上去只会把自己顶死。fork 里加了 async-sink 背压检测，阻止 autoscaler 把作业扩到 sink 吃不下。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最后还有 realizer 的&lt;strong&gt;安全检查&lt;/strong&gt;：比如公司级区域故障转移期间，拒绝在被撤离的区域缩容；执行前确认新集群磁盘放得下作业的 checkpoint 状态；大集群再加一点 standby 缓冲。&lt;/p&gt;
&lt;h2 id=&#34;效果与调校&#34;&gt;效果与调校&lt;/h2&gt;
&lt;p&gt;开源方案的 autoscaler 去年在 Netflix 自定义作业上 GA，早期结果里有个硬数字：客户端遥测日志团队的 Flink 计算支出&lt;strong&gt;年化下降 58%，约省 110 万美元&lt;/strong&gt;。效率来自三件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;静态配置永远要按峰值留量，autoscaler 吃掉了昼夜和周末低谷；&lt;/li&gt;
&lt;li&gt;性能优化或节后流量回落之后，不再依赖团队手工降配，autoscaler 持续跟进；&lt;/li&gt;
&lt;li&gt;统一容器规格带来更好的装箱（bin-packing）和更细的扩缩粒度。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但&amp;quot;缩得太狠&amp;quot;是另一个坑：砍过头 CPU 打满、lag 飙升，而系统没法立刻反应——每次重启后指标窗口和稳定期都要重建。Netflix 现在把&lt;strong&gt;目标利用率定在 0.45，低于社区默认的 0.7&lt;/strong&gt;，故意用一点效率换稳定：对大型有状态作业，更少、更平稳的扩缩动作值这个价。&lt;/p&gt;
&lt;p&gt;剩下的最大成本已经不是 scaler 的逻辑，而是&lt;strong&gt;重启和状态恢复本身&lt;/strong&gt;。Flink 2 的分离式状态架构（状态放外部存储而不是本地磁盘）有望让扩缩代价与状态大小解耦——Netflix 已开始支持 Flink 2.2，准备实验新 state backend。&lt;/p&gt;
&lt;h2 id=&#34;三条可以带走的教训&#34;&gt;三条可以带走的教训&lt;/h2&gt;
&lt;p&gt;文章结尾总结的三条，泛化到 Flink 之外依然成立：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;指标选择比算法精巧更重要。&lt;/strong&gt; 最有价值的调试很少是关于扩缩数学的，而是&amp;quot;该信哪个信号&amp;rdquo;。调算法之前，先搞懂你的指标——口径会变、会漏报、会测不到真正的瓶颈（第一代 scaler 的两次事故都是指标层的）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;默认值要合理，但留出调节空间。&lt;/strong&gt; 托管作业相似度够高，一套好默认值覆盖大多数——这正是平台的意义。但把单一配置强加给所有作业，是在惩罚不适配的那些：默认值 + 每作业覆盖，同时把需要深专业知识的旋钮藏起来。大多数团队永远不该需要思考 autoscaler 的存在。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;先采用，再扩展（Adopt, then extend）。&lt;/strong&gt; 2019 年自建是因为没有成熟选项；社区方案出现后，正确的动作既不是永远捍卫自己的投资，也不是连夜拆掉，而是：&lt;strong&gt;新工作负载先用开源的，把修复贡献回上游，再有计划地迁移存量&lt;/strong&gt;。自研基础设施的真实成本，要在&amp;quot;本来可以采用现成方案&amp;quot;的对照下衡量。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;一句话收拢：自动扩缩容的难点从来不在&amp;quot;扩缩&amp;quot;二字，而在于&lt;strong&gt;你凭什么相信指标、怎么隔离故障、以及什么时候承认社区已经做得比你好了&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;来源：Samuel Yeboah, Francesco Di Chiara, Mingliang Liu, &amp;ldquo;&lt;a href=&#34;https://netflixtechblog.com/a-tale-of-two-flink-autoscalers-e9f6a1b1492b&#34;&gt;A Tale of Two Flink Autoscalers&lt;/a&gt;&amp;rdquo;, Netflix TechBlog。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>Netflix MAPS 笔记：让推荐模型「看见」素材，冷启动就不冷了</title>
      <link>https://ftmao.com/blog/posts/netflix-maps-multimodal-personalization/</link>
      <pubDate>Fri, 28 Aug 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/netflix-maps-multimodal-personalization/</guid>
      <description>&lt;h1 id=&#34;netflix-maps-笔记让推荐模型看见素材冷启动就不冷了&#34;&gt;Netflix MAPS 笔记：让推荐模型「看见」素材，冷启动就不冷了&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;原文：&lt;a href=&#34;https://netflixtechblog.com/maps-netflixs-multimodal-asset-personalization-at-scale-32f96320785e&#34;&gt;MAPS: Netflix&amp;rsquo;s Multimodal Asset Personalization at Scale&lt;/a&gt;
目标读者：做推荐、搜推广或 AI 应用的工程师，尤其是被冷启动、离线线上不一致折磨过的人。
阅读目标：搞清楚 Netflix 怎么用多模态向量把素材个性化的冷启动问题「绕」过去，以及那套消融实验和代理任务的方法论为什么值得抄。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;问题本身素材是个不透明的-id&#34;&gt;〇、问题本身：素材是个不透明的 ID&lt;/h2&gt;
&lt;p&gt;Netflix 把海报、视频预览这些视觉线索统称为素材（asset），给每个会员挑哪张海报，本身就是一个个性化问题。老模型回答「老标题挑哪张图」回答得很好，但「新标题刚上线怎么办」回答得很差：模型只从历史交互里学习，每张素材在它眼里只是一个不透明的 ID，图里画了什么人、什么色调，它一概看不见。于是新剧上线时素材没有任何历史，只能加大探索流量去「刷数据」，其余时候退回人气启发式——完全不认你的口味。这就是教科书式的冷启动。&lt;/p&gt;
&lt;p&gt;原文的解法一句话：&lt;strong&gt;让模型看见并听见它要推荐的东西&lt;/strong&gt;。新素材一到手就有向量，而向量空间已经承载了会员对相似素材的偏好，所以个性化在接近上线的那一刻就能启动，不用等交互慢慢堆出来。&lt;/p&gt;
&lt;h2 id=&#34;一海报个性化让模型看图&#34;&gt;一、海报个性化：让模型看图&lt;/h2&gt;
&lt;p&gt;做法直接到近乎朴素：用预训练的 CLIP 把每张海报编码成一个 768 维向量，和素材原有的可学习 ID 向量&lt;strong&gt;拼接&lt;/strong&gt;，作为素材的最终表示。就这一个改动。&lt;/p&gt;
&lt;p&gt;效果的关键在于迁移：会员对「某位喜剧演员的脸」「某种配色」的偏好，本来写在图片向量空间里而不是绑死在素材 ID 上，所以新剧海报一编码完，这些偏好立刻生效——哪怕这张图从没给任何人展示过。冷启动从盲区变成了「向量空间对此早有意见」。&lt;/p&gt;
&lt;h2 id=&#34;二五个模型合成一个&#34;&gt;二、五个模型合成一个&lt;/h2&gt;
&lt;p&gt;第二个收益更工程化。同一部剧的海报要铺到五个画布上（billboard、vertical-box、horizontal-panel、short-panel、landscape-panel），裁剪比例各不相同。ID 时代的模型不知道这些裁剪图是同一幕戏，信号无法在画布之间流动，于是历史上每个画布各训一个模型，各自承受各自的冷启动。&lt;/p&gt;
&lt;p&gt;CLIP 向量对裁剪、缩放、宽高比基本不变，同一幕戏的五个版本映射到几乎同一个向量——壁垒没了，五个模型合成一个，高流量画布学到的偏好立刻灌溉稀疏画布。收益最大的恰恰正是数据最少的画布。&lt;/p&gt;
&lt;p&gt;合并带来一个新问题：五个画布的曝光量天差地别，直接按原始计数混训，高流量画布和高频交互类型会淹没一切，本想帮扶的稀疏画布反而最吃亏；手工调权重又换来一堆拍脑袋超参。Netflix 的做法是&lt;strong&gt;奖励加权&lt;/strong&gt;：每条训练样本按其交互类型的长期奖励分（来自 Netflix 既有的长期奖励建模）加权。交互类型在各画布分布不均，按长期价值加权之后，画布配比自动重平衡——画布按「它驱动的交互的长期价值」贡献梯度，而不是按曝光量。没有手工权重，模型优化目标从「最高频的短期动作」换成了「长期会员满意度」。&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
    IMG[&amp;#34;新海报上线&amp;#34;] --&amp;gt; CLIP[&amp;#34;CLIP 图像编码器&amp;lt;br/&amp;gt;输出 768 维向量&amp;#34;]
    ID[&amp;#34;素材 ID 向量&amp;#34;] --&amp;gt; CONCAT[&amp;#34;拼接成素材表示&amp;#34;]
    CLIP --&amp;gt; CONCAT
    CONCAT --&amp;gt; MODEL[&amp;#34;统一模型&amp;lt;br/&amp;gt;五个画布共享&amp;#34;]
    HOT[&amp;#34;高流量画布&amp;lt;br/&amp;gt;billboard 等&amp;#34;] --&amp;gt;|&amp;#34;交互信号沿向量空间迁移&amp;#34;| MODEL
    MODEL --&amp;gt;|&amp;#34;长期奖励加权混训&amp;#34;| SPARSE[&amp;#34;稀疏画布直接受益&amp;lt;br/&amp;gt;short-panel 等&amp;#34;]
    style MODEL fill:#fff3cd
    style SPARSE fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h2 id=&#34;三离线评估ips-与探索流量&#34;&gt;三、离线评估：IPS 与探索流量&lt;/h2&gt;
&lt;p&gt;这套结果能信，前提是评估干净。用当前线上策略的日志评估新模型是有偏的：线上策略偏爱某些素材，日志记录的是「策略选了什么」，不是「会员从全集里会选什么」。和线上策略意见不合的新模型会被冤枉——它会挑的曝光在日志里几乎不存在。&lt;/p&gt;
&lt;p&gt;Netflix 用一小股&lt;strong&gt;随机探索流量&lt;/strong&gt;做 IPS（逆倾向评分）：随机策略按已知分布在候选素材里采样，倾向分在服务时刻精确记录，而不是事后估计。每条观测按倾向分的倒数加权，被探索流量压稀的曝光得到相应抬升，得到候选策略的无偏奖励估计。原文有句话值得划线：&lt;strong&gt;倾向分由构造保证已知而非事后建模，是离线数字能跟踪线上结果的最大单一原因。&lt;/strong&gt; 候选模型必须在 IPS（相对线上基线的比值）上先赢，才配拿 A/B 流量。&lt;/p&gt;
&lt;h2 id=&#34;四消融实验两个成分缺一不可&#34;&gt;四、消融实验：两个成分缺一不可&lt;/h2&gt;
&lt;p&gt;「图片向量」和「模型合并」两个想法捆在一起，Netflix 分开消融，对照老的五模型系统：V1 只加图片向量（仍是五个画布模型），V2 只合并（统一模型但纯 ID），V3 两者都要。离线指标上 ±1% 以内不算显著，V1、V2 单独做基本都在噪声带里；线上全平台跑了至少四周的 A/B 给出更清楚的分界线：&lt;strong&gt;V1、V2 双双不显著，只有 V3 拿到统计显著的提升&lt;/strong&gt;——V3 就是今天线上跑的版本。&lt;/p&gt;
&lt;p&gt;原因也讲得透：V1 告诉单画布模型素材长什么样，但稀疏画布样本太少，教不会它怎么用这条信息；V2 数据够了，但全是 ID 信号，新素材没有。V3 两者兼有，成熟画布教会共享模型「CLIP 向量如何映射到会员偏好」，这个映射直接迁移到稀疏画布。效果是相乘不是相加——V3 在 short-panel 上的提升（5.691%）超过 V1 与 V2 之和。原文的教训写得很直白：&lt;strong&gt;下结论「内容特征没用」之前，先找第二个阻塞因子。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;真实考验来自产品侧：Netflix 十年来最大的电视首页改版，让 short-panel 一夜之间成为主战场——即将吃下最多曝光的画布恰好历史数据最少。V3 赶在改版前上线，配合一个月的 holdback A/B，核心发现指标和播放时长双双显著增长，且增幅比稳态消融更大。&lt;/p&gt;
&lt;h2 id=&#34;五查询感知的海报排序&#34;&gt;五、查询感知的海报排序&lt;/h2&gt;
&lt;p&gt;浏览时你的长期口味是对的信号，搜索时不是：搜某个演员时你要的是带他的海报，哪怕你的广义口味另有指向。搜索页的意图写在 query 里，海报应当响应它。&lt;/p&gt;
&lt;p&gt;因为 CLIP 把文本和图像投进了同一个向量空间，这件事近乎白送：直接算 query 的文本向量与素材图像向量的余弦相似度，和原有个性化分数按权重 α（0 到 1，线上 A/B 调）混合。第一项是「我们认为你喜欢什么」，第二项是「你刚开口要什么」。没有任何额外建模——向量本来就在素材表示里，排序时加一个相似度项而已。&lt;/p&gt;
&lt;h2 id=&#34;六视频预览mediafm&#34;&gt;六、视频预览：MediaFM&lt;/h2&gt;
&lt;p&gt;视频预览比静态图难：吸引力来自运动、节奏、对白、配乐，单帧说不清。第一版内容感知方案 SeqCLIP 只取各帧的 CLIP 向量求平均——看得见长相，听不见声音，而对白和音乐恰恰扛着预览片的大半调性。&lt;/p&gt;
&lt;p&gt;补全这块的是 MediaFM，Netflix 第一个自研多模态地基模型，在 8000 万个镜头（shot）上训练，每个镜头融合三路信号成一个向量：视觉走 SeqCLIP，音频走预训练语音音频编码器，文本是字幕经大规模文本模型编码。接入方式和 CLIP 一样：把向量拼进素材表示，不需要新基建。&lt;/p&gt;
&lt;p&gt;离线 IPS 和五周全平台 A/B 给出一致的排序：&lt;strong&gt;MediaFM ＞ SeqCLIP ＞ 纯 ID&lt;/strong&gt;，每加一层内容感知都有收益，TV 端收益最大。音频和定时文本是 SeqCLIP 这类纯视觉编码器够不到的，这部分增量是真的。MediaFM 已作为默认视频预览向量全平台上线。&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
    subgraph FM[&amp;#34;MediaFM 多模态地基模型&amp;lt;br/&amp;gt;8000 万镜头训练&amp;#34;]
        V[&amp;#34;视觉&amp;lt;br/&amp;gt;SeqCLIP 帧向量平均&amp;#34;] --&amp;gt; FUSE[&amp;#34;融合成镜头级向量&amp;#34;]
        A[&amp;#34;音频&amp;lt;br/&amp;gt;预训练语音音频编码器&amp;#34;] --&amp;gt; FUSE
        T[&amp;#34;文本&amp;lt;br/&amp;gt;字幕经大规模文本模型&amp;#34;] --&amp;gt; FUSE
    end
    FUSE --&amp;gt; STORE[&amp;#34;Embedding Store&amp;lt;br/&amp;gt;训练与推理同源 无偏斜&amp;#34;]
    STORE --&amp;gt; PROBE[&amp;#34;线性探针代理任务&amp;lt;br/&amp;gt;仅凭向量预测去偏人气赢家&amp;#34;]
    PROBE --&amp;gt;|&amp;#34;跑赢随机猜测才放行&amp;#34;| AB[&amp;#34;离线 IPS + 线上 A/B&amp;lt;br/&amp;gt;MediaFM ＞ SeqCLIP ＞ 纯 ID&amp;#34;]
    style PROBE fill:#fff3cd&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h2 id=&#34;七便宜地挑向量线性探针&#34;&gt;七、便宜地挑向量：线性探针&lt;/h2&gt;
&lt;p&gt;新向量层出不穷，端到端试验太贵——数据工程、重训、数周 A/B 流量，不可能每个候选都走全程。Netflix 用一个便宜问题把漏斗口收窄：&lt;strong&gt;仅凭内容向量，能不能预测一个朴素无个性化策略下哪张素材会赢？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;做法：固定一批标题，用探索数据按倾向分去偏后找出每个标题的人气赢家，打成二值标签；然后只用一个线性探针从素材向量恢复这个标签（标准二值交叉熵），不带标题、演员等任何元数据。保持线性和纯向量是刻意的——它隔离出「人气驱动因子有多少真的编码进了向量」。向量若抓住了人气的语义驱动，线性分类器就该能认出赢家；抓不住，探针就和随机猜测一样，而后者正是它的及格线。&lt;/p&gt;
&lt;p&gt;这个探针先把一大批候选剪到两个决赛选手（SeqCLIP 和最强 MediaFM 变体），再送全程评估。三条信号——探针准确率、离线 IPS、线上 A/B——对两个选手的排序完全一致，所以现在每个 MediaFM 新版本发布前都得先过探针这一关。&lt;/p&gt;
&lt;h2 id=&#34;八embedding-store让一切成立的地基&#34;&gt;八、Embedding Store：让一切成立的地基&lt;/h2&gt;
&lt;p&gt;所有向量——CLIP、SeqCLIP、MediaFM——都活在 Netflix Embedding Store 里（Netflix AI Platform 的组件）。地基模型把原始素材编码成向量只做一次，Store 用同一接口喂给所有下游；关键性质是&lt;strong&gt;训练时和在线推理时服务的是完全相同的向量&lt;/strong&gt;，模型学到的和线上见到的之间没有 skew。&lt;/p&gt;
&lt;p&gt;另一个性质是解耦：新向量（或老向量的新版本）注册、回填、验证全部独立进行，不碰任何消费方的训练和推理代码，下游靠配置即可启用。CLIP 进海报模型、同一批向量撑起查询感知排序、MediaFM 滚进视频预览模型，各自是独立变更而不是跨团队迁移。下一步目标是把 Store 推向图文视频统一的语义空间，做跨模态检索和跨界面统一排序。&lt;/p&gt;
&lt;h2 id=&#34;工程实践联想&#34;&gt;工程实践联想&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;冷启动的最优解常常是换表示，不是加探索。&lt;/strong&gt; 探索流量和人气回退都是在「ID 无知」的框架里打补丁；一旦实体有了内容向量，偏好沿向量空间迁移，问题在表示层就消掉了。做实体冷启动（新商品、新文档、新用户）时先问：这个实体有没有一个下游模型读得懂的内容表示。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「特征没用」往往是因为别处先堵着。&lt;/strong&gt; V1 的失败不是图片向量没信息，是稀疏画布样本不够教模型用；V2 的失败不是合并不对，是 ID 信号对不上新素材。多因素实验里单变量不显著，先怀疑第二个阻塞因子，别急着枪毙特征。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;评估体系的纯度比模型重要。&lt;/strong&gt; IPS 能跟踪线上结果，靠的是倾向分在服务时刻由构造已知——日志里事后估出来的倾向分自带一层建模误差。值得为评估专门留一股随机探索流量，这笔钱花在刀刃上。&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; 训练推理同源、版本注册回填独立、下游配置即用——这三条让「换向量」从跨团队项目降级为一次配置变更。Embedding Store 这类组件才是多模态落地真正的护城河，模型本身反而不是。&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;：素材个性化的冷启动——老模型把素材当不透明 ID，新素材只能靠探索流量和人气启发式硬扛。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;海报&lt;/strong&gt;：CLIP 768 维向量拼接 ID 向量，偏好沿向量空间即时迁移；五个画布模型合并为一个，混训按交互类型的长期奖励分加权，画布按长期价值而非曝光量贡献。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;评估&lt;/strong&gt;：专门探索流量 + IPS，倾向分服务时刻已知，离线数字才跟得上线上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;消融&lt;/strong&gt;：V1（仅向量）、V2（仅合并）线上均不显著，V3（两者兼有）显著胜出，short-panel 提升 5.691% 且超过 V1+V2 之和；电视首页改版实战中 holdback A/B 证实收益更大。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;搜索&lt;/strong&gt;：query 文本向量与素材图像向量的余弦相似度，按 α 与个性化分数混合，零额外建模。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;视频&lt;/strong&gt;：MediaFM（8000 万镜头，视觉+音频+文本三模态）＞ SeqCLIP ＞ 纯 ID，TV 端收益最大，已全平台默认上线。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方法论&lt;/strong&gt;：线性探针代理任务廉价筛选向量，三条信号排序一致后成为 MediaFM 发版门禁；Embedding Store 保证训练推理同源、解耦上下游。&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/maps-netflixs-multimodal-asset-personalization-at-scale-32f96320785e&#34;&gt;MAPS: Netflix&amp;rsquo;s Multimodal Asset Personalization at Scale&lt;/a&gt;（原文，Netflix TechBlog）。&lt;/li&gt;
&lt;li&gt;Radford et al., &amp;ldquo;Learning Transferable Visual Models From Natural Language Supervision&amp;rdquo;（CLIP 原始论文，文中海报向量与图文共空间的来源）。&lt;/li&gt;
&lt;li&gt;Netflix TechBlog 关于长期奖励建模（long-term reward modeling）与 Netflix AI Platform / Embedding Store 的相关文章（原文内引用，用于奖励加权与向量基础设施）。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    <item>
      <title>GenRec 笔记：Netflix 怎么把推荐排序器做成 LLM 原生</title>
      <link>https://ftmao.com/blog/posts/netflix-genrec-llm-native-recommendation/</link>
      <pubDate>Thu, 30 Jul 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/netflix-genrec-llm-native-recommendation/</guid>
      <description>&lt;h1 id=&#34;genrec-笔记netflix-怎么把推荐排序器做成-llm-原生&#34;&gt;GenRec 笔记：Netflix 怎么把推荐排序器做成 LLM 原生&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;原文：&lt;a href=&#34;https://netflixtechblog.com/genrec-towards-llm-native-recommendation-at-netflix-f20be6f643e3&#34;&gt;GenRec: Towards LLM-Native Recommendation at Netflix&lt;/a&gt;
目标读者：做推荐、搜索、排序的后端工程师，以及关心「LLM 怎么落地进成熟业务系统」的 AI 工程师。
阅读目标：搞清楚 GenRec 的两阶段训练框架、目录感知打分头、prefill-only 推理这三个核心机制，以及它用远少于生产系统的标注数据追平甚至反超的原因。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一问题推荐栈的复杂度税&#34;&gt;一、问题：推荐栈的复杂度税&lt;/h2&gt;
&lt;p&gt;Netflix 现有生产排序器是多年演化的结果：数千个手工特征、专门的序列建模结构、特征交互结构、多任务目标，支撑电影、剧集、游戏、直播、播客等多种内容类型和多个产品界面。它能跑，但** onboarding 一个新场景（新内容类型、新界面）的成本极高**——特征工程、架构改动、基础设施、实验，一样都少不了。这是典型的复杂度税。&lt;/p&gt;
&lt;p&gt;与此同时，LLM 圈已经在推荐方向有不少探索（原文点名 PLUM、GLIDE、OneRec-Think）。思路很诱人：把用户历史和物品元数据直接表示成文本，靠 LLM 的世界知识和语言理解在共享语义空间里抓关系，甚至用自然语言 prompt 去「引导」推荐。但&lt;strong&gt;现成 LLM 直接上岗是不合格的推荐器&lt;/strong&gt;：偏向全局热门、幻觉出目录外的物品、无视业务约束、个性化弱。GenRec 要回答的问题就是：把 LLM 正经后训练成排序器，能不能打过一套调了多年的生产系统？&lt;/p&gt;
&lt;p&gt;任务设定本身很标准：全目录排序（有候选集时是 top-K）。给定用户、交互历史、当前上下文（设备、界面、地区、时间），输出一个个性化排序，优化目标是&lt;strong&gt;长期会员效用&lt;/strong&gt;（满意度和留存的代理），不是短期点击。&lt;/p&gt;
&lt;h2 id=&#34;二两阶段先适配再排序&#34;&gt;二、两阶段：先适配，再排序&lt;/h2&gt;
&lt;p&gt;GenRec 的训练分两阶段，分工明确：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Phase 1 —— Netflix 适配的基座 LLM&lt;/strong&gt;：从开源 LLM 出发，在 Netflix 专有语料上继续训练，让它学会内容理解、会员行为与偏好模式，同时保住通用语言能力。更新频率低，作为多个应用共享的「Netflix-aware」基座。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Phase 2 —— GenRec 本体&lt;/strong&gt;：在 Phase-1 基座上用排序数据和排序目标做后训练，专注排序质量与可控性，用奖励加权损失融入多种奖励信号；更新频率高，跟踪新内容和口味漂移；并且从设计上就在推理成本约束下优化。&lt;/li&gt;
&lt;/ul&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
    OSS[&amp;#34;开源 LLM&amp;#34;] --&amp;gt;|&amp;#34;Netflix 专有语料继续训练&amp;lt;br/&amp;gt;低频更新&amp;#34;| P1[&amp;#34;Phase 1&amp;lt;br/&amp;gt;Netflix 适配基座 LLM&amp;lt;br/&amp;gt;多应用共享&amp;#34;]
    LOG[&amp;#34;会员交互日志&amp;lt;br/&amp;gt;播放 时长 评分 弃看 等&amp;#34;] --&amp;gt;|&amp;#34;文本化为对话&amp;lt;br/&amp;gt;user 消息 上下文与历史&amp;lt;br/&amp;gt;assistant 消息 真实行为&amp;#34;| DATA[&amp;#34;Phase 2 训练数据&amp;#34;]
    P1 --&amp;gt; DATA
    DATA --&amp;gt;|&amp;#34;排序目标 + 语言建模目标&amp;lt;br/&amp;gt;+ 奖励加权损失&amp;#34;| P2[&amp;#34;Phase 2&amp;lt;br/&amp;gt;GenRec 排序模型&amp;lt;br/&amp;gt;高频更新 成本约束&amp;#34;]
    RM[&amp;#34;独立奖励模型&amp;lt;br/&amp;gt;长期满意度代理&amp;lt;br/&amp;gt;行为再平衡&amp;#34;] -.-&amp;gt;|&amp;#34;给每条样本一个标量权重&amp;#34;| P2
    style P1 fill:#fff3cd
    style P2 fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这个两阶段划分是全文我觉得最值得抄的部分：&lt;strong&gt;基座慢迭代、排序快迭代&lt;/strong&gt;。基座沉淀通用能力，排序层追内容新鲜度——后面消融实验会证明，Phase-1 越陈旧，Phase-2 的相对收益越大。&lt;/p&gt;
&lt;h2 id=&#34;三训练数据即对话上下文即特征&#34;&gt;三、训练数据即对话，上下文即特征&lt;/h2&gt;
&lt;p&gt;Netflix 会员每天产生千亿级交互事件。GenRec 把日志转成单轮或多轮「对话」：user 消息是文本化的上下文、画像、历史、物品元数据和任务（比如「预测用户接下来会播放或点赞什么」），assistant 消息是会员的真实行为（播了什么、播了多久、给了什么反馈）。Phase-2 训练时，模型学的是 assistant 消息对 user 消息的条件依赖。&lt;/p&gt;
&lt;p&gt;注意一个容易误读的点：&lt;strong&gt;对话格式只为训练服务&lt;/strong&gt;。推理时不做自回归生成——把文本化的上下文喂进去，直接用打分头给候选物品算分。对话形式存在的原因是配合语言建模目标、保住模型对文本的理解能力。&lt;/p&gt;
&lt;p&gt;文本化的瓶颈是 token 预算。把用户历史里每个事件都摊开写，成本在 Netflix 规模下不可接受。于是上下文窗口成了新的「特征预算」，GenRec 的做法是给上下文做工程：&lt;/p&gt;
&lt;ul&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;li&gt;&lt;strong&gt;摘要压缩&lt;/strong&gt;：重复性行为（连续刷剧这类 binge 行为）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选择性展开&lt;/strong&gt;：重要或冷启动物品（比如新上线内容）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;固定预算内优先近期的、高信号的历史，老的压缩或丢掉。prompt 结构上还要最大化共享前缀，吃满 prefix caching。&lt;strong&gt;「prompt 就是新的特征向量」&lt;/strong&gt;——这句话是全文的眼。&lt;/p&gt;
&lt;h2 id=&#34;四三个目标排序语言奖励&#34;&gt;四、三个目标：排序、语言、奖励&lt;/h2&gt;
&lt;p&gt;GenRec 的损失是三件事的组合：&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;：保留 LM 损失，防止模型在文本化输入上丢掉语言理解能力，也给「推荐理由解释」这类生成场景留门。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;奖励加权对齐&lt;/strong&gt;：只在原始行为序列上训练，模型会学出坏习惯——过度偏向刷剧、只推单一内容类型。解法是独立奖励模型给每条训练样本算一个标量权重：长期满意度代理（这次短期行为对回访、目录探索、持续活跃有多大贡献）加行为再平衡（跨内容类型、跨上线阶段调节）。高价值样本加权，低价值样本降权。原文特别说明：这比完整 RL 简单省钱，实践中对齐效果够用；GRPO 这类 RL 方法有额外收益但成本高，留给未来工作。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;五架构与-serving打分头--prefill-only&#34;&gt;五、架构与 serving：打分头 + prefill-only&lt;/h2&gt;
&lt;p&gt;架构贴着基座走：decoder-only Transformer，加一个&lt;strong&gt;目录感知打分头&lt;/strong&gt;，只对 Netflix 在库物品打分。推理管线三步：&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
    V[&amp;#34;Verbalizer&amp;lt;br/&amp;gt;历史 上下文 物品元数据&amp;lt;br/&amp;gt;序列化成单条文本&amp;#34;] --&amp;gt; LLM[&amp;#34;LLM 前向一次&amp;lt;br/&amp;gt;取池化隐状态 h&amp;lt;br/&amp;gt;概括当前偏好与上下文&amp;#34;]
    LLM --&amp;gt; HEAD[&amp;#34;打分头&amp;lt;br/&amp;gt;h 与物品嵌入做点积或小 MLP&amp;lt;br/&amp;gt;softmax 得到分布&amp;#34;]
    CAT[&amp;#34;目录物品嵌入&amp;lt;br/&amp;gt;与主干联合训练&amp;#34;] --&amp;gt; HEAD
    HEAD --&amp;gt; RANK[&amp;#34;排序输出&amp;lt;br/&amp;gt;天然不会幻觉出目录外物品&amp;#34;]
    style HEAD fill:#fff3cd
    style RANK fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;打分头这步设计得很实在：物品嵌入、打分头、主干&lt;strong&gt;联合训练&lt;/strong&gt;；目录极大时用 sampled softmax 或候选集。由于分数只算在目录物品上，「幻觉出目录外内容」这个 LLM 推荐的老毛病在结构上就被掐死了——不需要后处理过滤。&lt;/p&gt;
&lt;p&gt;Serving 在 Netflix 内部 LLM 栈上跑 vLLM。成本由三个因素驱动：模型大小、上下文长度、推理模式。对应三个手段：&lt;/p&gt;
&lt;ul&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;li&gt;&lt;strong&gt;prefill-only 推理&lt;/strong&gt;：不对候选集做自回归解码（那在成本上是自杀），prompt 只消费一次，单次前向给整个候选集打完分。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第三条是整套方案能上线的关键。一个排序器如果按 LLM 聊天那种方式逐 token 生成推荐结果，账永远算不过来；把 LLM 当成「一次性编码器 + 打分头」，账就算过来了。&lt;/p&gt;
&lt;h2 id=&#34;六实验结果少数据反而赢&#34;&gt;六、实验结果：少数据，反而赢&lt;/h2&gt;
&lt;p&gt;对照组是调了多年的生产排序器（数千个特征、定制架构）。结果分几层：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;离线&lt;/strong&gt;：用大约 40 分之一的 Phase-2 标注样本，MRR 反而高约 1.6%。继续加数据、加输入信号，指标继续涨。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;线上 A/B&lt;/strong&gt;：批量计算的推荐界面，约 10% 流量跑约 4 周，短期和长期指标均统计显著为正。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据与模型 scaling&lt;/strong&gt;：1B 和 10B 主干都随 Phase-2 数据量上涨，大模型绝对值更高、曲线形状相似；固定训练预算下，更大主干稳定更好——推荐也吃 scaling law。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Phase-1 vs 开源&lt;/strong&gt;：用 Netflix 适配基座比直接用现成开源 LLM，离线指标高约 10%-20%。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Phase-2 vs Phase-1&lt;/strong&gt;：在 Phase-1 刚训完的时间点，Phase-2 后训练带来 35%-50% 提升；Phase-1 越陈旧（新内容、口味漂移），Phase-2 相对收益越大，2 周后约 80%。这直接论证了两阶段划分的正确性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据效率&lt;/strong&gt;：从强 Phase-1 出发，GenRec 用 10-40 倍更少的 Phase-2 标注样本追平或超过生产排序器——这点值钱在 Phase-2 更新频率高，标注效率直接决定迭代速度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上下文压缩&lt;/strong&gt;：清洗事件、找 MRR 对事件数的「肘点」、调措辞详略，三步下来 token 预算可以压到原来的三分之一左右，离线指标几乎不掉；serving 成本约与上下文长度成正比，所以成本也同比降。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;七更大的判断从特征工程到上下文工程&#34;&gt;七、更大的判断：从特征工程到上下文工程&lt;/h2&gt;
&lt;p&gt;原文最后一节不是在总结 GenRec，是在押注一个范式转移，四条：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;特征工程 → 上下文工程&lt;/strong&gt;：建模范畴从「设计特征」变成「决定哪些信号进上下文、回溯多久、怎么在 token 预算内压缩」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定制架构 → 共享基座&lt;/strong&gt;：双塔、DLRM、定制 attention 各立山头的时代，让位给「一个基座 + 数据与文本化策略 + 后训练目标 + 推理优化」的差异化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scaling law 成为设计指南&lt;/strong&gt;：传统 RecSys 因为稀疏 ID、重工程、任务定制架构容易撞墙；LLM 化的推荐继承了清晰的数据和模型 scaling 行为，投入产出变得可规划。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RecSys 基建 → LLM 基建&lt;/strong&gt;：GPU、vLLM/Triton、batching、caching——推荐 serving 越来越像通用 LLM serving。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;工程实践联想&#34;&gt;工程实践联想&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;「prefill-only」是给 LLM 落地祛魅的一刀。&lt;/strong&gt; 很多团队评估「LLM 进生产」时默认按生成式推理算账，结论当然是不划算。GenRec 的示范是：先问这个任务是不是真的需要逐 token 生成。排序、分类、匹配这类任务，LLM 的正确用法是当编码器，一次前向出全部结果——成本模型完全不同。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目录感知打分头是「防幻觉」的标准答案。&lt;/strong&gt; 不要在生成端防幻觉再靠后处理拦截，把输出空间在结构上约束到合法集合（打分头、受限解码、枚举候选），比训一个「不幻觉的模型」可靠得多。这个模式放之四海皆准：凡是输出必须在闭集内的场景，都该这么干。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;奖励加权是 RL 的穷人版，但可能够你用很久。&lt;/strong&gt; 完整 RLHF/GRPO 的成本和组织复杂度劝退大多数团队；「独立奖励模型给样本算权重、乘进损失」用监督训练的工程成本拿到对齐收益的大部分。先把这条路走通，再谈要不要上 RL。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上下文工程是可以量化的工程，不是 prompt 玄学。&lt;/strong&gt; GenRec 把它做成了有肘点曲线、有详略实验、有三分之一预算硬指标的东西。做 RAG 的人应该直接抄这个流程：清洗事件 → 画「指标 vs 上下文量」曲线找肘点 → 在保留集合上调措辞密度。上下文不是越多越好，是有预算的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「prompt 是新的特征向量」意味着你的特征基建要换。&lt;/strong&gt; 特征平台管的是 Schema、版本、回填；上下文工程管的是文本化模板、压缩策略、共享前缀。如果你的推荐/排序团队还在纯特征工程范式里，GenRec 给出的信号是：下一个五年的技能树在后者。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;本篇小结&#34;&gt;本篇小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GenRec 是什么&lt;/strong&gt;：Netflix 的 LLM 排序器。文本化用户历史与上下文，在 Netflix 适配基座上做排序后训练，挂目录感知打分头，奖励加权对齐长期价值，prefill-only 推理控成本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心机制&lt;/strong&gt;：两阶段（慢基座 + 快排序）、对话格式训练但非生成式推理、打分头把输出约束在目录内、奖励加权替代完整 RL。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键数字&lt;/strong&gt;：40 倍更少标注样本下 MRR +1.6%；Phase-1 基座 +10%-20%；Phase-2 +35%-50%（基座陈旧 2 周后 +80%）；上下文压到三分之一指标几乎不掉；10% 流量 4 周 A/B 短期长期均显著为正。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最大的启示&lt;/strong&gt;：推荐的工程重心从特征工程转向上下文工程，「prompt 就是新的特征向量」；LLM 进排序系统的正确姿势是编码器 + 打分头，不是聊天生成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保留意见&lt;/strong&gt;：原文没给具体基座模型、参数量细节和线上指标绝对值，成本数字也只有相对比较；「match or exceed 生产系统」的结论目前只在批量计算界面上验证过，实时低延迟场景没提。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;参考资料&#34;&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;原文：Ying Li, Arjun Rao, Shradha Sehgal, &lt;a href=&#34;https://netflixtechblog.com/genrec-towards-llm-native-recommendation-at-netflix-f20be6f643e3&#34;&gt;GenRec: Towards LLM-Native Recommendation at Netflix&lt;/a&gt;, Netflix TechBlog, 2026-07-30。&lt;/li&gt;
&lt;li&gt;原文点名的相关 LLM 推荐工作：PLUM、GLIDE、OneRec-Think。&lt;/li&gt;
&lt;li&gt;推理框架：&lt;a href=&#34;https://github.com/vllm-project/vllm&#34;&gt;vLLM&lt;/a&gt;（GenRec serving 所依赖）。&lt;/li&gt;
&lt;li&gt;原文提及的 RL 方法：&lt;a href=&#34;https://arxiv.org/abs/2402.03300&#34;&gt;GRPO&lt;/a&gt;（Group Relative Policy Optimization，DeepSeekMath 论文提出）。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    <item>
      <title>Netflix 自建 LLM 推理平台笔记：四个层层相扣的决策</title>
      <link>https://ftmao.com/blog/posts/netflix-in-house-llm-serving/</link>
      <pubDate>Fri, 17 Jul 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/netflix-in-house-llm-serving/</guid>
      <description>&lt;h1 id=&#34;netflix-自建-llm-推理平台笔记四个层层相扣的决策&#34;&gt;Netflix 自建 LLM 推理平台笔记：四个层层相扣的决策&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;原文：&lt;a href=&#34;https://netflixtechblog.com/in-house-llm-serving-at-netflix-a5a8e799ea2c&#34;&gt;In-House LLM Serving at Netflix&lt;/a&gt;（Netflix TechBlog，2026-07，AI Platform 的 Model Runtime 与 Inference 团队）
目标读者：正在或准备自建 LLM 推理服务的工程师，尤其是要在&amp;quot;用托管 API&amp;quot;和&amp;quot;自己跑全栈&amp;quot;之间做取舍的团队。
阅读目标：看清 Netflix 这套平台的四个核心决策（引擎、打包、API 面、发布）各自约束了什么，以及哪些坑是设计阶段想不到、只有生产流量能教会的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;为什么不直接用托管-api&#34;&gt;〇、为什么不直接用托管 API&lt;/h2&gt;
&lt;p&gt;多数组织通过托管 API 消费 LLM，Netflix 选择在既有生产环境内跑全栈——从模型部署到推理，而不是另起一个 ML 孤岛。原文明确说：这篇文章只讲&amp;quot;曾经认真考虑过备选方案&amp;quot;的决策，而且重点是&lt;strong&gt;为什么&lt;/strong&gt;这么选，以及生产暴露了哪些设计阶段没预料到的东西。这个姿态本身就值得学：决策文档的价值不在结论，在被否决的选项和事后才显形的代价。&lt;/p&gt;
&lt;h2 id=&#34;一架构总览llm-不是特例&#34;&gt;一、架构总览：LLM 不是特例&lt;/h2&gt;
&lt;p&gt;Netflix 的会员规模 ML 由一个统一的 JVM serving 系统打头，负责路由、A/B 测试逻辑、候选生成、特征获取、推理、后处理和逐级日志，实时与离线批量缓存两条路径都支持。调用方到达推理有两条路：经 serving 系统的 gRPC 路径，以及新兴 LLM 应用用的直连 HTTP 路径。&lt;/p&gt;
&lt;p&gt;推理在哪跑取决于模型大小：小的 CPU 模型直接在进程内跑，省掉远程调用开销；大模型需要 GPU，serving 系统本地做前后处理，把推理委托给远端服务 &lt;strong&gt;MSS（Model Scoring Service）&lt;/strong&gt;。MSS 是共享推理后端，XGBoost、TensorFlow、PyTorch、LLM 全走同一套接口，底下是 NVIDIA Triton Inference Server 管模型加载、批处理和 GPU 调度。Triton 之上还有一个 Java 控制面，管部署、版本、健康检查、自动扩缩和多 region  rollout——模型作者只管打包产物和配置，控制面负责 provisioning GPU 实例和零停机升级。&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
    subgraph CALLER[&amp;#34;调用方两条路径&amp;#34;]
        GRPC[&amp;#34;gRPC 路径&amp;lt;br/&amp;gt;经 JVM serving 系统&amp;lt;br/&amp;gt;路由 A/B 特征 后处理 日志&amp;#34;]
        HTTP[&amp;#34;HTTP 直连路径&amp;lt;br/&amp;gt;新兴 LLM 应用&amp;#34;]
    end
    subgraph MSS[&amp;#34;MSS 共享推理后端&amp;lt;br/&amp;gt;XGBoost TF PyTorch LLM 统一接口&amp;#34;]
        TRITON[&amp;#34;Triton Inference Server&amp;lt;br/&amp;gt;模型加载 批处理 GPU 调度&amp;#34;]
        CP[&amp;#34;Java 控制面&amp;lt;br/&amp;gt;部署 版本 健康检查 扩缩 多 region&amp;#34;]
        CP --&amp;gt; TRITON
    end
    GRPC --&amp;gt; MSS
    HTTP --&amp;gt; TRITON
    INPROC[&amp;#34;小模型 CPU 进程内推理&amp;#34;] -.-&amp;gt;|&amp;#34;省远程调用开销&amp;#34;| GRPC
    style MSS fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;一句话：&lt;strong&gt;LLM 不做特殊雪花&lt;/strong&gt;。XGBoost 集成和大型 LLM 用同一个 gRPC 调用打分，客户端库、健康检查、部署流水线全部复用。这个&amp;quot;不特殊化&amp;quot;原则后面会反复出现。&lt;/p&gt;
&lt;h2 id=&#34;二决策一引擎从-tensorrt-llm-换到-vllm&#34;&gt;二、决策一：引擎从 TensorRT-LLM 换到 vLLM&lt;/h2&gt;
&lt;p&gt;平台最初建在 TensorRT-LLM 上——当时性能领先，且已和 Triton 集成。到 2025 年夏天两件事变了：开源引擎的性能差距基本抹平；工作负载也从单一形态扩展到 embedding 生成、ranking/retrieval 的 prefill-only 推理、自回归解码、以及带逐步约束逻辑的自定义模型。重新 benchmark 后选 vLLM 作为 paved-path 引擎，理由是&lt;strong&gt;运营契合度&lt;/strong&gt;而非峰值性能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自定义模型架构免多步编译流水线，非标模型迭代更快；&lt;/li&gt;
&lt;li&gt;有自定义解码逻辑的扩展钩子（后面约束解码全靠它）；&lt;/li&gt;
&lt;li&gt;可调试性好——编译型引擎里查失败和中间状态很痛；&lt;/li&gt;
&lt;li&gt;研究侧很多同学本来就在用 vLLM，研究到生产的交接成本低。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意选型口径：不是&amp;quot;谁快选谁&amp;quot;，是&amp;quot;谁在我们的负载组合 + 团队现状下总成本最低&amp;quot;。&lt;/p&gt;
&lt;h2 id=&#34;三决策二打包方式选-vllm-backend-但要锁版本&#34;&gt;三、决策二：打包方式，选 vLLM backend 但要锁版本&lt;/h2&gt;
&lt;p&gt;Triton 支持两种打包，区别在&lt;strong&gt;模型产物与前端升级的耦合紧度&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Python backend&lt;/strong&gt;：作者在打包时显式定义输入输出 tensor 规格，规格冻结在产物里，必须和前端 request builder 的期望匹配——前端每次动 I/O 规格，打包代码都要配合改，否则运行时才挂。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;vLLM backend&lt;/strong&gt;：产物只有一个指向权重和 tokenizer 的 JSON 配置，I/O 规格由 Triton 在部署时动态生成。模型和前端各自独立演进。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;vLLM backend 是架构上正确的默认，但生产咬了两口：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Triton 与 vLLM 版本漂移&lt;/strong&gt;。Triton 的 vLLM backend 是针对特定 vLLM API 面编译的，一漂移就整个加载失败——真实案例是 Triton 25.09 还在 import &lt;code&gt;vllm.engine.metrics&lt;/code&gt;，而这个模块在 vLLM 0.11.2 里已经删了。对策：烘焙镜像时锁死兼容版本对，并禁止模型作者在打包时覆盖 vLLM 版本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自定义模型逻辑&lt;/strong&gt;。vLLM backend 假设标准 HuggingFace 兼容模型并接管全生命周期；需要自定义前后处理、ensemble 流水线、非标 tokenization 的模型，只能退回 Python backend 拿 &lt;code&gt;execute()&lt;/code&gt; 的完全控制权。这个逃生门对一部分模型会长期必要。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;四决策三api-面grpc-不动加一层-openai-兼容前端&#34;&gt;四、决策三：API 面——gRPC 不动，加一层 OpenAI 兼容前端&lt;/h2&gt;
&lt;p&gt;内部打分协议保持统一 gRPC 不变，但 OpenAI 兼容 API 已成 LLM 生态事实标准——推理引擎、编排框架、评估工具、客户端库全说它——所以平台把它作为&lt;strong&gt;附加&lt;/strong&gt;前端暴露。回报体现在实验到生产的路径上：从托管模型毕业到自托管微调模型（为质量、延迟、成本或数据隐私），几乎是平滑迁移，同一套 API，代码改动极小。&lt;/p&gt;
&lt;p&gt;实现上直接复用 NVIDIA 的 Triton OpenAI 兼容前端：内嵌一个 Triton server，用 &lt;code&gt;TritonLLMEngine&lt;/code&gt; 把请求 schema 转成 Triton 推理请求，FastAPI 对外服务；KServe 的 HTTP/gRPC 前端同时开启，Java 控制面仍走 gRPC 访问同一实例。但直接采用暴露了一个缺口：&lt;strong&gt;&lt;code&gt;response_format&lt;/code&gt; 字段 schema 里接受、实际在到达 vLLM 之前被静默丢弃&lt;/strong&gt;——调用方以为自己要了 JSON 输出，实际没有任何 guided decoding 约束，拿到畸形 JSON 也收不到任何报错。Netflix 的解法是 git-subtree 掉这个前端并打补丁，在请求时把 &lt;code&gt;response_format&lt;/code&gt; 翻译成 vLLM 的 guided decoding 参数。静默丢参数比报错危险得多，这是个通用教训。&lt;/p&gt;
&lt;h2 id=&#34;五决策四发布策略red-black-与-versioned&#34;&gt;五、决策四：发布策略——Red-Black 与 Versioned&lt;/h2&gt;
&lt;p&gt;GPU 部署拉起比 CPU 服务慢，模型版本间 I/O schema 还可能变，发布要解决的是&amp;quot;新版本上线不掉请求&amp;quot;。平台给两种策略：&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
    Q[&amp;#34;新版本要改 I/O schema 吗&amp;lt;br/&amp;gt;例如 tensor 维度变了&amp;#34;] --&amp;gt;|&amp;#34;否 接口稳定&amp;#34;| RB[&amp;#34;Red-Black&amp;lt;br/&amp;gt;新旧并存 健康检查后分阶段切流&amp;lt;br/&amp;gt;新扩容与老缩容同速率 失败原子回滚&amp;#34;]
    Q --&amp;gt;|&amp;#34;是 无法避免&amp;#34;| V[&amp;#34;Versioned&amp;lt;br/&amp;gt;每个 modelId + modelVersion 独立部署&amp;lt;br/&amp;gt;多版本同时在线服务&amp;#34;]
    RB --&amp;gt; RBGAP[&amp;#34;已知缺口&amp;lt;br/&amp;gt;消费者要等新版全量才能改配置&amp;lt;br/&amp;gt;迁移窗口内老请求打到新部署会失败&amp;#34;]
    V --&amp;gt; VCOST[&amp;#34;代价&amp;lt;br/&amp;gt;过渡期 GPU 成本临时上涨&amp;lt;br/&amp;gt;平台按闲置清理旧版本 但永远保留最新&amp;#34;]
    style RB fill:#e8f5e9
    style V fill:#fff3cd&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;Red-Black 的协调缺口很微妙：新版本要改 schema 时，上游消费者&lt;strong&gt;没法提前&lt;/strong&gt;改配置（改了会打到还在线上的旧版），只能等新版全量后再切，于是迁移窗口内必然有&amp;quot;老请求打新部署&amp;quot;的失败。Versioned 用冗余部署彻底解耦模型发布与消费者更新。Netflix 的建议是把可变配置（如 tensor shape）直接嵌进推理模型让它版本无关，尽量走便宜的 Red-Black，Versioned 只留给接口变更无法避免的少数情况。&lt;/p&gt;
&lt;h2 id=&#34;六两个运维细节&#34;&gt;六、两个运维细节&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;启动序列&lt;/strong&gt;。直接启动时从 S3 或 Hugging Face 拉大模型太慢，冷启动会超出调度器容忍度；对策是在模型&amp;quot;announce&amp;quot;时就把模型物化到 Amazon FSx，warm start 命中高性能文件系统而非对象存储。另外 OpenAI 兼容前端内嵌 Triton 还是独立跑 Triton，是打包时按部署配置的。其余步骤是机械的：解包、经 Python &lt;code&gt;entry_points&lt;/code&gt; 装自定义 vLLM 插件、清理 Prometheus multiprocess 目录、引擎就绪前闸住 gRPC 端口。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;统一 metrics 端点&lt;/strong&gt;。vLLM 把指标写成 &lt;code&gt;PROMETHEUS_MULTIPROC_DIR&lt;/code&gt; 下的 &lt;code&gt;.db&lt;/code&gt; 文件，Triton 走自己的 Prometheus 端点，两边互不知情；Triton 内置桥接只暴露 40 多个 vLLM 指标里的 9 个，token 吞吐、KV cache 利用率、prefix cache 命中率这些关键指标全缺。解法是加了一个轻量 HTTP 代理：HTTP 拉 Triton 指标，用 &lt;code&gt;MultiProcessCollector&lt;/code&gt; 从磁盘读 vLLM 指标，合并成单个 &lt;code&gt;/metrics&lt;/code&gt; 输出——存量 dashboard 和告警零改动。&lt;/p&gt;
&lt;h2 id=&#34;七深水区约束解码的规模问题&#34;&gt;七、深水区：约束解码的规模问题&lt;/h2&gt;
&lt;p&gt;这是最硬核的一节。Netflix 一些生产负载需要细粒度控制 token 生成，思路是&lt;strong&gt;把约束推进解码循环内部&lt;/strong&gt;——不为非法生成付钱再重试或修补，而是让模型&amp;quot;构造性地&amp;quot;生成合规输出。实现走 vLLM 的自定义 logits processor 接口：每个约束建模为随已生成 token 历史演进的状态机，每步吐出 token 可用性掩码；每个请求配自己的 processor 实例，因为不同请求规则不同。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;V0 为什么撑不住&lt;/strong&gt;：初版纯 Python 实现功能正确，但 vLLM V0 里自定义 logits processor 是逐请求跑的——GPU 为整个 batch 算出 logits，CPU 拷贝过来等传输完，然后&lt;strong&gt;串行&lt;/strong&gt;对每个请求跑约束逻辑（GIL 挡住了并行）。CPU 耗时随 batch 线性增长，尾延迟爆炸，端到端变成 CPU-bound，尽管 GPU 前向是高效批处理的。这个瓶颈在单请求 benchmark 里完全不可见，只在真实并发下现形。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;V1 的结构修复&lt;/strong&gt;（2025 Q4 迁移）：vLLM V1 把 logits 处理提升到批级，processor 改写为对批级数据结构操作、跨请求一起算掩码，热路径用 C++ 多线程重写绕过 GIL。代价是 V1 API 要求经 &lt;code&gt;update_state(batch_update)&lt;/code&gt; 显式跟踪 batch 成员变化——比 V0 的逐请求接口复杂，但动态演化的 batch 里要维持正确状态别无选择。效果是 logits 处理时间随 batch 增长保持平坦。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;两个设计阶段没想到的状态陷阱&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Partial prefill&lt;/strong&gt;：V1 做 chunked prefill，一个请求可能跨多个引擎 step 才填完，而 &lt;code&gt;BatchUpdate&lt;/code&gt; 的粒度分不清&amp;quot;填完了&amp;quot;还是&amp;quot;填了一半&amp;quot;，只能内部自己跟踪。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Preemption&lt;/strong&gt;：显存压力下 vLLM 可能逐出半完成请求的 KV cache，之后带着不同的 prompt 和输出 token 列表重新调度——这直接打破状态机&amp;quot;输出 token 列表单调增长&amp;quot;的假设。对策是检测 token 历史在解码步之间变短，发现就重置状态机、按新 prompt 重新初始化。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;八工程实践联想&#34;&gt;八、工程实践联想&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;单请求 benchmark 会撒谎。&lt;/strong&gt; V0 约束解码的瓶颈只在真实并发下出现——GPU 批处理越高效，串行 CPU 段相对越刺眼（阿姆达尔定律的实战版）。凡是有&amp;quot;批 GPU + 逐请求 CPU&amp;quot;形态的系统，压测必须带真实并发模型，否则上线即踩雷。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生态兼容比自研协议值钱。&lt;/strong&gt; OpenAI 兼容前端让&amp;quot;托管换自托管&amp;quot;几乎零成本，本质是买了个退出期权。但采用第三方组件时要主动审计&lt;strong&gt;静默行为&lt;/strong&gt;——&lt;code&gt;response_format&lt;/code&gt; 那种&amp;quot;schema 接受、执行丢弃&amp;quot;的缺口，只有端到端验证能抓到，类型系统和 schema 校验都救不了你。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;锁版本不是洁癖，是边界合同。&lt;/strong&gt; Triton 25.09 对 vLLM 0.11.2 的事故说明：两个各自快速迭代的组件之间，API 面就是合同，合同必须显式 pin 住，且要封死用户绕过 pin 的后门（禁止打包时覆盖版本）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接口变更的协调成本要摆上台面。&lt;/strong&gt; Red-Black 的&amp;quot;老请求打新部署&amp;quot;缺口是典型的分布式系统两将军问题变体；Versioned 用资源换解耦是标准解法，但更便宜的做法是从源头消灭破坏式变更——把可变配置嵌进模型。设计 schema 时就该问：这个字段未来变了，谁要协调谁？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有状态逻辑要对底层语义变更做回归审计。&lt;/strong&gt; partial prefill 和 preemption 两个坑，根因都是状态机的隐含假设（一次填完、token 单调增长）被引擎新语义打破。引擎升级时不只跑功能测试，要把每条状态假设列出来逐条对账。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;本篇小结&#34;&gt;本篇小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Netflix 在既有 JVM serving 体系内自建 LLM 推理全栈，核心原则是 &lt;strong&gt;LLM 不做特例&lt;/strong&gt;：所有模型统一 gRPC 打分，共享 MSS + Triton + Java 控制面。&lt;/li&gt;
&lt;li&gt;四个决策层层相扣：引擎选 vLLM（运营契合度胜过峰值性能）→ 打包选 Triton vLLM backend（I/O 规格部署时动态生成，代价是版本锁死，自定义模型留 Python backend 逃生门）→ API 面加 OpenAI 兼容前端（顺手修了 &lt;code&gt;response_format&lt;/code&gt; 静默丢失）→ 发布双策略 Red-Black / Versioned 应对 schema 变更协调。&lt;/li&gt;
&lt;li&gt;运维两个坑：模型先物化到 FSx 解决冷启动；自建代理合并 Triton 与 vLLM 双 metrics 源（官方桥接只暴露 9/40+ 指标）。&lt;/li&gt;
&lt;li&gt;约束解码是全文精华：约束建模为状态机 + 逐步 token 掩码；V0 逐请求串行被 GIL 卡死、CPU 随 batch 线性增长，V1 批级化 + C++ 热路径后耗时拉平；partial prefill 与 preemption 两个状态陷阱靠内部跟踪和&amp;quot;token 历史缩短即重置&amp;quot;解决。&lt;/li&gt;
&lt;li&gt;后续方向：system prompt 压缩、vLLM V1 异步调度、logits processor 向量化成 GPU 融合算子、低精度模型变体。&lt;/li&gt;
&lt;li&gt;全文最值得带走的元教训：设计评审看不出这些坑，&lt;strong&gt;生产负载是最终的验收测试&lt;/strong&gt;——所以平台要为&amp;quot;发现设计阶段想不到的问题&amp;quot;留好观测口和逃生门。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;参考资料&#34;&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://netflixtechblog.com/in-house-llm-serving-at-netflix-a5a8e799ea2c&#34;&gt;In-House LLM Serving at Netflix&lt;/a&gt;，Netflix TechBlog，2026-07-17（原文）。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/vllm-project/vllm&#34;&gt;vLLM&lt;/a&gt;：开源 LLM 推理引擎，本文的 paved-path 选择。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/triton-inference-server/server&#34;&gt;NVIDIA Triton Inference Server&lt;/a&gt;：MSS 底层的模型加载、批处理与 GPU 调度层。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/NVIDIA/TensorRT-LLM&#34;&gt;TensorRT-LLM&lt;/a&gt;：平台初代引擎，被替换前的 baseline。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/kserve/kserve&#34;&gt;KServe&lt;/a&gt;：与 OpenAI 前端并置启用的 HTTP/gRPC 前端。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://aws.amazon.com/fsx/&#34;&gt;Amazon FSx&lt;/a&gt;：模型物化的高性能文件系统，用于压低冷启动。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    <item>
      <title>Netflix 服务拓扑系统笔记：实时依赖地图是怎么被生产环境捶出来的</title>
      <link>https://ftmao.com/blog/posts/netflix-service-topology-at-scale/</link>
      <pubDate>Mon, 13 Jul 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/netflix-service-topology-at-scale/</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/building-service-topology-at-scale-architecture-challenges-and-lessons-learned-f4b792f3f0d8&#34;&gt;Building Service Topology at Scale: Architecture, Challenges, and Lessons Learned&lt;/a&gt;
目标读者：做过或准备做流式数据管线的后端工程师；被 Kafka 消费滞后、JVM GC、热点节点折磨过的人读起来会格外有体感。
阅读目标：搞清楚这套系统的三个核心决策（流式优先、三级聚合管线、动态一致性哈希）各自解决了什么问题，以及 V1 到 V2 连环踩坑的顺序——那个顺序比任何单个方案都值得记住。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;开场白比正文值钱&#34;&gt;〇、开场白比正文值钱&lt;/h2&gt;
&lt;p&gt;这个系列第一篇讲的是 what 和 why：Netflix 工程师需要统一的实时服务依赖视图来加速排障、评估爆炸半径，方案是把 eBPF 网络流、IPC 指标、分布式追踪三个数据源做成物理隔离的三层图。这篇讲的是 how——工程现实。&lt;/p&gt;
&lt;p&gt;作者的开场自白很诚实：&lt;strong&gt;V1 在本地环境完美运行，上生产就是另一个故事&lt;/strong&gt;。Kafka 消费者落后，实例内存耗尽，有的节点流量是其他节点的 100 倍，GC 停顿吃掉的 CPU 比业务逻辑还多。这篇文章不是成功故事，是一趟踩坑复盘。&lt;/p&gt;
&lt;h2 id=&#34;一流式优先新鲜度不是锦上添花&#34;&gt;一、流式优先：新鲜度不是锦上添花&lt;/h2&gt;
&lt;p&gt;传统拓扑系统是批处理：按小时或按天聚合，存全量快照。小规模没问题，但凌晨三点出事故时，一小时前的依赖图是考古，不是可观测性。&lt;/p&gt;
&lt;p&gt;决策是 streaming-first：多区域 Kafka 流和 IPC 指标以 Server-Sent Events 持续摄入，反应式管线处理，拓扑更新延迟在几十分钟量级，对比批处理的小时级、天级。事故响应、变更验证这些场景等不了下一个小时级批次。&lt;/p&gt;
&lt;p&gt;流式处理绕不开一个根本问题：下游慢了怎么办？三个传统方案都不行——无界队列会缓冲到内存爆掉；丢弃式流控会让拓扑残缺，丢的正是连接信息；批处理来不及。他们的答案是&lt;strong&gt;背压&lt;/strong&gt;：Stage 3 写图数据库写不动，通知 Stage 2 减速，Stage 2 通知 Stage 1，Stage 1 让 Kafka consumer 暂停拉取，数据就在 Kafka 里等着。任何一级被打爆（流量尖峰、GC 停顿、外部系统变慢），整条管线自动降到可持续的速率。数据晚到几秒几分钟，但既不丢也不崩。代价是反应式流本身的复杂度，这个后面会还回来。&lt;/p&gt;
&lt;h2 id=&#34;二三层物理隔离&#34;&gt;二、三层物理隔离&lt;/h2&gt;
&lt;p&gt;多源方案落地为三个物理隔离的层，各自配不同的存储：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;网络层&lt;/strong&gt;：eBPF 流日志，存图数据库分区。覆盖全，但没有应用上下文。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IPC 层&lt;/strong&gt;：应用指标，存另一个与网络层隔离的图数据库。端点信息丰富，但只覆盖插桩了的服务。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;追踪层&lt;/strong&gt;：分布式追踪，存列式存储（Parquet）。是真实请求路径，但有采样。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;物理隔离的意义是各层可以按自己的吞吐、查询模式和演进节奏独立优化。查询时并行查相关存储再合并结果，做到亚秒延迟。&lt;/p&gt;
&lt;h2 id=&#34;三三级聚合管线一个架构解两个题&#34;&gt;三、三级聚合管线：一个架构解两个题&lt;/h2&gt;
&lt;p&gt;这是全文的核心。网络流日志有个根本缺陷：&lt;strong&gt;它只能看到单个网络跳，看不到应用级连接&lt;/strong&gt;。云环境里流量很少直连，中间隔着负载均衡器、NAT 网关、API 网关、代理。流日志里 App A 到 LB 是一条流，LB 到 App B 是另一条流，但工程师要的是 App A 到 App B 这条逻辑依赖。&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
    K[&amp;#34;多区域 Kafka&amp;lt;br/&amp;gt;eBPF 流日志&amp;#34;] --&amp;gt; S1[&amp;#34;Stage 1 初始聚合&amp;lt;br/&amp;gt;过滤非法流&amp;lt;br/&amp;gt;5 分钟时间窗聚合&amp;lt;br/&amp;gt;一致性哈希分发&amp;#34;]
    S1 --&amp;gt;|&amp;#34;SSE 流&amp;#34;| S2[&amp;#34;Stage 2 中间件解析&amp;lt;br/&amp;gt;按中间件分组&amp;lt;br/&amp;gt;双跳拼成直连边&amp;lt;br/&amp;gt;再次一致性哈希&amp;#34;]
    S2 --&amp;gt;|&amp;#34;SSE 流&amp;#34;| S3[&amp;#34;Stage 3 最终聚合与富化&amp;lt;br/&amp;gt;查 KV 补元数据&amp;lt;br/&amp;gt;转成图实体&amp;lt;br/&amp;gt;限流写库&amp;#34;]
    S3 --&amp;gt; DB[&amp;#34;图数据库&amp;#34;]
    S3 -.-&amp;gt;|&amp;#34;背压: 写不动就减速&amp;#34;| S2
    S2 -.-&amp;gt;|&amp;#34;背压传播&amp;#34;| S1
    S1 -.-&amp;gt;|&amp;#34;暂停拉取&amp;lt;br/&amp;gt;数据留在 Kafka&amp;#34;| K
    style S2 fill:#fff3cd&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;三个要点：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么三级而不是一级？数据局部性。&lt;/strong&gt; 要把 App A 到 LB 加 LB 到 App B 拼成 App A 到 App B，必须让同一个中间件的所有流落在同一台实例上做 join。而 Stage 1 里流是按 Kafka 分区散在各实例上的。所以 Stage 2 的关键职能是按中间件标识重新分发——经典 map-reduce：Stage 1 做 map，Stage 2 做 shuffle 加 reduce，Stage 3 做最终聚合。&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
    subgraph RAW[&amp;#34;解析前 两条单跳流&amp;#34;]
        A1[&amp;#34;App A&amp;#34;] --&amp;gt; LB[&amp;#34;Load Balancer&amp;#34;]
        LB --&amp;gt; B1[&amp;#34;App B&amp;#34;]
    end
    subgraph RES[&amp;#34;解析后 逻辑依赖&amp;#34;]
        A2[&amp;#34;App A&amp;#34;] --&amp;gt;|&amp;#34;合并双跳指标&amp;lt;br/&amp;gt;记录途经的中间件&amp;#34;| B2[&amp;#34;App B&amp;#34;]
    end
    RAW ==&amp;gt;|&amp;#34;按中间件 shuffle 到同一实例&amp;lt;br/&amp;gt;join 入流与出流&amp;#34;| RES
    style RES fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;为什么三级而不是两级？&lt;/strong&gt; 他们最初就是两级，测试能跑，生产翻车：中间件解析要求把某个热门中间件的所有流集中到一台实例，热点应用的实例成了热节点；而数据富化（查外部存储拿健康状态和元数据）又意味着最忙的实例同时 I/O 最重。拆成三级后，解析（计算密集）和富化持久化（I/O 密集）隔离，每条流被分发、解析、再分发、最后持久化——&lt;strong&gt;负载被两轮哈希打散&lt;/strong&gt;，即使中间件流量是平均值的 100 倍，也没有单实例成为瓶颈。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么 SSE 而不是 gRPC？&lt;/strong&gt; 最初用的 gRPC，结果序列化开销、连接池管理、流式响应的内存压力吃掉的 CPU 比业务逻辑还多。消息队列则白白增加基础设施复杂度。SSE 轻量、HTTP 基础、和反应式流的背压天然集成。教训原话：行业最佳实践不普遍适用，测量，别假设。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么 IPC 层一级就够？&lt;/strong&gt; IPC 指标本来就是应用级的，没有中间件要解析；而且数据从源头就按一致性哈希分对了。这带出一条普适原则：&lt;strong&gt;数据的分区方式决定处理架构&lt;/strong&gt;——进来时分对了就直接聚合，分不对（比如网络流需要中间件解析）就得加 shuffle 级。&lt;/p&gt;
&lt;h2 id=&#34;四动态一致性哈希白嫖现有基础设施&#34;&gt;四、动态一致性哈希：白嫖现有基础设施&lt;/h2&gt;
&lt;p&gt;ASG 动态扩缩容时怎么决定哪个实例收哪个 aggregator？传统做法假设静态集群，扩缩容要显式再平衡、协调服务或手工搬数据。他们的做法：每个实例查服务注册中心拿当前健康的实例列表，排序（保证所有实例视图一致），然后用 &lt;code&gt;findOwnerInstance(aggregator.primaryKey)&lt;/code&gt; 路由。注册中心本来就为健康检查跟踪着 ASG 成员，拿它当集群成员的事实来源，&lt;strong&gt;动态成员资格是白来的&lt;/strong&gt;。一致性哈希保证成员变化时大部分 aggregator 留在原实例，排序列表保证视图一致。扩容时新实例立即接到自己那份流量，部署时 aggregator 无缝漂移到健康实例，全程无协调协议、无人工干预。&lt;/p&gt;
&lt;h2 id=&#34;五v1-的四个坑一个连一个&#34;&gt;五、V1 的四个坑，一个连一个&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;坑一：Kafka 消费滞后。&lt;/strong&gt; lag 从秒涨到分钟再到小时。插桩后发现四个原因：分区数对消费组规模偏少、单次 fetch 拉取记录太少、socket 缓冲区没按吞吐调、跨区域读有额外延迟。对症：加分区、调大每次 fetch 的记录数（拿单条延迟换吞吐）、加大接收缓冲。结果 lag 降到峰值时也在一分钟内。&lt;strong&gt;然后新瓶颈暴露了&lt;/strong&gt;：实例自己吃不下更高的摄入速率——修一个瓶颈必然暴露下一个，这是分布式系统的常态。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;坑二：热点节点与数据放大。&lt;/strong&gt; 最严重的问题。认证、推荐这类热门服务被几百个服务调用，流记录多几个数量级；最初按目的服务做一致性哈希归属，于是拥有热门服务的实例流量是别人的 100 倍。更糟的是洗牌时的数据放大：10 个实例都有本地客户端调某个服务，各自把 aggregator 发给 owner，owner 要合并 10 份。async-profiler 加 heap dump 的结论很清楚：热实例 CPU 大头在 GC，内存压力导致 GC 抖动，抖动吃 CPU，CPU 慢了内存压力更大——恶性循环直到实例宕机，负载转移引发级联故障。解法就是前面那套三级管线：Stage 1 先在内存里做 5 分钟窗口的在线聚合，原始流尽快丢弃被 GC，网络上传的只有压缩后的聚合结果；再经两轮哈希打散。为一个需求（中间件解析）做的架构决策，顺手解了另一个问题（负载不均）——作者说这叫有益的副作用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;坑三：内存与 GC。&lt;/strong&gt; 修完热点还有高堆占用。原因有三：对象要等 5 分钟窗口攒完、数据在各级间做不必要的类型转换、以及——最有争议的一条——按 Scala 最佳实践用不可变数据结构做 aggregator，每次更新都造新对象，每秒百万记录下垃圾回收器直接被淹没。务实选择：&lt;strong&gt;热路径上改用可变 aggregator&lt;/strong&gt;（别处保持不可变），堆分配降了 50% 以上，GC 停顿从几百毫秒降到几十毫秒。教训：最佳实践是起点不是戒律，要用测量来背书，并且清醒地知道自己交换了什么。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;坑四：反应式流的复杂度。&lt;/strong&gt; Pekko Streams 管线会莫名其妙 stall，没有堆栈可查；&lt;code&gt;.async&lt;/code&gt; 操作符一开始用错，过度并行的流开销大于收益。应对：投入时间真正学懂需求驱动模型、把流图简化为线性流优先、在流边界加监控（缓冲区大小、元素吞吐、背压事件）、团队内部沉淀文档。强力抽象需要投资，别假设自己会了。&lt;/p&gt;
&lt;h2 id=&#34;六v2把能跑的打磨成生产级&#34;&gt;六、V2：把能跑的打磨成生产级&lt;/h2&gt;
&lt;p&gt;V1 解决了架构级问题，V2 是持续优化，模式都一样——生产暴露假设，profiling 定位根因，定点修复：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;aggregator 全程直达&lt;/strong&gt;：之前每级之间都把 aggregator 转成完整图实体，其实下级只需要压缩状态。改为只在 Stage 3 落库前转换一次，砍掉两次中间转换。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;序列化统一 JSON&lt;/strong&gt;：自定义序列化导致难以复现的偶发错误。JSON 比二进制略低效，但人可读让调试容易得多，一致性直接消灭了一整类 bug。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流参数调优&lt;/strong&gt;：继续调并行度、缓冲区、&lt;code&gt;.async&lt;/code&gt; 边界位置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;图数据库写入不均&lt;/strong&gt;：某些分区写流量大、某些闲置，限流触发不均。改为批量写入加改进分区分布逻辑。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;富化放在聚合时&lt;/strong&gt;：应用健康、归属等元数据在 Stage 3 落库前补齐，避免查询时的 join 开销。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;七时间旅行任意时刻的拓扑重建&#34;&gt;七、时间旅行：任意时刻的拓扑重建&lt;/h2&gt;
&lt;p&gt;&amp;ldquo;事故发生时调用图长什么样？&amp;ldquo;全量快照存储成本爆炸，事件溯源回放太慢。方案是三件套组合：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;时间窗 aggregator 快照&lt;/strong&gt;：每个 aggregator 带 &lt;code&gt;startTs&lt;/code&gt; 和 &lt;code&gt;endTs&lt;/code&gt;，按 entity_id 加时间戳存进图数据库，相当于每 5 分钟一个检查点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;属性级变更追踪&lt;/strong&gt;：图数据库只存变化过的属性加时间戳，比全实体拷贝省得多，还提供了窗口边界之内的精度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;查询时重建&lt;/strong&gt;：查时间范围内的 mutation 历史，按序应用，重建拓扑状态。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;外加一个聪明设计：&lt;strong&gt;查询时再聚合&lt;/strong&gt;复用摄入时的同一套 aggregator 类，可以做没预计算过的任意 groupby 维度（可用性层级、业务域、部署集群），支持探索式分析而不撑爆存储。&lt;/p&gt;
&lt;h2 id=&#34;八原文给的三条通用教训&#34;&gt;八、原文给的三条通用教训&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;规模定性地改变一切&lt;/strong&gt;：每秒 100 次可行的东西在每秒 10 万次失败，变化不是线性的是质变的。不可变结构、单级聚合、gRPC，在小规模下全是对的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一次只优化一个瓶颈&lt;/strong&gt;：修 Kafka lag 暴露热点，修热点暴露 GC，修 GC 暴露序列化。这不是失败，是复杂系统的本性。按影响排序，修透一个再修下一个。&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;ol&gt;
&lt;li&gt;&lt;strong&gt;背压是降级策略，不是性能特性。&lt;/strong&gt; 它的价值不在快，在于&amp;quot;慢了也不死不丢&amp;rdquo;。做异步管线时先想清楚：下游慢了，数据去哪？答案如果是&amp;quot;内存里堆着&amp;quot;或&amp;quot;丢掉&amp;rdquo;，早晚要还。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据分区方式决定架构形状。&lt;/strong&gt; IPC 一级聚合、网络流三级聚合，差异全在数据进来时分没分对。设计管线前先问：处理这个 key 需要的数据，天然在一起吗？不在，就认命加 shuffle 级。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对&amp;quot;最佳实践&amp;quot;保持工程师式的不敬。&lt;/strong&gt; 不可变数据结构、gRPC、二进制序列化，三个正确常识在这套系统里全被推翻了——但每次推翻都有 profiling 数据背书，且范围克制（可变只用于热路径）。违反常识不可怕，凭感觉违反才可怕。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优化是一条链，不是一次战役。&lt;/strong&gt; 修一个瓶颈暴露下一个，这是吞吐上升的必然结果。所以别指望&amp;quot;性能优化专项&amp;quot;一劳永逸，把 profiling 工具链（async-profiler、heap dump、GC 日志）和边界监控变成日常设施才是正解。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;白嫖现有基础设施是高级的架构品味。&lt;/strong&gt; 拿服务注册中心当集群成员来源，零协调成本拿到动态一致性哈希。新组件引入前先盘一遍手里已有的东西能替代什么——他们连消息队列都因此省掉了。&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;：流式优先加背压（Kafka 当缓冲池，慢下来但不丢不崩）；网络、IPC、追踪三层物理隔离存储，查询时并行合并。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;三级聚合管线&lt;/strong&gt;：Stage 1 时间窗聚合、Stage 2 按中间件 shuffle 并解析双跳、Stage 3 富化持久化。一个设计同时解决中间件解析和热点节点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键妥协&lt;/strong&gt;：热路径可变数据结构（堆分配降 50%+）、gRPC 换 SSE、序列化统一 JSON——全是测量驱动的反常识决策。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;时间旅行&lt;/strong&gt;：窗口快照加属性级 mutation 加查询时重建，任意时刻拓扑可查，还能查询时再聚合做任意维度 groupby。&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/building-service-topology-at-scale-architecture-challenges-and-lessons-learned-f4b792f3f0d8&#34;&gt;Building Service Topology at Scale: Architecture, Challenges, and Lessons Learned&lt;/a&gt;（Netflix TechBlog，作者 Parth Jain、Rakesh Sukumar、Yingwu Zhao、Renzo Sanchez-Silva、Nathan Fisher）。&lt;/li&gt;
&lt;li&gt;文中涉及的关键技术：&lt;a href=&#34;https://pekko.apache.org/&#34;&gt;Apache Pekko&lt;/a&gt;（反应式流实现，Akka fork）、&lt;a href=&#34;https://github.com/async-profiler/async-profiler&#34;&gt;async-profiler&lt;/a&gt;（JVM 采样分析器，热点与 GC 根因定位的主力工具）、&lt;a href=&#34;https://www.reactive-streams.org/&#34;&gt;Reactive Streams&lt;/a&gt;（背压协议规范）。&lt;/li&gt;
&lt;li&gt;系列第一篇介绍多源拓扑的 what 与 why，第三篇将讲追踪层与统一查询，均在 Netflix TechBlog 连载。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    <item>
      <title>GenPage 学习笔记：Netflix 用一个 Transformer 直接生成整个首页</title>
      <link>https://ftmao.com/blog/posts/netflix-genpage-generative-homepage/</link>
      <pubDate>Mon, 29 Jun 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/netflix-genpage-generative-homepage/</guid>
      <description>&lt;h1 id=&#34;genpage-学习笔记netflix-用一个-transformer-直接生成整个首页&#34;&gt;GenPage 学习笔记：Netflix 用一个 Transformer 直接生成整个首页&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;原文：&lt;a href=&#34;https://netflixtechblog.com/genpage-towards-end-to-end-generative-homepage-construction-at-netflix-77146fba8a08&#34;&gt;GenPage: Towards End-to-End Generative Homepage Construction at Netflix&lt;/a&gt;
目标读者：做推荐、搜推广或 AI 工程的工程师；也适合想看「LLM 范式怎么落进重工业级在线系统」的人。
阅读目标：搞清楚两件事——GenPage 怎么把「首页构建」改写成「序列生成」问题，以及为了让这个改写扛住生产环境，他们必须解决哪几个工程难题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一问题的改写从多级流水线到一次生成&#34;&gt;一、问题的改写：从多级流水线到一次生成&lt;/h2&gt;
&lt;p&gt;Netflix 首页不是一个列表，是一个二维结构：哪些行出现、每行里放哪些实体、整体怎么排版，几乎全都个性化。传统做法是多级流水线——行级和实体级各自有召回与排序组件，模型多、阶段之间的优化目标互相对不齐，特征工程沉重。&lt;/p&gt;
&lt;p&gt;GenPage 的改写只有一句话：&lt;strong&gt;把用户历史和请求上下文当作 prompt，自回归生成整个首页作为 response。&lt;/strong&gt; 要回答的问题从「给这个候选打多少分」变成：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;已知关于这个用户和这次请求的一切，应该生成什么样的首页来最大化用户满意度？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;和 TIGER、HSTU、OneRec 这些只生成扁平排序列表的生成式推荐不同，GenPage 把行、实体、布局一起生成。动机四条：端到端建模（一个 transformer 替代多级堆栈，砍掉阶段间目标错位和大量特征工程）；整页级 RL 优化（捕捉行与实体之间的交互，比如顶部的 Continue Watching 满足即时意图但会压低整页浏览深度）；更好的 scaling 行为；以及对新产品形态（直播、游戏、播客、个性化 UI、逐实体艺术图）的扩展性。&lt;/p&gt;
&lt;p&gt;先说成绩再放细节：线上 A/B 对一个成熟、高度优化的多级生产系统，核心用户参与指标统计显著提升（p ＜ 0.001），同时端到端 serving 延迟降了 20%。&lt;/p&gt;
&lt;h2 id=&#34;二数据自建-tokenizer把首页变成一门语言&#34;&gt;二、数据：自建 tokenizer，把首页变成一门语言&lt;/h2&gt;
&lt;p&gt;从传统推荐迁到生成式 transformer，第一件事是重新想数据表示。GenPage 把上下文和首页都编码成一条离散 token 序列。每个训练样本对应一次首页曝光，由三部分组成：Context（用户行为历史、档案属性、请求上下文）、Page（页面上展示的行和实体，按布局顺序）、Feedback（用户与页面的交互）。只有前两者进入 token 化，Feedback 交给奖励系统产生监督信号。&lt;/p&gt;
&lt;p&gt;他们没有用现成的文本 tokenizer，而是为首页数据建了领域专用 tokenizer，换来两个关键优势：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;计算效率&lt;/strong&gt;。举例：「用户 30 天前看了 50 分钟《女子监狱》」这件事，GPT-5 tokenizer 要 16 个 token，他们的方案压到 4 个——&lt;code&gt;[Entity_ID]&lt;/code&gt;、&lt;code&gt;[Action_Type]&lt;/code&gt;、&lt;code&gt;[Action_Time_Bucket]&lt;/code&gt;、&lt;code&gt;[Action_Duration_Bucket]&lt;/code&gt;。序列变短直接降低推理成本和延迟。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;产品控制&lt;/strong&gt;。token 和产品概念（行、实体）一一对应，给业务规则的执行留了一个干净的接口（第五节会看到这条多值钱）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上下文 token 里，用户历史表示为动作序列（显式信号如播放、加片单、点赞，隐式信号如预告片观看、详情页访问），档案 token 编码语言与 profile 类型，请求上下文 token 编码时段、星期几、设备。太长的数据源（比如完整曝光历史）用摘要版本——作者坦白这是手工痕迹，本质是把 prompt engineering 塞回了流水线，端到端学压缩是未来方向。各段之间插特殊 token 作分隔，连续信号（时间戳、时长）分桶离散化。&lt;/p&gt;
&lt;p&gt;页面 token 侧：每个实体、每个行各是一个 token，首页按从左到右、从上到下序列化。词表每日更新纳新；服务时仍不在词表里的实体，靠语义 embedding 融合和 fallback token 处理（见第五节）。分页推荐也自然落在范式里：每次翻页请求前，把已生成行的 page token 连同用户在这些行上的最新实时行为追加进 prompt，模型同时看到长期偏好和会话内行为。&lt;/p&gt;
&lt;h2 id=&#34;三奖励系统一个标量两种用法&#34;&gt;三、奖励系统：一个标量，两种用法&lt;/h2&gt;
&lt;p&gt;监督信号来自 Netflix 内部的奖励系统（既有工作，通过线上 A/B 调参对齐长期满意度）：给页面上每个曝光实体打一个标量奖励——通宵刷完的剧比看了 10 分钟的电影奖励高，被曝光后放弃的实体拿负奖励。&lt;strong&gt;页面级奖励定义为页面上所有曝光实体奖励之和。&lt;/strong&gt; 这个标量同时喂给监督后训练和 RL，但用法不同，见下。&lt;/p&gt;
&lt;h2 id=&#34;四模型与训练llm-配方照搬到推荐&#34;&gt;四、模型与训练：LLM 配方照搬到推荐&lt;/h2&gt;
&lt;p&gt;架构是标准的 decoder-only transformer，理由是简单、灵活、能吃整个 transformer 训练与 serving 生态。有一个值得记的细节：&lt;strong&gt;输入 embedding 和输出投影不共享权重（untie）&lt;/strong&gt;——预训练优化的是词表上的 softmax，WBC 后训练优化的是逐 token 的 sigmoid 分数，两种目标对 logits 的要求不同，解绑给模型留出兼顾的余地。&lt;/p&gt;
&lt;p&gt;训练配方照搬 LLM：先预训练教模型「首页这门语言」，再后训练对齐用户满意度。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;预训练（next-token prediction）&lt;/strong&gt;：只用线上拿到正反馈的曝光样本，让模型 bootstrap 出模仿现有生产系统的能力。作者特意澄清：样本形态其实更像 LLM 的 SFT 对而不是裸文本预训练，叫它预训练是因为从零训练而非微调。局限也直说：预训练只教模仿，不优化奖励幅度；而且模型上线后反复在自己生成的页面上训练，有退化风险——所以要后训练。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后训练之 WBC（weighted binary classification）&lt;/strong&gt;：把生成变成 token 级价值预测。自定义 tokenization 在这里再次兑现——每个页面 token 直接对应一个实体或行，奖励可以干净地挂上去：奖励的符号给二分类标签（正参与 vs 放弃），幅度给权重（通宵刷剧权重高于短播放），对对应 token 的 logit 优化加权二分类交叉熵。此时 logit 可解释为「在这个位置生成这个 token 的价值估计」。推理时贪心逐 token 取最高价值。优点是构造上自带 token 级 credit assignment，不用像 RL 那样去推断每个决策对最终页面奖励的贡献。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后训练之 RL&lt;/strong&gt;：把页面生成当序列决策，直接优化页面级奖励。借鉴 RLHF 两步走：先训一个 transformer 奖励模型，预测生成页面的页面级奖励（以实体奖励之和为监督目标；注意它和奖励系统是两回事——奖励系统处理真实曝光的反馈，奖励模型不用真把页面给用户看，这让 RL 能对任意候选页面打分）；再用 Dr. GRPO 优化策略，并用 KL 惩罚把策略锚在预训练 checkpoint 附近——奖励模型只在生产策略生成的页面附近可靠，KL 约束既把页面留在奖励模型的覆盖区内，也压制 reward hacking。另加规则化的 format rewards：页面得长得像行的列表，业务关键的行和实体不能排太靠下。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;RL 相对 WBC 多开三扇门：整页优化（多样性、stopping power、页面级业务约束都能进奖励）；test-time reasoning（推理输出可看作自动化特征工程）；多 token 实体支持——若实体要用多个 token 表示（如 &lt;code&gt;[Show_ID]&lt;/code&gt; 加 &lt;code&gt;[Episode_#]&lt;/code&gt;），WBC 的逐 token 标注会有歧义（一个实体级奖励怎么摊到多个 token 上），RL 优化序列级 return 天然绕开这个问题。&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
    subgraph DATA[&amp;#34;训练样本: 一次首页曝光&amp;#34;]
        CTX[&amp;#34;上下文 token&amp;lt;br/&amp;gt;用户历史 档案 请求上下文&amp;#34;]
        PAGE[&amp;#34;页面 token&amp;lt;br/&amp;gt;行与实体 按布局顺序序列化&amp;#34;]
        FB[&amp;#34;用户反馈&amp;lt;br/&amp;gt;播放 点赞 放弃&amp;#34;]
    end
    CTX --&amp;gt; PRE[&amp;#34;预训练&amp;lt;br/&amp;gt;next-token prediction&amp;lt;br/&amp;gt;只用正反馈样本&amp;#34;]
    PAGE --&amp;gt; PRE
    FB --&amp;gt; RS[&amp;#34;奖励系统&amp;lt;br/&amp;gt;每个曝光实体一个标量奖励&amp;#34;]
    PRE --&amp;gt; WBC[&amp;#34;后训练 WBC&amp;lt;br/&amp;gt;符号作标签 幅度作权重&amp;lt;br/&amp;gt;加权二分类交叉熵&amp;#34;]
    RS --&amp;gt; WBC
    RS --&amp;gt; RM[&amp;#34;奖励模型&amp;lt;br/&amp;gt;预测生成页面的页面级奖励&amp;#34;]
    RM --&amp;gt; RL[&amp;#34;后训练 RL&amp;lt;br/&amp;gt;Dr. GRPO 加 format rewards&amp;#34;]
    PRE --&amp;gt; RL
    RL -.-&amp;gt;|&amp;#34;KL 惩罚锚定预训练 checkpoint&amp;lt;br/&amp;gt;防 reward hacking&amp;#34;| PRE
    style PRE fill:#fff3cd
    style RL fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&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;。新内容没有交互数据，token embedding 学不出来。两个互补策略：context injection——把新实体或时效实体（如 Live Now 赛事）的元数据直接注入上下文 token；semantic embedding fusion——实体表示改为 ID embedding 与内容 embedding（来自简介、演职员、字幕、类型、视频内容）的融合，训练时以小概率把实体 ID token 随机换成通用 fallback token，逼模型学会只靠内容 embedding 做推荐。效果：新实体的内容元数据一到，它在潜在空间里立刻就有有意义的表示，不等交互数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模型新鲜度&lt;/strong&gt;。每天从头重训大 transformer 不现实，但推荐模型必须跟上趋势和新片入库。方案是 multi-cadence incremental training：大周期在全量历史窗口上做完整预训练加后训练；两次大周期之间，每天从昨天的 checkpoint 继续后训练，数据用最新一天加历史采样的混合，防过拟合和灾难性遗忘。每日新进词表的 token 用同类型 fallback token 初始化（如新行用 &lt;code&gt;[Row_Fallback_Token]&lt;/code&gt;），训练时随机把少量已知 token 替换成 fallback，教模型优雅处理未知 token。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;业务规则&lt;/strong&gt;。首页必须满足结构约束（行的列表）和产品逻辑（去重、行钉位置、类目一致性——喜剧行里必须都是喜剧）。训练信号只能鼓励服从，不能保证。所以规则在推理时强制执行：constrained decoding，每个自回归步先算出合规 token 的掩码打到 logits 上。自定义 tokenization 让这件事格外便宜——实体和行都是单 token，业务规则直接映射成 token 级掩码，没有文本词表上多 token 记账的麻烦。钉行就是把那个位置其他 token 全 mask 掉。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推理延迟&lt;/strong&gt;。逐 token 生成整个页面太贵。利用首页结构做 hybrid row decoding：每行只有前几个实体（用户注意力最集中、决定行质感的位置）自回归生成，然后基于这个前缀做一次前向，拿到所有合格实体的 logits，按分数补齐该行其余位置，同样受业务规则约束。自回归条件性保留在最值钱的地方，长行的逐 token 解码成本省掉了。&lt;/li&gt;
&lt;/ol&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
    REQ[&amp;#34;请求&amp;#34;] --&amp;gt; CTX2[&amp;#34;构造上下文 token&amp;lt;br/&amp;gt;含会话内最新行为&amp;#34;]
    CTX2 --&amp;gt; GEN[&amp;#34;自回归生成&amp;lt;br/&amp;gt;每行只生成前几个实体&amp;#34;]
    GEN --&amp;gt; MASK[&amp;#34;约束解码&amp;lt;br/&amp;gt;业务规则映射为 token 掩码&amp;#34;]
    MASK --&amp;gt; GEN
    GEN --&amp;gt; FILL[&amp;#34;整行补齐&amp;lt;br/&amp;gt;一次前向取全部合格实体&amp;lt;br/&amp;gt;按分数补满该行&amp;#34;]
    FILL --&amp;gt; OUT[&amp;#34;输出首页若干行&amp;#34;]
    OUT -.-&amp;gt;|&amp;#34;翻页时把已生成行和最新行为&amp;lt;br/&amp;gt;追加回 prompt&amp;#34;| CTX2
    style MASK fill:#fff3cd&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h2 id=&#34;六离线实验真正起作用的是什么&#34;&gt;六、离线实验：真正起作用的是什么&lt;/h2&gt;
&lt;p&gt;消融在 Netflix 内部数据上做，模型约 200M 参数，只报各研究内部的相对比较。四个问题，四个答案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;预训练有用吗？&lt;/strong&gt; 有。WBC 前加不加预训练对比，全指标显著提升。Entity AUC 从 0.91 到 0.92 看着小，翻译过来是随机正负实体对的错排率从 9% 降到 8%——成熟生产系统上单点改动罕见的幅度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模型规模怎么 scale？&lt;/strong&gt; 从约 120M 扫到约 900M 参数，预训练 loss 和 WBC loss 都呈幂律下降，和 LLM 的 scaling 行为同构。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上下文信息怎么 scale？&lt;/strong&gt; 固定模型规模，持续丰富 prompt（加新数据源加改进 tokenization），WBC loss 大幅下降。两组扫描不严格可比，但差距悬殊：&lt;strong&gt;模型扩约 7.5 倍只降 WBC loss 约 1.3%，上下文富化累计降约 6.9%&lt;/strong&gt;，好几次单次设计良好的上下文加入就超过整轮扩容。结论：当前阶段个性化质量的瓶颈首先是模型能看到的信息和表示，其次才是容量；等上下文榨干了，容量才会成为主驱动。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RL 真在做页面级优化吗？&lt;/strong&gt; 页面奖励持续提升（作者承认这条基本是 confirmatory——奖励和策略优化用的是同一个模型）。更有意思的副产品：&lt;strong&gt;多样性根本不在 RL 目标里，但页面多样性（页内实体两两 embedding 距离）随训练上升&lt;/strong&gt;——策略在整页层面优化，而不是孤立地抠每个 token。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;七线上-ab-与意外发现&#34;&gt;七、线上 A/B 与意外发现&lt;/h2&gt;
&lt;p&gt;线上对生产基线的 A/B 里，GenPage 在既有候选集上解码（候选集顺便处理掉大量资格类业务规则）。几个观察都值得记：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有训练数据配置不同的变体都在核心指标上显著提升，增益对设计选择鲁棒，不依赖某个特定配置。&lt;/li&gt;
&lt;li&gt;出现了非预期的曝光类目分布漂移（新内容 vs 老内容、剧集 vs 电影）。作者的诊断很诚实：这大概是 GenPage 个性化更锐利的表现——首页 impression 效率上升，用户用更少曝光完成参与；而更锐利的个性化把从生产系统继承来的组件（比如奖励系统）尚未与新范式对齐的问题暴露了出来。&lt;/li&gt;
&lt;li&gt;会话内响应性强且自然：最新行为迅速影响后续推荐，一两天后淡回长期偏好——模型自己在 attend 动作时间戳，生产栈里要靠大量手工特征工程才能做到的事，在生成式范式里涌现出来。&lt;/li&gt;
&lt;li&gt;延迟降 20%，和「生成式一定慢」的直觉相反：多个排序阶段和重特征计算被一个跑在原始 token 上的 transformer 取代，自定义 tokenization 与 hybrid decoding 再压缩了解码步数。作者强调这还没压榨完，余量可以再投给容量或更富的 prompt。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;工程实践联想&#34;&gt;工程实践联想&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;prompt 工程是新的特征工程，而且 ROI 可能更高。&lt;/strong&gt; 6.9% 对 1.3% 这组数字值得贴在每个推荐团队的墙上：在堆模型容量之前，先问自己信息瓶颈有没有解决——模型看不见的信号，再大的容量也学不到。这和我们日常给 LLM 应用调 prompt 的经验完全同构：多数时候收益不在换更大的模型，在把上下文喂对。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自定义 tokenizer 本质是一次领域建模。&lt;/strong&gt; 4 token 对 16 token 的压缩只是表面收益，真正值钱的是「token 与产品概念一一对应」——它让业务规则从模型输出后的事后修补，变成解码期的一层掩码。凡是需要严格控制生成内容的场景（合规、结构约束、配额），这个思路可以直接搬：把控制点设计进词表，而不是指望 prompt 约束。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;规则兜底放在解码时，别指望训练。&lt;/strong&gt; 训练信号只能鼓励、不能保证——这句坦白适用于所有生成式系统。凡是硬约束（不能出现的内容、必须出现的位置），都应该有推理期的机制级保障，RLHF 式的软对齐管软偏好。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模仿学习起步、对齐训练接棒，是离线冷启动新系统的通用配方。&lt;/strong&gt; 先用生产系统的正反馈样本 bootstrap，避开没有策略就没有数据的死锁；再承认模仿的天花板（不优化奖励幅度、自蒸馏会退化），用后训练接棒。WBC 和 RL 的二选一也很务实：先用简单可优化的对齐拿下实体级指标，把 RL 留给页面级目标，不上来就啃最硬的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「单模型替代流水线」的延迟账要重新算。&lt;/strong&gt; 直觉上生成式更慢，实际是把 N 个模型加 N 套特征计算换成一次前向序列。系统总成本从来不在单个模型的 FLOPs 上，在组件数量、数据依赖和特征管线上——架构简化的收益经常大于模型本身的开销。&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;：首页构建从「多级召回排序」改写为「prompt 进、页面出」的自回归序列生成，行、实体、布局一次生成；这是与 TIGER、HSTU、OneRec 等扁平列表生成式推荐的分界。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据&lt;/strong&gt;：领域专用 tokenizer 把上下文和页面编码成一条 token 序列，换来 4 倍级序列压缩和「token 即产品概念」的控制接口；反馈不进序列，进奖励系统。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;训练&lt;/strong&gt;：照搬 LLM 配方——正反馈样本上 next-token 预训练，后训练二选一：WBC（token 级价值预测，自带 credit assignment）或 RL（奖励模型 + Dr. GRPO + KL 锚定，解锁整页优化、test-time reasoning、多 token 实体）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上线四件套&lt;/strong&gt;：冷启动靠 context injection 加 semantic embedding fusion；新鲜度靠多节奏增量训练加 fallback token；业务规则靠 constrained decoding（规则即 token 掩码）；延迟靠 hybrid row decoding（每行只自回归前几个实体）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心结论&lt;/strong&gt;：线上核心指标显著优于成熟多级基线且延迟降 20%；离线两条最值得记——丰富 prompt 的收益远大于模型扩容（6.9% 对 1.3%），RL 在不含多样性目标时反而提升了页面多样性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;未竟之事&lt;/strong&gt;：长上下文仍靠手工摘要，语言、多模态、reasoning 能力尚未接入；作者看好的方向是领域 token 与通用文本 token 的混合 tokenization——概念上等于给 LLM 增加一个「推荐模态」，LLM 和推荐系统的边界会继续模糊。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这篇最打动我的不是架构，是那份诚实：明说预训练样本其实更像 SFT，明说长上下文还是手工摘要，明说奖励提升是 confirmatory，明说线上出现了没优化的分布漂移。工业系统的论文能写到这个坦白程度，比指标本身更值得学。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;参考资料&#34;&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Lequn Wang, Jiangwei Pan, Linas Baltrunas, &lt;a href=&#34;https://netflixtechblog.com/genpage-towards-end-to-end-generative-homepage-construction-at-netflix-77146fba8a08&#34;&gt;GenPage: Towards End-to-End Generative Homepage Construction at Netflix&lt;/a&gt;, Netflix TechBlog, 2026-06（本文唯一原文来源）。&lt;/li&gt;
&lt;li&gt;原文提及的对照工作：TIGER、HSTU、OneRec（生成扁平排序列表的生成式推荐系统）。&lt;/li&gt;
&lt;li&gt;原文提及的 RL 算法：Dr. GRPO（GRPO 的变体，缓解训练目标中的偏差）。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    <item>
      <title>Netflix 如何用 Kueue 简化批计算：一次百万级任务的换心手术</title>
      <link>https://ftmao.com/blog/posts/netflix-batch-compute-kueue/</link>
      <pubDate>Mon, 22 Jun 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/netflix-batch-compute-kueue/</guid>
      <description>&lt;h1 id=&#34;netflix-如何用-kueue-简化批计算一次百万级任务的换心手术&#34;&gt;Netflix 如何用 Kueue 简化批计算：一次百万级任务的换心手术&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;原文：&lt;a href=&#34;https://netflixtechblog.com/how-netflix-simplified-batch-compute-with-kueue-87860682629c&#34;&gt;How Netflix Simplified Batch Compute with Kueue&lt;/a&gt;
目标读者：维护过或正在维护自研任务调度/排队系统的工程师；以及所有纠结过「自研轮子 vs 换开源方案」的人。
阅读目标：搞清楚 Netflix 为什么敢把百万级批任务的排队调度层整体换成 Kueue、怎么换的、换完拿到了什么。重点不在 Kueue 的用法，在这次迁移的&lt;strong&gt;姿势&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;先把背景摊开cmb-和-titus&#34;&gt;〇、先把背景摊开：CMB 和 Titus&lt;/h2&gt;
&lt;p&gt;Netflix 的容器平台叫 Titus，跑批任务用的是自建方案 &lt;strong&gt;CMB（Compute Managed Batch）&lt;/strong&gt;，2018 年建的。CMB 负责的事：用户提交跑到结束的批工作负载，按租户层级排队、按优先级有序执行、按租户管理容量；真正跑任务的活交给 Titus。&lt;/p&gt;
&lt;p&gt;Titus 对 CMB 有两个关键能力：&lt;strong&gt;跨 cell 联邦&lt;/strong&gt;（一个 cell 就是一个 K8s 集群）和&lt;strong&gt;联邦容量预留&lt;/strong&gt;。所以 CMB 只需要对 Titus 的一个端点说话，不用关心底下集群的拓扑——这一层抽象是后面整次迁移能「无感」的地基。&lt;/p&gt;
&lt;h3 id=&#34;租户层级与两种容量&#34;&gt;租户层级与两种容量&lt;/h3&gt;
&lt;p&gt;CMB 的租户是一棵树，分两类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内部租户（Internal Tenant）&lt;/strong&gt;：只做组织，不接收任务，没有队列；子节点可以是内部或叶子租户。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;叶子租户（Leaf Tenant）&lt;/strong&gt;：能接收任务，挂队列，不能再有子节点。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;容量也分两种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;预留容量（Reserved）&lt;/strong&gt;：内部租户的预留在其子树内公平共享；叶子租户的预留则是独占切分，别的租户碰不了，保证吞吐。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;共享容量（Shared）&lt;/strong&gt;：Compute 团队维护一个全局池子，谁都可以 burst 进去。池子按租户公平共享——但注意，&lt;strong&gt;CMB 的公平只在准入（admission）那一刻生效&lt;/strong&gt;：它没有抢占，任务一旦放行就跑到结束，哪怕公平份额已经变了。这个语义缺口后面会被 Kueue 填上。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一为什么是-kueue自研轮子的折旧时刻&#34;&gt;一、为什么是 Kueue：自研轮子的折旧时刻&lt;/h2&gt;
&lt;p&gt;CMB 2018 年建的时候，开源界还没有像样的批计算方案。但 K8s 生态滚了几年之后，CMB 有的和想要的功能——公平共享、层级租户、容量管理、优先级队列——开源项目全都有了。反过来，CMB 想加新功能（比如抢占）越来越费劲，因为它离底层的 K8s 集群隔得太远。这就是自研系统的经典折旧曲线：&lt;strong&gt;当年它填补空白，现在它阻碍你吃到生态红利。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;团队评估了怎么把批抽象现代化，最后选了 Kueue，理由五条，第一条最值得琢磨：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Kueue 不替换 kube-scheduler。&lt;/strong&gt; 和 YuniKorn、Volcano 不同，Kueue 只管排队和准入，Pod 调度还是 kube-scheduler 干，所以能和 Titus 已有的调度画像（scheduling profiles）集成。换掉 Titus 的调度画像会让任务摆放（placement）碎片化，损害效率。这条的工程含义：选组件时，&lt;strong&gt;「它不管哪一段」和「它管哪一段」同样重要&lt;/strong&gt;——Kueue 把调度和排队拆开，只拿走排队这一段，接入面就小得多。&lt;/li&gt;
&lt;li&gt;社区采用势头和创新速度。&lt;/li&gt;
&lt;li&gt;支持异构硬件上的多租户配额管理。&lt;/li&gt;
&lt;li&gt;能操作 &lt;code&gt;v1.Pod&lt;/code&gt;、&lt;code&gt;batch/v1.Job&lt;/code&gt; 这类原生原语，也支持 RayJob / RayCluster 这类高层抽象，留好了扩展空间。&lt;/li&gt;
&lt;li&gt;自带团队本来想在 CMB 里自己造的功能：抢占、all-or-nothing 调度、拓扑感知调度。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二迁移netflix-batch-的三条军规&#34;&gt;二、迁移：Netflix Batch 的三条军规&lt;/h2&gt;
&lt;p&gt;这次把 CMB 工作负载迁到 Kueue 的项目，内部叫 &lt;strong&gt;Netflix Batch&lt;/strong&gt;。军规三条：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;对 CMB 终端用户零负担、完全透明。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;容器启动速率和整体最大吞吐不允许回退。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;用 Kueue 替换 CMB 的排队与调度。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;新旧提交流程的关键差别：排队和调度被下放给每个启用了 Kueue 的 Titus cell 里的 Kueue，Titus 联邦层通过一个&lt;strong&gt;自研 Kueue 路由器&lt;/strong&gt;把任务路由到 Kueue cell。注意这里没说换成 K8s 的通用调度入口——联邦这层壳还在，换的是壳里面的引擎。&lt;/p&gt;
&lt;p&gt;租户管理这边，对运维来说迁移简单到「在 UI 上给租户点一个按钮」，出问题也能一键回滚。按钮底下的映射关系是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内部租户 → &lt;strong&gt;Cohort&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;叶子租户 → &lt;strong&gt;ClusterQueue + LocalQueue&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;租户上的容量配置 → &lt;strong&gt;resource flavor + 名义配额（nominal quota）&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&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
    subgraph OLD[&amp;#34;迁移前 CMB&amp;#34;]
        U1[&amp;#34;用户与应用&amp;#34;] --&amp;gt;|&amp;#34;提交批任务&amp;#34;| CMBQ[&amp;#34;CMB 自研排队与调度&amp;lt;br/&amp;gt;租户层级加公平共享&amp;#34;]
        CMBQ --&amp;gt; FED[&amp;#34;Titus 联邦层&amp;#34;]
        FED --&amp;gt; CELL[&amp;#34;Titus cell 各 K8s 集群&amp;#34;]
    end
    subgraph NEW[&amp;#34;迁移后 Netflix Batch&amp;#34;]
        U2[&amp;#34;用户与应用 同一 API&amp;#34;] --&amp;gt;|&amp;#34;提交批任务 无感&amp;#34;| ROUTER[&amp;#34;自研 Kueue 路由器&amp;#34;]
        ROUTER --&amp;gt; KQ[&amp;#34;各 cell 内的 Kueue&amp;lt;br/&amp;gt;ClusterQueue 排队与准入&amp;#34;]
        KQ --&amp;gt; KS[&amp;#34;kube-scheduler 负责 Pod 摆放&amp;#34;]
    end
    OLD -.-&amp;gt;|&amp;#34;只换排队调度引擎&amp;lt;br/&amp;gt;API 与联邦层不动&amp;#34;| NEW
    style KQ fill:#fff3cd&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这张图是整篇文章的骨架：&lt;strong&gt;对上一寸不动，对下一寸不动，只把中间自研的那层换成开源组件。&lt;/strong&gt; 用户侧的 API、Titus 的联邦与调度画像都是旧资产，全保留了。&lt;/p&gt;
&lt;h3 id=&#34;三条经验教训全文最有信息量的部分&#34;&gt;三条经验教训（全文最有信息量的部分）&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先保 API 平价，先换底层组件。&lt;/strong&gt; 不给用户暴露新 API 面，而是把底下组件换掉作为第一步。原文的说法是 unstacking bets——把赌注拆开：不让「用户迁新 API」和「底层换引擎」两件事耦合成一次豪赌。项目风险降了，客户体验也没断。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;别把最复杂的客户留到最后。&lt;/strong&gt; 他们一上来就迁最大最复杂的那个客户。信心是先难后易建起来的，后面其他客户的迁移就顺了——结果整个生产迁移只花了 &lt;strong&gt;4 周&lt;/strong&gt;。多数迁移的直觉是「先易后难、小步快跑」，Netflix 反着来：最难的客户先过，因为它把所有边角情况一次性暴露完，剩下的全是子集。这和「先打最硬的仗」是一个道理——前提是第 1 条保证了输得起。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kueue 的默认参数顶不住 Netflix 的量。&lt;/strong&gt; 必须把 QPS、Burst、groupKindConcurrency 调得比默认高很多才能满足吞吐。这个坑是提前在仿 Titus 的开发环境跑负载测试趟出来的，不是生产上撞出来的。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三换完之后抢占式公平共享补上了语义缺口&#34;&gt;三、换完之后：抢占式公平共享补上了语义缺口&lt;/h2&gt;
&lt;p&gt;Kueue 已在生产全量铺开，管理着&lt;strong&gt;数百万级&lt;/strong&gt;批工作负载。前面说的 CMB「公平只在准入时生效、无抢占」的语义缺口，现在用**基于抢占的公平共享（Preemption-based Fair Sharing）**补上了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;预留容量没人用的时候可以借给其他租户，主人回来再抢回来——&lt;strong&gt;预留语义保住，闲置不再浪费&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;高优先级任务可以抢占低优先级任务——关键业务的周转时间变快，且不怕饿死。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;原文给的 ClusterQueue 抢占配置示例：&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;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;apiVersion&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;kueue.x-k8s.io/v1beta2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;kind&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;ClusterQueue&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;team-a-cq&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;preemption&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;reclaimWithinCohort&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;Any&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;withinClusterQueue&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;LowerPriority&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;code&gt;reclaimWithinCohort: Any&lt;/code&gt; 意思是：同 Cohort 里谁借了我的份额，我随时可以抢回来（不限优先级）；&lt;code&gt;withinClusterQueue: LowerPriority&lt;/code&gt; 意思是：队列内部只抢优先级比我低的。两条规则分别对应「租户之间」和「租户之内」两个维度的公平。&lt;/p&gt;
&lt;p&gt;效果：Compute 团队观察到&lt;strong&gt;平均资源利用率显著提升&lt;/strong&gt;（原文没给具体数字，只有这个定性结论）。另外这套经验正在外溢到其他内部团队，包括搭 K8s 原生训练基础设施的团队——批计算和 ML 训练的调度问题，本质上是同一个。&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
    subgraph COHORT[&amp;#34;Cohort 对应内部租户 子树&amp;#34;]
        CQA[&amp;#34;ClusterQueue 团队 A&amp;lt;br/&amp;gt;有预留配额&amp;#34;] 
        CQB[&amp;#34;ClusterQueue 团队 B&amp;lt;br/&amp;gt;有预留配额&amp;#34;]
    end
    CQA --&amp;gt;|&amp;#34;配额闲置时&amp;lt;br/&amp;gt;借给同 Cohort 成员&amp;#34;| CQB
    CQB --&amp;gt;|&amp;#34;主人回来 reclaimWithinCohort Any&amp;lt;br/&amp;gt;抢占借出去的资源&amp;#34;| CQA
    CQA --&amp;gt;|&amp;#34;队列内部&amp;lt;br/&amp;gt;高优先级抢占低优先级&amp;#34;| PRE[&amp;#34;withinClusterQueue LowerPriority&amp;#34;]
    PRE --&amp;gt; R[&amp;#34;预留语义不丢&amp;lt;br/&amp;gt;闲置容量不浪费&amp;lt;br/&amp;gt;利用率上升&amp;#34;]
    style R fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;四工程实践联想&#34;&gt;四、工程实践联想&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;迁移的黄金姿势：API 不动，引擎换心。&lt;/strong&gt; Netflix 把「用户迁新 API」和「底层换引擎」拆成两个独立的赌注，先只赌后者。回到我们自己的项目：存储换引擎、队列换实现、框架换内核，第一件事永远是把门面焊死。门面动了，你就同时在做两个项目。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;先迁最难的客户，但要先有回滚按钮。&lt;/strong&gt; 「最难的先来」之所以成立，是因为他们有 UI 一键切换加一键回滚托底。没有廉价回滚能力之前，先难后易是鲁莽，不是勇气。顺序是：先造回滚，再挑硬骨头。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调度与排队分层，是值得抄的架构决策。&lt;/strong&gt; Kueue 只做排队准入，Pod 摆放留给 kube-scheduler。这个切分让 Netflix 保住了 Titus 多年的调度调优积累。评估任何「大而全」的调度系统（YuniKorn、Volcano 都是全替换派）时，先问：它接管的那一层，我原有的积累值多少钱？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;开源组件的默认参数是按「示例集群」调的，不是按你的量。&lt;/strong&gt; QPS、Burst、并发数这类旋钮，上生产前必须在仿真环境里压出来。Netflix 的负载测试环境「模仿 Titus」，这笔钱花在了迁移前，不是事故后。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自研系统要定期做折旧审计。&lt;/strong&gt; CMB 不是烂系统，是时代变了：它独有的功能被生态追平，它缺失的功能（抢占）自研成本却越来越高。每两年问一次「这轮子如果今天立项，我还会自己造吗」，答案变了就该动手了。顺带一提，做 AI 训练平台的可以直接抄作业：批任务的排队、配额、抢占、多租户公平，和 GPU 训练集群的调度问题是同构的，Kueue 支持 RayJob/RayCluster 就是 Netflix 明说的扩展点。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;本篇小结&#34;&gt;本篇小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;背景&lt;/strong&gt;：CMB（2018 年自研托管批处理）跑在 Titus 上，租户分内部/叶子两级，容量分预留/共享两种；共享容量只在准入时公平，无抢占。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动机&lt;/strong&gt;：K8s 生态追平了 CMB 的功能，而自研新功能（如抢占）成本越来越高；Kueue 不替换 kube-scheduler、支持异构配额和 Pod/Job/RayJob，且有抢占、all-or-nothing、拓扑感知调度等原生能力。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;迁移（Netflix Batch）&lt;/strong&gt;：用户零感知、吞吐不回退、排队调度换 Kueue；内部租户映射为 Cohort，叶子租户映射为 ClusterQueue + LocalQueue，容量配置映射为 resource flavor + nominal quota；UI 一键切换、可回滚。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;经验&lt;/strong&gt;：API 平价先行拆分赌注；最大最复杂的客户最先迁，生产迁移仅 4 周；Kueue 需调高 QPS/Burst/groupKindConcurrency，靠仿真环境负载测试提前摸底。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;现状&lt;/strong&gt;：生产全量、数百万批工作负载；基于抢占的公平共享（&lt;code&gt;reclaimWithinCohort: Any&lt;/code&gt; + &lt;code&gt;withinClusterQueue: LowerPriority&lt;/code&gt;）让预留容量可出借可回收，平均资源利用率显著提升。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一句话收束：这篇文章表面讲 Kueue，实际讲的是一个成熟组织怎么给自研系统办一场体面的退休——不办追悼会（大 rewrite），办的是工作交接（API 不变、逐层替换、最难的先交）。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;参考资料&#34;&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://netflixtechblog.com/how-netflix-simplified-batch-compute-with-kueue-87860682629c&#34;&gt;How Netflix Simplified Batch Compute with Kueue&lt;/a&gt;（原文，作者 Alvin Bao、Alex Petrov、Jennifer Lai、Aidan Sherr、Samartha Chandrashekar，2026-06-22）&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://kueue.sigs.k8s.io/&#34;&gt;Kueue 官方文档&lt;/a&gt;（K8s 原生的批任务排队系统，原文提到的 Cohort、ClusterQueue、LocalQueue、resource flavor、抢占语义的权威定义）&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://yunikorn.apache.org/&#34;&gt;YuniKorn&lt;/a&gt; 与 &lt;a href=&#34;https://volcano.sh/&#34;&gt;Volcano&lt;/a&gt;（原文对比中提到的全替换派调度器）&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://netflix.github.io/titus/&#34;&gt;Netflix Titus&lt;/a&gt;（Netflix 容器平台，CMB 与 Netflix Batch 的运行底座）&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    <item>
      <title>Netflix 的因果推断 Agent 工作流：没有标准答案时，怎么审计一个 AI</title>
      <link>https://ftmao.com/blog/posts/netflix-agentic-workflow-causal-inference/</link>
      <pubDate>Mon, 08 Jun 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/netflix-agentic-workflow-causal-inference/</guid>
      <description>&lt;h1 id=&#34;netflix-的因果推断-agent-工作流没有标准答案时怎么审计一个-ai&#34;&gt;Netflix 的因果推断 Agent 工作流：没有标准答案时，怎么审计一个 AI&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;原文：&lt;a href=&#34;https://netflixtechblog.com/a-human-augmenting-agentic-workflow-for-causal-inference-4623f0a9c5af&#34;&gt;A Human-Augmenting Agentic Workflow for Causal Inference&lt;/a&gt;
目标读者：在做 AI Agent 落地的工程师、数据科学家，以及所有想过「这个 agent 给的数到底能不能信」的人。
阅读目标：搞清楚 Netflix 怎么把一个「没有标准答案、错不自知」的分析任务工程化成 agent 工作流，以及这套思路对普通后端和 AI 工程的迁移价值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;先拆一个直觉agent-给了答案你敢用吗&#34;&gt;〇、先拆一个直觉：agent 给了答案，你敢用吗&lt;/h2&gt;
&lt;p&gt;先想一个场景：你让一个数据分析 agent 回答「看某部爆款剧对会员长期留存有没有因果效应」。它查了数、跑了回归、自信满满地返回一个数。你该信多少？它有没有处理掉那些隐蔽的偏差——比如把狂热粉丝当成普通观众？没有深厚的方法论功底，你甚至看不出它错没错。&lt;/p&gt;
&lt;p&gt;这就是观测性因果推断（Observational Causal Inference，OCI）的特殊处境。一般的 agent 评估套路是拿输出和 ground truth 程序化对比——但&lt;strong&gt;观测数据里根本没有 ground truth&lt;/strong&gt;。不是数据不够，是这个问题在本质上没有标准答案可比对。Netflix 这篇文章的核心贡献，就是正面回答了「评估对象不存在时怎么办」：&lt;strong&gt;评估不了结果，就审计过程。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;整个工作流立在他们 AI 时代之前就建好的 OCI 工具箱上，指导思想是「目标试验模拟」（target trial emulation）：拿到一个时点型因果问题，先问「回答这个问题的理想 A/B 实验长什么样？」这个实验可能太贵、太慢甚至不可行，但这个思想实验能逼你把假设钉死。&lt;/p&gt;
&lt;h2 id=&#34;一四个设计诊断工具箱里真正的硬货&#34;&gt;一、四个设计诊断：工具箱里真正的硬货&lt;/h2&gt;
&lt;p&gt;为了让「目标试验」这个类比可操作，工具箱内置了四个设计诊断，专门检查处理组和对照组的比较是否公平：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;协变量平衡&lt;/strong&gt;：加权之后，处理组和对照组在处理前协变量上的标准化均差应小于 0.2。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重叠性（overlap）&lt;/strong&gt;：接受处理的概率（倾向得分）应该落在 0.1 到 0.9 之间——太接近 0 或 1 的样本，处理与否基本是注定的，不存在「可比的对照」。&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;/ul&gt;
&lt;p&gt;这四条没有什么 AI 含量，全是统计学的老手艺。但这正是文章的潜台词：&lt;strong&gt;agent 工作流的可靠性不来自模型更聪明，来自把纪律焊死在流程里。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;二三个角色principalactorcritic&#34;&gt;二、三个角色：Principal、Actor、Critic&lt;/h2&gt;
&lt;p&gt;工作流有三个角色，后两个由软件扮演，跑成一个 actor-critic 循环：&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
    P[&amp;#34;Principal 人类&amp;lt;br/&amp;gt;写初始分析计划&amp;lt;br/&amp;gt;交代混杂因素与业务背景&amp;lt;br/&amp;gt;指定可用工具和数据集&amp;#34;] --&amp;gt; A[&amp;#34;Actor 软件&amp;lt;br/&amp;gt;把计划细化为分析 spec&amp;lt;br/&amp;gt;只用被允许的工具执行&amp;lt;br/&amp;gt;跑核心分析和四个设计诊断&amp;#34;]
    A --&amp;gt; ART[&amp;#34;工件&amp;lt;br/&amp;gt;plan 和 spec&amp;lt;br/&amp;gt;图表 报告 可复现 notebook&amp;#34;]
    A --&amp;gt; C[&amp;#34;Critic 软件&amp;lt;br/&amp;gt;找计划盲区 查计划与执行的一致性&amp;lt;br/&amp;gt;给结果定可信度等级&amp;lt;br/&amp;gt;对照理想 RCT 找差距&amp;#34;]
    C --&amp;gt;|&amp;#34;诊断失败或发现盲区&amp;lt;br/&amp;gt;触发补救或下一轮迭代&amp;#34;| A
    ART --&amp;gt;|&amp;#34;过程审计&amp;lt;br/&amp;gt;人类可下载重跑&amp;#34;| P
    style P fill:#fff3cd
    style ART fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;Critic 的职责清单值得细看，它不只是「再看看对不对」：检查 Principal 计划里没提到的混杂因素；核对计划、spec、实际执行三者是否对齐；看完诊断后给结果定可信度等级；明确说明估计量是否偏离了 ATE（比如因为倾向得分裁剪）；把实际分析和理想 RCT 做对照；还要至少给出一种替代测量方案（比如激励式 RCT）。&lt;/p&gt;
&lt;p&gt;人类这边的配套是一个模板化 notebook，底层用经过验证的（非 agent 的）OCI 工具箱，效应估计用 doubly robust learning。agent 产出的报告有版本控制，执行过的 notebook 上传到文件存储，Principal 可以下载下来亲自重跑。&lt;strong&gt;人不用亲自干苦活，但每一步都留了可以让人验算的凭据&lt;/strong&gt;——这就是标题里 &amp;ldquo;human-augmenting&amp;rdquo; 的实际含义：不是替代人，是把人的精力从重复劳动（反复检查协变量平衡、跑敏感性分析、管理迭代版本）挪到只有人能做的事上（框定问题、审视假设、判断结果）。&lt;/p&gt;
&lt;h2 id=&#34;三案例新娱乐类型的留存影响估计值缩水到四分之一&#34;&gt;三、案例：新娱乐类型的留存影响，估计值缩水到四分之一&lt;/h2&gt;
&lt;p&gt;Netflix 近年加了不少视频之外的新娱乐类型，自然要问：这些东西对会员满意度和续订有什么影响？他们拿其中一个类型（文中叫 Type X）做了对照实验：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;处理&lt;/strong&gt;：Type X 的使用天数&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结局&lt;/strong&gt;：两个月留存&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;潜在混杂&lt;/strong&gt;：包括处理前的 Type X 使用天数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;基线&lt;/strong&gt;：把这份分析计划不加任何脚手架直接喂给 Claude Sonnet 4.6。模型选了一个说得过去的策略——用留存对使用天数加控制变量做线性回归——输出干净漂亮。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;paved path（铺好的路）&lt;/strong&gt;：同一份计划、同一个模型，走他们的工具箱加 agentic 工作流，产出的估计值&lt;strong&gt;只有基线的 25%&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;差异的根源是&lt;strong&gt;早期采用者偏差&lt;/strong&gt;（early adopter bias）：新功能的第一批用户和一般人群系统性不同——他们可能是 Netflix 重度用户，或者是相关 IP 的死忠粉。这个偏差在数据里表现为重叠性崩塌：绝大多数观测的 Type X 参与概率估值都极小（功能还太新）。critic 在报告里抓住了这个问题，还标记了安慰剂检验失败：早期采用者在&lt;strong&gt;处理发生之前&lt;/strong&gt;测量的重要协变量上就和非采用者有显著差异——两组人本来就不是一路人。&lt;/p&gt;
&lt;p&gt;补救手段来自工作流内置的 playbook：重叠性差就用 Crump 式裁剪，估计因果效应前先把倾向得分落在 0.1 到 0.9 之外的样本裁掉。代价是结论的适用范围缩水——估计的 ATE 只适用于「参与与否不是注定」的那部分人群，critic 必须在报告里把这个注意事项写明。但换来的是可信度的质变：裁剪后的估计只依赖「处理近似随机」的子人群，和目标试验的精神一致；而基线那个数靠的是对低概率人群的外推，外推出来的数字没有扎实数据支撑，还很可能被早期采用者偏差污染。&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
    PLAN[&amp;#34;同一份分析计划&amp;lt;br/&amp;gt;同一个 Sonnet 4.6&amp;#34;] --&amp;gt; BASE[&amp;#34;裸调 one-shot&amp;lt;br/&amp;gt;线性回归 不加诊断&amp;#34;]
    PLAN --&amp;gt; PP[&amp;#34;paved path&amp;lt;br/&amp;gt;脚手架加诊断加 critic&amp;#34;]
    BASE --&amp;gt; B1[&amp;#34;结果干净漂亮&amp;lt;br/&amp;gt;但依赖外推&amp;lt;br/&amp;gt;被早期采用者偏差污染&amp;#34;]
    PP --&amp;gt; D1[&amp;#34;重叠性诊断失败&amp;lt;br/&amp;gt;安慰剂检验失败&amp;#34;]
    D1 --&amp;gt; D2[&amp;#34;Crump 裁剪&amp;lt;br/&amp;gt;只保留倾向得分 0.1 到 0.9 的人群&amp;#34;]
    D2 --&amp;gt; B2[&amp;#34;估计值只有基线的 25%&amp;lt;br/&amp;gt;适用范围缩水但可信&amp;#34;]
    style B1 fill:#ffcccc
    style B2 fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这个案例最扎心的地方在于：&lt;strong&gt;基线的产出是「defensible」的&lt;/strong&gt;——策略挑不出大毛病，呈现也很专业。如果你不是 OCI 从业者，你大概率就收下了。&lt;/p&gt;
&lt;h2 id=&#34;四编排后续分析agent-真正省力的部分&#34;&gt;四、编排后续分析：agent 真正省力的部分&lt;/h2&gt;
&lt;p&gt;分析做完还有两类自然后续，都是「同一分析跑 N 个变体」的脏活：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;敏感性分析&lt;/strong&gt;：换不同的裁剪阈值重做分析。他们让 agent 把裁剪界从 0 到 1（不裁剪）一路变到 0.15 到 0.85，结论是重叠人群上的 ATE 估计在 0.005 到 0.995 的界内对阈值选择稳健。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;时间序列&lt;/strong&gt;：把同一分析在十个不同的日期分区上重跑，看效应随时间的演变。这里有个微妙的坑：跨时间比较必须协调各次分析的人群——如果某次为了可信度裁掉了一批用户，其他几次也得裁同一批。跑出来的结果显示季节性：冬季日期的处理效应强于夏季。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这两类活的技术含量为零，出错概率不低（参数改一个忘一个、人群没对齐），正是 agent 该吃的苦。&lt;/p&gt;
&lt;h2 id=&#34;五开源与评测critic-真的能区分好坏估计&#34;&gt;五、开源与评测：critic 真的能区分好坏估计&lt;/h2&gt;
&lt;p&gt;Netflix 开源了一个独立版 &lt;code&gt;oci-agent&lt;/code&gt;，附带两个基于 ACIC 2016（大西洋因果推断会议竞赛）公开数据集的评测，以及一个只用开源软件（EconML）的轻量因果机器学习 notebook。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;评测一&lt;/strong&gt;：对 77 个数据生成过程（DGP）各随机抽 3 个数据集，共 231 个估计，让 critic 依据诊断把它们分成 satisfactory 和 unsatisfactory。结果：192 个 satisfactory、39 个 unsatisfactory。对照竞赛里 44 个参赛方法，他们的统计方法本身就有竞争力——RMSE 足够低，95% 置信区间的覆盖率约 95%（校准良好）。但重点在这：&lt;strong&gt;satisfactory 那组的 RMSE 显著更低、区间校准更好，unsatisfactory 那组明显更差&lt;/strong&gt;——诊断套件加 critic 确实能把可信的估计和不可信的估计分开。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;评测二&lt;/strong&gt;：同一份分析计划，带脚手架 vs 裸调（one-shot prompting）。随机抽十个 ACIC 数据集，带脚手架的 LLM 复原了九个的真值，且估计与真值高度相关；裸调 Sonnet 4.6 则一致地错，和真值基本不相关。&lt;/p&gt;
&lt;p&gt;作者也坦白了局限：合成数据集压测不了 agent 对真实业务场景的语义理解。但仓库演示了两条核心原则——&lt;strong&gt;用足量脚手架让 agent 按设计遵守最佳实践；要求可检查的工件，让人类能审计 agent 的过程而不只是结果。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;工程实践联想&#34;&gt;工程实践联想&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;没有 ground truth 的领域，把 eval 拆成「结果评估」和「过程审计」两层。&lt;/strong&gt; 推荐、增长、风控策略、A/B 之外的分析场景，大量任务都没有可比对的标准答案。Netflix 的答案是：结果层用代理指标（ACIC 合成数据上的 RMSE），过程层靠工件留痕（plan、spec、notebook、版本化报告）让人审。我们写 agent 时也该问一句：这个 agent 的产出，三个月后另一个工程师能不能重建它的推理链？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;脚手架比模型重要。&lt;/strong&gt; 同一个 Sonnet 4.6，裸调和走 paved path 的估计差了 4 倍——而且错的是看起来更漂亮的那个。「换更强的模型」解决的是能力问题，「模板、诊断、playbook、强制的 critic 环节」解决的是纪律问题。分析类任务里，纪律缺口比能力缺口大得多。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;actor-critic 结构的关键是 critic 有独立的、可操作的责任清单。&lt;/strong&gt; Netflix 的 critic 不是泛泛地「你再检查一遍」，而是有具体义务：找计划盲区、核对三层一致性、定可信度等级、对照理想 RCT、给替代方案。critic 的职责越具体，漏检越少。这和 code review 清单化是同一个道理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;诊断失败要有 playbook，而不是一句「请注意」。&lt;/strong&gt; 重叠性差对应 Crump 裁剪，裁剪导致估计量偏离 ATE 就要求 critic 在报告里标注适用范围。诊断、补救、注意事项三条线绑在一起，流程才是闭环的。我们系统里的告警如果只报不处理，和人肉运维没区别。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;把人放在「审」的位置，而不是「干」的位置。&lt;/strong&gt; 人的精力留给框定问题、审视假设、判断结果；重复执行、版本管理、批量变体交给 agent。这个分工原则对任何「专业判断密集 + 重复劳动密集」的工作流都成立，不止因果推断。&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;：观测性因果推断没有 ground truth，agent 给的因果答案无法拿结果对错来评估，错了也很难被发现。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解法&lt;/strong&gt;：Principal-Actor-Critic 三角色工作流。人类写计划和背景，actor 按模板执行分析并跑四个设计诊断（协变量平衡 SMD ＜ 0.2、倾向得分落在 0.1 到 0.9、安慰剂结局、敏感性分析），critic 有明确清单地挑毛病并定可信度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;人的位置&lt;/strong&gt;：human-augmenting 的落点是工件——版本化报告、可下载重跑的 notebook——用过程审计代替结果评估。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;案例&lt;/strong&gt;：新娱乐类型的留存效应，裸调 Sonnet 4.6 给出漂亮但依赖外推的回归结果；走工作流后诊断抓住早期采用者偏差，Crump 裁剪后估计值只有基线的 25%。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;评测&lt;/strong&gt;：ACIC 2016 上 231 个估计，critic 判定的 192 个 satisfactory 估计质量显著优于 39 个 unsatisfactory；带脚手架 10 个数据集复原 9 个真值，裸调全军覆没。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;私货&lt;/strong&gt;：这篇文章真正的主题不是因果推断，是「如何工程化地使用一个你信不过的聪明组件」——脚手架定纪律、critic 做对抗、工件留凭据、人类审过程。这套四件套放在任何高风险 agent 场景都成立。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;参考资料&#34;&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;原文：Winston Chou 等，&lt;a href=&#34;https://netflixtechblog.com/a-human-augmenting-agentic-workflow-for-causal-inference-4623f0a9c5af&#34;&gt;A Human-Augmenting Agentic Workflow for Causal Inference&lt;/a&gt;，Netflix Technology Blog，2026-06-08。&lt;/li&gt;
&lt;li&gt;开源仓库：Netflix 的独立版 &lt;code&gt;oci-agent&lt;/code&gt;（原文有链接，基于 EconML）。&lt;/li&gt;
&lt;li&gt;2016 Atlantic Causal Inference Conference（ACIC）数据分析竞赛数据集（评测所用的 77 个 DGP 来源）。&lt;/li&gt;
&lt;li&gt;Crump 等人的倾向得分裁剪方法（重叠性诊断失败的补救手段，原文引用的 playbook 条目）。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    <item>
      <title>Netflix 实时服务拓扑图笔记：把「谁依赖谁」从考古变成活地图</title>
      <link>https://ftmao.com/blog/posts/netflix-service-topology-map/</link>
      <pubDate>Fri, 29 May 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/netflix-service-topology-map/</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/from-silos-to-service-topology-why-netflix-built-a-real-time-service-map-0165ba13a7bc&#34;&gt;From Silos to Service Topology: Why Netflix Built a Real-Time Service Map&lt;/a&gt;（Netflix Tech Blog，2026-05-29，作者 Parth Jain 等）
目标读者：维护过微服务系统、半夜被告警叫醒过、对着一堆监控面板猜「到底谁挂了」的后端工程师。
阅读目标：搞清楚 Netflix 为什么认为「依赖关系」值得单独立项做一套系统，三路数据源怎么互补，以及那套「网络中间人解析」管线在解决什么问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一问题信号很多地图没有&#34;&gt;一、问题：信号很多，地图没有&lt;/h2&gt;
&lt;p&gt;原文开场是经典的凌晨三点剧本：工程师被 page 醒，某个关键服务错误率升高，用户观影体验受损。在几千个微服务支撑的系统里，这时候要回答的问题其实非常朴素，但谁也答不快。&lt;/p&gt;
&lt;p&gt;Netflix 做了一件值得学的事：&lt;strong&gt;他们没有拍脑袋立项，而是分析了四年间工程师提交的数千条支持请求&lt;/strong&gt;，发现高频问题惊人地一致：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我的上游和下游依赖是谁？&lt;/li&gt;
&lt;li&gt;这个故障是我自己的问题，还是我依赖的某个服务坏了？&lt;/li&gt;
&lt;li&gt;我下线这台服务做维护，会影响谁？该通知哪些团队？&lt;/li&gt;
&lt;li&gt;为什么这个服务在我的指标里显示为「Unknown」？&lt;/li&gt;
&lt;li&gt;最近我的调用链路上有什么变化，能解释这个行为吗？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;全是依赖问题。而传统可观测性三件套各自只给碎片：指标告诉你症状，日志告诉你单个服务在干嘛，追踪告诉你单次请求怎么流的——&lt;strong&gt;没有一样东西能告诉你稳态下整张依赖网长什么样&lt;/strong&gt;。工程师得在凌晨三点用脑子把多个工具的信息缝起来，慢、易错、压力大。&lt;/p&gt;
&lt;p&gt;这里有个判断很准：缺的不是信号，是&lt;strong&gt;关系&lt;/strong&gt;。信号再多，回答不了「谁连着谁」这种结构性问题。&lt;/p&gt;
&lt;h2 id=&#34;二踩过的坑五条教训&#34;&gt;二、踩过的坑：五条教训&lt;/h2&gt;
&lt;p&gt;Netflix 不是第一次干这件事。原文坦承他们试过外部图数据库、商业平台、各种存储和内部原型，每轮迭代换来一条教训：&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;：能装几千节点的方案，到 Netflix 的服务数量和流量规模会撞墙。&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;li&gt;&lt;strong&gt;单一视角讲不全&lt;/strong&gt;：网络连接数据缺应用上下文，应用指标只覆盖做了埋点的服务，必须多源合并。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这五条几乎条条是钱烧出来的，尤其是第四条——&lt;strong&gt;一个会撒谎的地图比没有地图更害人&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id=&#34;三核心设计三个真相来源三张物理隔离的图&#34;&gt;三、核心设计：三个真相来源，三张物理隔离的图&lt;/h2&gt;
&lt;p&gt;这是全文最有分量的一个架构决策。Netflix 的结论是：&lt;strong&gt;没有任何单一数据源能讲完整的依赖故事&lt;/strong&gt;，所以他们用三路互补的数据各自建一张图：&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
    subgraph SRC[&amp;#34;三路数据源&amp;#34;]
        EB[&amp;#34;eBPF 网络流&amp;lt;br/&amp;gt;内核态抓真实连接&amp;#34;]
        IPC[&amp;#34;IPC 指标&amp;lt;br/&amp;gt;应用埋点 gRPC GraphQL REST&amp;#34;]
        TR[&amp;#34;端到端追踪&amp;lt;br/&amp;gt;采样后的真实请求路径&amp;#34;]
    end
    subgraph G[&amp;#34;三张物理隔离的图&amp;#34;]
        G1[&amp;#34;网络图&amp;lt;br/&amp;gt;图数据库分区一&amp;lt;br/&amp;gt;覆盖全 但无应用语义&amp;#34;]
        G2[&amp;#34;应用图&amp;lt;br/&amp;gt;图数据库分区二&amp;lt;br/&amp;gt;有端点细节 但只覆盖已埋点服务&amp;#34;]
        G3[&amp;#34;请求图&amp;lt;br/&amp;gt;列式存储&amp;lt;br/&amp;gt;真实行为 但有采样盲区&amp;#34;]
    end
    EB --&amp;gt; G1
    IPC --&amp;gt; G2
    TR --&amp;gt; G3
    G1 --&amp;gt; U[&amp;#34;统一视图&amp;lt;br/&amp;gt;并行查询三路 合并节点与边&amp;lt;br/&amp;gt;亚秒级返回&amp;#34;]
    G2 --&amp;gt; U
    G3 --&amp;gt; U
    style U fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;三路的价值与短板正好互补：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;eBPF 网络流（网络层）&lt;/strong&gt;：在内核层抓连接记录，&lt;strong&gt;不管应用有没有埋点，每个服务都在图上&lt;/strong&gt;——这是完整性的地基。短板是没有应用上下文：你知道 A 连了 B 的 IP 和协议，但不知道调的是哪个 API 端点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IPC 指标（应用层）&lt;/strong&gt;：埋点服务发出的进程间通信指标，能看到具体端点、错误率、延迟分布、协议细节。短板是&lt;strong&gt;只覆盖做了埋点的服务&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;端到端追踪（请求层）&lt;/strong&gt;：不是「A 可以调 B」，而是「A 在处理这个具体请求时确实调了 B」，能捕获条件分支和特性开关带来的真实运行时行为。短板是&lt;strong&gt;采样&lt;/strong&gt;——冷门代码路径在聚合视图里可能看不见。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;工程实现上有个细节值得注意：三张图&lt;strong&gt;物理隔离&lt;/strong&gt;——网络层和 IPC 层各在图数据库的一个分区，追踪层用列式存储（为分析型查询优化）。隔离换来两个好处：各层独立演进，查询时可以三路并行、最后合并结果，即使三层全合并也能做到亚秒返回。&lt;/p&gt;
&lt;p&gt;这个设计翻译过来就是一句话：&lt;strong&gt;完整性靠网络流兜底，语义靠 IPC 补齐，行为真实性靠追踪验证&lt;/strong&gt;。任何单源方案都在这三者里缺一角。&lt;/p&gt;
&lt;h2 id=&#34;四数据管线三级聚合与中间人解析&#34;&gt;四、数据管线：三级聚合与「中间人解析」&lt;/h2&gt;
&lt;p&gt;架构层面，流日志从 Kafka 到可查的图要走这样一条管线：&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
    K[&amp;#34;多区域 Kafka&amp;lt;br/&amp;gt;持续消费流日志&amp;#34;] --&amp;gt; P[&amp;#34;Pekko Streams&amp;lt;br/&amp;gt;分布式处理 自动分区&amp;lt;br/&amp;gt;天然背压&amp;#34;]
    P --&amp;gt; S1[&amp;#34;第一级 初始聚合&amp;lt;br/&amp;gt;从 Kafka 进来的原始流&amp;#34;]
    S1 --&amp;gt; S2[&amp;#34;第二级 中间人解析&amp;lt;br/&amp;gt;识别 LB NAT 网关 代理&amp;lt;br/&amp;gt;把两跳拼回一跳&amp;#34;]
    S2 --&amp;gt; S3[&amp;#34;第三级 最终聚合&amp;lt;br/&amp;gt;叠加健康状态&amp;#34;]
    S3 --&amp;gt; DB[&amp;#34;图数据库&amp;lt;br/&amp;gt;建在分布式 KV 之上&amp;lt;br/&amp;gt;高吞吐多跳遍历&amp;#34;]
    DB --&amp;gt; API[&amp;#34;gRPC API&amp;lt;br/&amp;gt;多跳遍历 按层过滤&amp;lt;br/&amp;gt;分页 亚秒响应&amp;#34;]
    style S2 fill:#fff3cd&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;三个点值得展开：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;处理框架用 Apache Pekko Streams&lt;/strong&gt;（Akka 的 fork），看重的是分布式容错、自动把负载分区到 Auto Scaling Group、以及响应式流自带的背压处理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;三级聚合里最聪明的是第二级&lt;/strong&gt;。网络流日志记录的是逐跳的物理连接：&lt;code&gt;App A → 负载均衡器 → App B&lt;/code&gt;，或者经过 NAT 网关。工程师要的不是这个，是 &lt;code&gt;App A → App B&lt;/code&gt; 这条应用级直连边。第二级负责识别出负载均衡器、NAT 网关、API 网关、代理这些「网络中间人」，把它们的入向流和出向流拼起来，还原出真正的应用到应用路径。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分三级还有个副作用：防热点&lt;/strong&gt;。当某些应用或中间人的流量是其他节点的上百倍时，把聚合分散到多个阶段执行，热点不会压垮单点。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;查询侧是 gRPC API：多跳遍历、按可用性等级（Tier 0、Tier 1 之类）和业务域过滤、大结果集分页、亚秒响应。UI 和 API 并重——&lt;strong&gt;工程师用界面排查，自动化系统（韧性框架、爆炸半径计算器、事故响应机器人）走 API&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id=&#34;五工程师拿到了什么&#34;&gt;五、工程师拿到了什么&lt;/h2&gt;
&lt;p&gt;系统落地后的能力清单，按原文压缩：&lt;/p&gt;
&lt;ul&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;li&gt;&lt;strong&gt;算爆炸半径&lt;/strong&gt;：下线维护或大改之前，先看清楚影响谁、通知谁、盯什么。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;叠健康状态&lt;/strong&gt;：拓扑上直接叠加哪些服务正在出问题，快速判断「我看到的故障是不是从别处传过来的」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;程序化查询&lt;/strong&gt;：Platform Modernization 团队用它验证直播关键服务的整条依赖链都打了正确的可用性等级标记——拓扑成了合规校验的数据源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;时间旅行&lt;/strong&gt;：查历史上某个时间点的拓扑长什么样。实现上没有每个时间片存一份（存储会爆炸），而是用&lt;strong&gt;各层专用的聚合器跨时间窗口累积&lt;/strong&gt;，需要历史视图时再重建。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后这条「时间旅行」是我觉得最有味道的：事故排查的黄金问题「&lt;strong&gt;故障开始前，我的依赖图变了什么&lt;/strong&gt;」终于被变成一条查询，而不是靠翻部署记录和记忆。&lt;/p&gt;
&lt;h2 id=&#34;六活的地图和下一步&#34;&gt;六、活的地图，和下一步&lt;/h2&gt;
&lt;p&gt;Netflix 反复强调「living map」这个词：新调用关系出现就近实时上图，调用停止的边会从图里淡出，部署改变行为就改变图。关键承诺是——&lt;strong&gt;工程师可以相信图上画的就是现实，而不是某个人对架构的想象&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;路线图上的三件事：&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;：更多端点级细节、协议信息、网络路径信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动根因分析&lt;/strong&gt;：一个智能体持续爬拓扑图、跨依赖关联故障、理解历史模式、自动给出疑似根因——&lt;strong&gt;拓扑图是这类智能自动化的知识图谱地基&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第三条值得划线：他们不是在说「接个 LLM 上去问答」，而是在说「先把依赖关系建成结构化、实时、可查询的图，智能体才有爬的地面」。这个顺序是对的。&lt;/p&gt;
&lt;h2 id=&#34;七工程实践联想&#34;&gt;七、工程实践联想&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;立项之前先做求助记录考古&lt;/strong&gt;。Netflix 拿四年的支持工单说话，高频问题清单就是需求文档。多数内部平台立项靠的是平台团队自己的痛感，而不是用户的——「我觉得你需要」和「数据说你在问」做出来的东西，采纳率天差地别。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「信号够多」不等于「可观测」&lt;/strong&gt;。指标、日志、追踪三件套回答的都是单点或单次的问题，依赖关系这种结构性知识需要单独建模、单独存储。如果你的团队排查故障还在靠「老张知道这条链路」，那就是缺这一层。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多源互补是处理「讲不全」问题的标准答案&lt;/strong&gt;。任何单一采集手段都有盲区：埋点有覆盖率问题，内核抓包有语义问题，采样有冷门路径问题。与其追求一个完美的源，不如让几个不完美的源互相兜底——这个思路对做 AI Agent 的上下文采集同样成立。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「中间人解析」是被低估的脏活&lt;/strong&gt;。基础设施里到处是 LB、网关、代理，原始数据永远带着这些噪音。谁能把物理路径还原成逻辑路径，谁的数据才可行动。做链路分析、做 RAG 的数据清洗，都会遇到同构的问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;图建好了，Agent 才有事可做&lt;/strong&gt;。Netflix 把自动根因分析放在路线图的末尾而不是开头，因为智能体的前提是有一张实时、准确、可编程查询的图。这对「AI + 运维」的喧嚣是一盆冷水：先修地基，再谈智能。&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;：微服务规模下，凌晨三点的工程师回答不了「谁依赖谁、爆炸半径多大、故障源在哪」；四年数千条工单证明这是最痛的缺口。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方案&lt;/strong&gt;：三路数据源各建一图、物理隔离、并行查询后合并——eBPF 网络流保完整性，IPC 指标保应用语义，分布式追踪保真实行为。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;管线&lt;/strong&gt;：多区域 Kafka → Pekko Streams → 三级聚合（核心是第二级的网络中间人解析，把 LB/NAT/代理的两跳还原成应用级直连边）→ 图数据库（建在分布式 KV 上）→ 亚秒级 gRPC API。&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;li&gt;&lt;strong&gt;预告&lt;/strong&gt;：这是系列第一篇，下一篇讲规模化落地的工程硬题——Kafka 消费延迟、GC 停顿、响应式流调试、热点治理。&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/from-silos-to-service-topology-why-netflix-built-a-real-time-service-map-0165ba13a7bc&#34;&gt;From Silos to Service Topology: Why Netflix Built a Real-Time Service Map&lt;/a&gt;（Netflix Technology Blog，2026-05-29，原文）&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://ebpf.io/&#34;&gt;eBPF 官方文档&lt;/a&gt;（内核态可编程观测技术，网络层数据源的基础）&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://pekko.apache.org/&#34;&gt;Apache Pekko&lt;/a&gt;（Akka 的开源 fork，文中分布式流处理框架）&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://opentelemetry.io/docs/concepts/signals/traces/&#34;&gt;OpenTelemetry Tracing&lt;/a&gt;（分布式追踪的通用概念背景，文中请求层数据源对应物）&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    <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>
    <item>
      <title>Netflix 用 JDK Vector API 优化推荐系统：一次教科书级的性能工程实录</title>
      <link>https://ftmao.com/blog/posts/netflix-jdk-vector-api-recommendation/</link>
      <pubDate>Tue, 03 Mar 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/netflix-jdk-vector-api-recommendation/</guid>
      <description>&lt;h1 id=&#34;netflix-用-jdk-vector-api-优化推荐系统一次教科书级的性能工程实录&#34;&gt;Netflix 用 JDK Vector API 优化推荐系统：一次教科书级的性能工程实录&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;原文：&lt;a href=&#34;https://netflixtechblog.com/optimizing-recommendation-systems-with-jdks-vector-api-30d2830401ec&#34;&gt;Optimizing Recommendation Systems with JDK&amp;rsquo;s Vector API&lt;/a&gt;
目标读者：写 Java 后端、关心 JVM 性能工程的工程师；也适合任何在“要不要引入原生库/SIMD”上纠结过的人。
阅读目标：看清一次完整性能优化的正确顺序——先改计算形状，再改数据布局，最后才轮到换计算内核。以及理解为什么 BLAS 这种“标准答案”在真实生产路径里会失效。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一背景ranker-里的-75-热点&#34;&gt;一、背景：Ranker 里的 7.5% 热点&lt;/h2&gt;
&lt;p&gt;Ranker 是 Netflix 最大、最复杂的服务之一，首页那些个性化推荐行就是它算出来的。看 CPU profile 时有一个特征始终扎眼：&lt;strong&gt;视频惊喜度打分（video serendipity scoring）&lt;/strong&gt;——回答一个简单问题：“这部新片和你最近看的东西有多不一样？”这个特征吃掉每个节点约 &lt;strong&gt;7.5% 的总 CPU&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;算法本身不复杂：候选视频和用户历史里的每个视频都表示成 embedding 向量；对每个候选，算出它与所有历史向量的余弦相似度，取最大值，用 1 减去它得到“新颖度”分数，作为下游推荐逻辑的输入特征。原始实现非常直白：对每个候选取 embedding，遍历历史逐对算余弦，维护最大值。容易理解，但在 Ranker 的规模下意味着大量串行工作、重复查找、分散的内存访问和糟糕的缓存局部性。火焰图确认了这一点：服务里最热的热点之一就是惊喜度编码器里的 Java 点积。算法层面，这是一个 &lt;strong&gt;M 个候选 × N 个历史&lt;/strong&gt;的嵌套循环，产生 O(M×N) 次独立点积。&lt;/p&gt;
&lt;p&gt;一个容易被忽略但决定了整个优化方向的数据：他们埋点统计了流量形状，发现&lt;strong&gt;约 98% 的请求是单视频请求，剩下 2% 是大批量请求&lt;/strong&gt;——但那 2% 的批次实在太大，最终处理的视频总量在单视频和批处理之间大约是 &lt;strong&gt;50:50&lt;/strong&gt;。所以批处理值得做，即使它对中位数请求毫无帮助。&lt;/p&gt;
&lt;h2 id=&#34;二第一步把嵌套循环看成矩阵乘&#34;&gt;二、第一步：把嵌套循环看成矩阵乘&lt;/h2&gt;
&lt;p&gt;第一个想法是换“计算形状”：不再想成“很多次小点积”，而是当成一次矩阵运算。设 embedding 维度为 D：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把所有候选 embedding 装进矩阵 A（形状 M×D）；&lt;/li&gt;
&lt;li&gt;把所有历史 embedding 装进矩阵 B（形状 N×D）；&lt;/li&gt;
&lt;li&gt;所有行归一化成单位向量；&lt;/li&gt;
&lt;li&gt;计算 &lt;code&gt;C = A × Bᵀ&lt;/code&gt;，C 就是 M×N 的余弦相似度矩阵。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样 &lt;strong&gt;M×N 次独立点积变成一次矩阵乘&lt;/strong&gt;——而这正是 CPU 和优化过的计算内核最擅长的东西。工程上通过同时支持 &lt;code&gt;encode()&lt;/code&gt;（单视频）和 &lt;code&gt;batchEncode()&lt;/code&gt;（批量）接入现有框架，保持向后兼容。到这里看起来像“做完了”。但并没有。&lt;/p&gt;
&lt;h2 id=&#34;三第二步批处理反而慢了-5&#34;&gt;三、第二步：批处理反而慢了 5%&lt;/h2&gt;
&lt;p&gt;上了批处理版本跑金丝雀，结果出人意料：&lt;strong&gt;性能回退约 5%&lt;/strong&gt;。算法没问题——把点积归并成矩阵乘在数学上是等价的。问题出在第一版实现自己引入的开销：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每个批次都新建 &lt;code&gt;double[][]&lt;/code&gt; 矩阵装候选、历史和结果。这些又大又短命的对象制造了 GC 压力；而且 &lt;code&gt;double[][]&lt;/code&gt; 在内存里本身就不连续，多一层指针跳转，缓存行为更差。&lt;/li&gt;
&lt;li&gt;第一版 Java 矩阵乘是朴素的标量实现，用不上 SIMD。换句话说，&lt;strong&gt;付了批处理的成本，却没拿到批处理该有的计算效率&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;教训来得很快：算法层面的改进，如果实现细节——内存布局、分配策略、计算内核——跟你作对，就一文不值。这一步定下了后面的路线：先把数据布局改成缓存友好的，消灭每批次分配，然后再回头解决矩阵乘内核。&lt;/p&gt;
&lt;h2 id=&#34;四第三步扁平-buffer-加-threadlocal-复用&#34;&gt;四、第三步：扁平 buffer 加 ThreadLocal 复用&lt;/h2&gt;
&lt;p&gt;重做数据布局。&lt;code&gt;double[m][n]&lt;/code&gt; 换成&lt;strong&gt;行主序的扁平 &lt;code&gt;double[]&lt;/code&gt;&lt;/strong&gt;：内存连续，访问模式可预测。然后引入 &lt;code&gt;ThreadLocal&amp;lt;BufferHolder&amp;gt;&lt;/code&gt;，持有候选、历史等可复用 buffer——&lt;strong&gt;buffer 只增长不收缩&lt;/strong&gt;，避免每请求分配，同时每个线程持有自己的一份，无锁无争用。核心逻辑就几行：&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&gt;class BufferHolder {
  double[] candidatesFlat = new double[0];
  double[] getCandidatesFlat(int required) {
    if (candidatesFlat.length &amp;lt; required) {
      candidatesFlat = new double[required];
    }
    return candidatesFlat;
  }
  // historyFlat 同理
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;光是这一步，批处理路径就变得可预测多了：分配少了，GC 压力小了，缓存局部性好了。剩下的问题回到最初的那个：矩阵乘到底用什么算？&lt;/p&gt;
&lt;h2 id=&#34;五第四步blas测试里很好生产里不行&#34;&gt;五、第四步：BLAS——测试里很好，生产里不行&lt;/h2&gt;
&lt;p&gt;显而易见的下一步是 BLAS（Basic Linear Algebra Subprograms）。隔离的微基准看起来很有希望，但一旦接进真实的批量打分路径，收益就没兑现。几个因素在作对：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;默认的 &lt;code&gt;netlib-java&lt;/code&gt; 路径走的是 F2J（Fortran 转 Java）的 BLAS，不是真正的原生实现；&lt;/li&gt;
&lt;li&gt;就算用原生 BLAS，也要付 setup 和 JNI 边界切换的开销；&lt;/li&gt;
&lt;li&gt;Java 的行主序和很多 BLAS 例程期望的列主序对不上，会引入转换和临时 buffer；&lt;/li&gt;
&lt;li&gt;这些额外的分配和拷贝在完整流水线里很扎眼，尤其是旁边还有 TensorFlow 的 embedding 计算。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;BLAS 不是白试——它帮忙看清了时间到底花在哪。但它不是那种即插即用的胜利。他们需要的是：&lt;strong&gt;纯 Java、贴合扁平 buffer 架构、还能用上 SIMD&lt;/strong&gt; 的东西。&lt;/p&gt;
&lt;h2 id=&#34;六第五步jdk-vector-api&#34;&gt;六、第五步：JDK Vector API&lt;/h2&gt;
&lt;h3 id=&#34;先补一课jdk-vector-api-到底是什么&#34;&gt;先补一课：JDK Vector API 到底是什么&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;起点是 SIMD。&lt;/strong&gt; 现代 x86 CPU 都有向量寄存器，一条指令可以同时算多个数：AVX2 的 256 位寄存器一次装 4 个 double，AVX-512 的 512 位一次装 8 个。一条向量乘加指令顶 4-8 条标量指令——这就是 SIMD（Single Instruction, Multiple Data，单指令多数据），点积、矩阵乘这类规则的数值计算是它的主场。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Java 的痛点是：这些指令一直在硬件里，Java 程序员却够不着。&lt;/strong&gt; JIT 有自动向量化，但只对形状简单、能被它证明安全的循环生效，代码稍微复杂一点就悄悄退回标量，而且没有任何方式强制它发生；想要确定性的 SIMD，只能走 JNI 调原生库——代价就是上一步 BLAS 踩过的那些坑。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Vector API 是 Project Panama 的一部分&lt;/strong&gt;，从 JDK 16 起以孵化模块 &lt;code&gt;jdk.incubator.vector&lt;/code&gt; 的形式提供，到原文写作时仍在孵化（所以运行时要显式 &lt;code&gt;--add-modules&lt;/code&gt;）。它把 SIMD 变成了普通 Java 代码：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;VectorSpecies&lt;/code&gt; 描述「一次算几个 lane」（lane 就是向量寄存器里的一个槽位），&lt;code&gt;SPECIES_PREFERRED&lt;/code&gt; 让运行时按当前 CPU 挑最优宽度；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DoubleVector.fromArray(...)&lt;/code&gt; 从数组加载一组 lane，&lt;code&gt;fma(b, acc)&lt;/code&gt; 做向量融合乘加，&lt;code&gt;reduceLanes(ADD)&lt;/code&gt; 把各 lane 归约成一个标量；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;同一份代码的可移植性由 JIT 兜底&lt;/strong&gt;：编译时映射到宿主机器最好的指令集，AVX-512 机器跑 8 lane，AVX2 机器跑 4 lane，都不支持就退回标量。&lt;/li&gt;
&lt;/ul&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
    subgraph S[&amp;#34;标量路径 一条指令算一个数&amp;#34;]
        A1[&amp;#34;a0 乘 b0&amp;#34;] --&amp;gt; R1[&amp;#34;acc 逐个累加&amp;#34;]
        A2[&amp;#34;a1 乘 b1&amp;#34;] --&amp;gt; R1
        A3[&amp;#34;a2 乘 b2&amp;#34;] --&amp;gt; R1
        A4[&amp;#34;a3 乘 b3&amp;#34;] --&amp;gt; R1
    end
    subgraph V[&amp;#34;SIMD 路径 AVX2 一条指令算四个数&amp;#34;]
        B1[&amp;#34;向量寄存器加载&amp;lt;br/&amp;gt;a0 a1 a2 a3&amp;#34;] --&amp;gt; F[&amp;#34;一条 fma 指令&amp;#34;]
        B2[&amp;#34;向量寄存器加载&amp;lt;br/&amp;gt;b0 b1 b2 b3&amp;#34;] --&amp;gt; F
        F --&amp;gt; R2[&amp;#34;acc 四个 lane 同时累加&amp;#34;]
    end
    style F fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;一句话总结：&lt;strong&gt;它把「手写 intrinsics 的活」变成了「写普通循环的活」&lt;/strong&gt;——你不用知道目标机器是 SSE 还是 AVX-512，JIT 替你选。对他们来说更关键的是：&lt;strong&gt;纯 Java，没有原生依赖，没有 JNI 切换，写起来就是普通 Java 代码&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;跟他们的负载是绝配：embedding 已经搬进了扁平连续的 &lt;code&gt;double[]&lt;/code&gt; buffer，热循环被海量点积占满。最后一步就是用基于 Vector API 的纯 Java SIMD 实现换掉 BLAS——藏在一个小工厂后面，类加载时由 &lt;code&gt;MatMulFactory&lt;/code&gt; 选最优实现：&lt;code&gt;jdk.incubator.vector&lt;/code&gt; 可用就用 Vector API，否则退回一个高度优化的标量实现——循环展开的点积（同事 Patrick Strawderman 实现，灵感来自 Lucene 里的模式）。&lt;/p&gt;
&lt;p&gt;Vector API 版本的内层循环：用 &lt;code&gt;fma()&lt;/code&gt;（fused multiply-add）把 &lt;code&gt;a * b&lt;/code&gt; 累加进向量累加器；&lt;code&gt;DoubleVector.SPECIES_PREFERRED&lt;/code&gt; 让运行时自己挑适合机器的 lane 宽度（AVX2 上通常 4 个 double，AVX-512 上 8 个）。简化版：&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&gt;for (int i = 0; i &amp;lt; M; i++) {
  for (int j = 0; j &amp;lt; N; j++) {
    DoubleVector acc = DoubleVector.zero(SPECIES);
    int k = 0;
    for (; k + SPECIES.length() &amp;lt;= D; k += SPECIES.length()) {
      DoubleVector a = DoubleVector.fromArray(SPECIES, candidatesFlat, i*D + k);
      DoubleVector b = DoubleVector.fromArray(SPECIES, historyFlat, j*D + k);
      acc = a.fma(b, acc);  // 融合乘加
    }
    double dot = acc.reduceLanes(VectorOperators.ADD);
    // 处理 k..D-1 的尾部
    similaritiesFlat[i*N + j] = dot;
  }
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;原来的一长串标量乘加，变成少量向量 &lt;code&gt;fma()&lt;/code&gt; 加一次归约——算法没变，CPU 向量单元的利用率完全不同。&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
    subgraph OLD[&amp;#34;原始路径 标量&amp;#34;]
        O1[&amp;#34;候选 embedding&amp;#34;] --&amp;gt; O3[&amp;#34;逐个算余弦相似度&amp;#34;]
        O2[&amp;#34;历史 embedding 逐个取&amp;#34;] --&amp;gt; O3
        O3 --&amp;gt; O4[&amp;#34;M 乘 N 次独立点积&amp;lt;br/&amp;gt;分散内存访问 缓存不友好&amp;#34;]
    end
    subgraph NEW[&amp;#34;优化后路径 SIMD&amp;#34;]
        N1[&amp;#34;候选装入扁平 buffer A&amp;lt;br/&amp;gt;M×D 行主序&amp;#34;] --&amp;gt; N3[&amp;#34;归一化为单位向量&amp;#34;]
        N2[&amp;#34;历史装入扁平 buffer B&amp;lt;br/&amp;gt;N×D 行主序&amp;#34;] --&amp;gt; N3
        N3 --&amp;gt; N4[&amp;#34;一次矩阵乘 C = A × Bᵀ&amp;lt;br/&amp;gt;Vector API fma 累加&amp;#34;]
        N4 --&amp;gt; N5[&amp;#34;每行取最大值&amp;lt;br/&amp;gt;novelty = 1 - maxSim&amp;#34;]
    end
    O4 -.-&amp;gt;|&amp;#34;重构计算形状&amp;#34;| N1
    style O4 fill:#ffcccc
    style N4 fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h2 id=&#34;七回退与安全孵化特性的生产姿势&#34;&gt;七、回退与安全：孵化特性的生产姿势&lt;/h2&gt;
&lt;p&gt;Vector API 还在孵化，运行时要加 &lt;code&gt;--add-modules=jdk.incubator.vector&lt;/code&gt; 才能用。他们不想让正确性和可用性绑在这个 flag 上，所以把回退行为显式设计出来：启动时探测 Vector API 支持，有就走 SIMD 批量矩阵乘，没有就走优化过的标量路径，单视频请求继续用逐条实现。运营故事很干净：服务可以选择启用 Vector API 拿满性能，不启用系统也照样安全可预测。&lt;/p&gt;
&lt;h2 id=&#34;八生产结果&#34;&gt;八、生产结果&lt;/h2&gt;
&lt;p&gt;完整方案（批处理 + 扁平 buffer + ThreadLocal 复用 + Vector API）在跑生产流量的金丝雀上测得：&lt;strong&gt;CPU 利用率下降约 7%，平均延迟下降约 12%&lt;/strong&gt;；为了归一化吞吐差异还跟踪了 CPU/RPS（每 RPS 消耗的 CPU），&lt;strong&gt;改善约 10%&lt;/strong&gt;——同样的流量可以少用约 10% 的 CPU，全量上线后数据保持住了。在函数级别，这个特征占的 CPU 从最初的 &lt;strong&gt;7.5% 降到约 1%&lt;/strong&gt;。汇编层面的变化也很直白：从循环展开的标量点积，变成 AVX-512 硬件上的向量化矩阵乘。&lt;/p&gt;
&lt;hr&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; 他们最后换的才是内核（Vector API），但前面三步缺任何一步，换内核都拿不到收益。那个 5% 的回退也说明：跳过布局直接批处理，等于同时引入新开销和新瓶颈。做性能优化先问“形状对不对”，再问“数据在内存里躺得对不对”，最后才轮到“用什么算”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;微基准通过不等于生产有效。&lt;/strong&gt; BLAS 在隔离测试里很有希望，一进真实路径就被 JNI、布局转换、临时分配吃光收益。任何库的 benchmark 都只对它自己的测试负责；决策必须在真实流量路径里做，最好带着上下游（他们旁边还跑着 TensorFlow 的 embedding 计算）一起测。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;double[][]&lt;/code&gt; 到扁平 &lt;code&gt;double[]&lt;/code&gt; 是 Java 性能工程的经典动作。&lt;/strong&gt; 二维数组是“数组的数组”，行指针分散，缓存局部性差；扁平数组行主序连续、访问可预测，配合 ThreadLocal buffer 只增不缩，每请求分配降成零——这套组合在 Lucene、Netty 里到处都是，值得变成肌肉记忆。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流量形状决定优化对象。&lt;/strong&gt; 98% 的请求是单视频，只看中位数的话批处理毫无价值；但那 2% 的大批量扛了一半总计算量。看 p50 优化和看总量优化是两个问题，先埋点搞清楚自己的流量长什么样。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;孵化/预览特性的正确姿势：工厂加探测加回退。&lt;/strong&gt; 不赌 &lt;code&gt;--add-modules&lt;/code&gt; 一定加得上，启动时探测、随时退回标量实现，“试用新 JDK 特性”就从一次豪赌变成一个配置项。这个模式可以复用到任何“新能力可用性不确定”的场景。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;顺带一个私货：这篇文章最好的地方是诚实——它把“我们以为做完了结果慢了 5%”和“BLAS 没成”都写了出来。性能工程的真实过程就是这样：每一步的正确性都要靠金丝雀和生产数据确认，不存在一步到位。那些只写成功结果的优化文章，参考价值反而低。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;本篇小结&#34;&gt;本篇小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题&lt;/strong&gt;：Netflix Ranker 的视频惊喜度打分（候选与历史 embedding 的余弦相似度取 max）占单节点 CPU 约 7.5%，热点是 O(M×N) 的逐对点积；流量形状 98% 单视频、2% 大批量，但计算总量 50:50。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;路径&lt;/strong&gt;：M×N 次点积重构成矩阵乘 &lt;code&gt;C = A × Bᵀ&lt;/code&gt;（行归一化后即余弦相似度矩阵）→ 金丝雀回退约 5%（&lt;code&gt;double[][]&lt;/code&gt; 分配与 GC、非连续布局、标量内核）→ 扁平 &lt;code&gt;double[]&lt;/code&gt; 行主序加 &lt;code&gt;ThreadLocal&amp;lt;BufferHolder&amp;gt;&lt;/code&gt; 复用（buffer 只增不缩）→ BLAS 因 F2J 非原生、JNI 开销、行列主序不匹配而放弃 → 终解 JDK Vector API（&lt;code&gt;fma()&lt;/code&gt; 累加、&lt;code&gt;SPECIES_PREFERRED&lt;/code&gt; 自适应 lane 宽度、JIT 映射 SSE/AVX2/AVX-512）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全姿势&lt;/strong&gt;：&lt;code&gt;MatMulFactory&lt;/code&gt; 启动时探测，不可用则回退到 Lucene 风格的循环展开标量实现，不依赖 &lt;code&gt;--add-modules&lt;/code&gt; 一定在场。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生产结果&lt;/strong&gt;：CPU 降约 7%、平均延迟降约 12%、CPU/RPS 改善约 10%、该特征的函数级 CPU 占比从 7.5% 降到约 1%；汇编从标量循环展开点积变成 AVX-512 向量化矩阵乘。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心结论&lt;/strong&gt;：优化拼的不是“最快的库”，而是把计算形状、内存布局、分配策略这些基本功做对——这些就位之后，Vector API 才成为那个“顺其自然的选择”。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;参考资料&#34;&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://netflixtechblog.com/optimizing-recommendation-systems-with-jdks-vector-api-30d2830401ec&#34;&gt;Optimizing Recommendation Systems with JDK&amp;rsquo;s Vector API — Netflix Technology Blog&lt;/a&gt;（原文，作者 Harshad Sane，2026-03-03）&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://openjdk.org/jeps/338&#34;&gt;JEP 338: Vector API (Incubator) — OpenJDK&lt;/a&gt;（Vector API 的 JEP 入口，含 SPECIES、fma、lane 等概念定义；后续孵化迭代见 JEP 417/426/448/460）&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/fommil/netlib-java&#34;&gt;netlib-java — GitHub&lt;/a&gt;（文中提到的 Java BLAS 库，默认 F2J 路径的出处）&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://lucene.apache.org/&#34;&gt;Apache Lucene&lt;/a&gt;（回退路径标量点积实现的灵感来源，Lucene 在向量检索里大量使用循环展开和 Panama Vector API）&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    <item>
      <title>Netflix 的 LLM 后训练框架：从「跑通脚本」到「管好四根支柱」</title>
      <link>https://ftmao.com/blog/posts/netflix-llm-post-training/</link>
      <pubDate>Fri, 13 Feb 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/netflix-llm-post-training/</guid>
      <description>&lt;h1 id=&#34;netflix-的-llm-后训练框架从跑通脚本到管好四根支柱&#34;&gt;Netflix 的 LLM 后训练框架：从「跑通脚本」到「管好四根支柱」&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;原文：&lt;a href=&#34;https://netflixtechblog.com/scaling-llm-post-training-at-netflix-0046f8790194&#34;&gt;Scaling LLM Post-Training at Netflix&lt;/a&gt;
目标读者：做过或想做 LLM 微调/强化学习的工程师，尤其是正在考虑「自建训练管线还是买现成框架」的人。
阅读目标：搞清楚两件事——后训练在 Netflix 规模下到底难在哪；以及一个内部框架靠什么证明自己比「组装开源组件」更有价值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;先拆一个直觉后训练不就是拿个开源模型-finetune-一下吗&#34;&gt;〇、先拆一个直觉：后训练不就是「拿个开源模型 finetune 一下」吗&lt;/h2&gt;
&lt;p&gt;直觉版本的后训练长这样：准备点私有数据，从 Hugging Face 拉一个开源权重，写个 batch 循环迭代。实验规模下确实就几行代码。原文的核心论点是：&lt;strong&gt;从「跑通脚本」到「生产级后训练」之间隔着一整条工程鸿沟&lt;/strong&gt;——数据管线、多机 GPU 集群的分布式状态、训练与推理交错的工作流，每一个都是深渊。&lt;/p&gt;
&lt;p&gt;Netflix 的场景是推荐、个性化和搜索，需要让通用基座模型理解自家片库和用户交互历史。这种场景下，post-training 很快就不再是建模问题，而是分布式系统问题。AI Platform 团队建这个框架的目标很直白：&lt;strong&gt;把基础设施复杂度藏起来，让模型开发者专注模型本身，而不是分布式管道工。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;一三个环节每个都有自己的坑&#34;&gt;一、三个环节，每个都有自己的坑&lt;/h2&gt;
&lt;p&gt;原文把后训练旅程拆成三步，每一步都有一批「纸面上没有」的坑。&lt;/p&gt;
&lt;h3 id=&#34;数据难的不是读取是精确控制&#34;&gt;数据：难的不是读取，是精确控制&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Loss masking 是命门。&lt;/strong&gt; HF chat template 只负责把对话序列化成文本，不告诉你哪些 token 该进 loss。必须显式做 loss mask，只让 assistant 的 token 参与优化——否则模型会去学习 prompt 和非目标文本，质量直接掉。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;变长序列是隐形浪费。&lt;/strong&gt; batch 内 padding 浪费算力，FSDP 各 worker 形状不齐会带来 GPU 同步开销。更省的做法是 &lt;strong&gt;sequence packing&lt;/strong&gt;：把多个样本塞进定长序列，再用 document mask 阻止跨样本注意力。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;模型加载不了只是开始&#34;&gt;模型：加载不了只是开始&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;模型放不进单卡时，需要 FSDP/TP 分片，并且要&lt;strong&gt;把权重分片直接加载到 device mesh 上&lt;/strong&gt;，避免在任何单设备上物化完整模型。&lt;/li&gt;
&lt;li&gt;加载完还要让它「可训练」：全量微调还是 LoRA，activation checkpointing、编译、精度设置——RL 场景下 rollout 和 policy 的精度必须对齐，这是容易翻车的细节。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大词表（＞128k）的内存陷阱&lt;/strong&gt;：logits 形状是 batch × seq_len × vocab，峰值内存会炸。常见缓解：投影前先丢掉被忽略的 token，沿序列维度分块算 logits 和 loss。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;训练生产训练不是一个-for-循环&#34;&gt;训练：生产训练不是一个 for 循环&lt;/h3&gt;
&lt;p&gt;要支持从 SFT 的单次前反向传播，到 on-policy RL 那种「rollout 生成、reward/reference 推理、policy 更新交错进行」的工作流。Netflix 用 Ray 的 actor 模型做编排，把建模逻辑和硬件解耦。另外两样是标配：实验追踪（loss 这类质量指标 + MFU 这类效率指标）和标准化 checkpoint 带来的容错恢复。&lt;/p&gt;
&lt;h2 id=&#34;二框架总览四根支柱&#34;&gt;二、框架总览：四根支柱&lt;/h2&gt;
&lt;p&gt;整个技术栈从底到顶是：Mako（Netflix 内部 ML 计算平台，在 AWS 上供给 GPU）→ 开源三件套（PyTorch、Ray、vLLM，基本开箱即用）→ Post-Training Framework（以库的形式提供 SFT、DPO、RL、知识蒸馏等标准化 recipe，用户用配置文件选配方、插任务组件）。&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
    subgraph APP[&amp;#34;使用者: 模型开发者&amp;#34;]
        CFG[&amp;#34;配置文件&amp;lt;br/&amp;gt;选 recipe 插组件&amp;#34;]
    end
    subgraph FW[&amp;#34;Post-Training Framework&amp;#34;]
        R1[&amp;#34;SFT&amp;#34;] --- R2[&amp;#34;DPO&amp;#34;] --- R3[&amp;#34;RL&amp;#34;] --- R4[&amp;#34;蒸馏&amp;#34;]
    end
    subgraph OSS[&amp;#34;开源基础组件&amp;#34;]
        PT[&amp;#34;PyTorch&amp;#34;] --- RAY[&amp;#34;Ray&amp;#34;] --- VLLM[&amp;#34;vLLM&amp;#34;]
    end
    subgraph INFRA[&amp;#34;基础设施&amp;#34;]
        MAKO[&amp;#34;Mako 内部 ML 计算平台&amp;lt;br/&amp;gt;在 AWS 上供给 GPU&amp;#34;]
    end
    CFG --&amp;gt; FW
    FW --&amp;gt; OSS
    OSS --&amp;gt; MAKO
    style FW fill:#fff3cd&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;框架的能力沿四个维度组织，对应 ML 系统的三支柱加一个新增支柱：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Data&lt;/strong&gt;：SFT、奖励建模、RL 各自的数据集抽象；超本地存储的数据集从云和磁盘做高吞吐流式读取；&lt;strong&gt;异步 on-the-fly packing&lt;/strong&gt;，把 CPU 密集的打包和 GPU 计算重叠，减少空闲。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Model&lt;/strong&gt;：支持现代架构（Qwen3、Gemma3）和 MoE 变体（Qwen3 MoE、GPT-OSS）；LoRA 直接集成进模型定义；高层 sharding API，开发者不用写底层分布式代码就能跨 device mesh 分片。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compute&lt;/strong&gt;：统一作业提交接口，单机到几百卡一个入口；自定义架构和 LoRA 下依然准确的 MFU 监控；完整 checkpoint（参数、优化器、dataloader、data mixer 状态），中断后精确恢复。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workflow&lt;/strong&gt;：这是 RL 逼出来的第四支柱——支持超越「单个训练循环」的多阶段执行模式。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;三教训一从-sft-到-rl架构要换心&#34;&gt;三、教训一：从 SFT 到 RL，架构要换心&lt;/h2&gt;
&lt;p&gt;框架最初围绕 SFT 设计：数据流相对静态、单一训练循环、SPMD 执行模型（每个 GPU worker 跑同一个 step 函数，处理不同数据分片，靠 PyTorch 分布式原语同步）。这个假设在 2025 年失效了——DeepSeek-R1 和 GRPO 这类高效 on-policy RL 普及之后，SFT 从终点变成了入场券。&lt;/p&gt;
&lt;p&gt;两者差异的本质在学习信号：&lt;strong&gt;SFT 的信号稠密而即时&lt;/strong&gt;（每个 token 位置都有全词表 logits 和可微 loss），长得就像 pre-training；&lt;strong&gt;on-policy RL 的信号稀疏而延迟&lt;/strong&gt;（episode 结束才有一个标量 reward），且训练数据依赖当前 policy 生成。单个子阶段（policy 更新、rollout、reference 推理、reward 打分）各自都能 SPMD 化，但端到端算法需要显式协调：prompt、采样轨迹、reward、advantage 这些产物在阶段间不停交接，生命周期要有人管。&lt;/p&gt;
&lt;p&gt;架构上的变化：原来的 driver 是「瘦」的，只负责拉起 N 个相同的 Ray actor，扩容就是加 worker；RL 要求把系统拆成不同角色（Policy、Rollout Worker、Reward Model、Reference Model），driver 进化成&lt;strong&gt;主动控制器&lt;/strong&gt;，编码整个控制面——何时生成 rollout、怎么组 batch 和打分、何时触发优化、跨阶段怎么调度集群资源。&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
    subgraph SFT[&amp;#34;SFT 架构: 瘦 driver + SPMD&amp;#34;]
        D1[&amp;#34;瘦 driver&amp;lt;br/&amp;gt;只负责拉起 worker&amp;#34;] --&amp;gt; A1[&amp;#34;actor 1&amp;lt;br/&amp;gt;完整训练循环&amp;#34;]
        D1 --&amp;gt; A2[&amp;#34;actor 2&amp;lt;br/&amp;gt;完整训练循环&amp;#34;]
        D1 --&amp;gt; A3[&amp;#34;actor N&amp;lt;br/&amp;gt;完整训练循环&amp;#34;]
    end
    subgraph RL[&amp;#34;RL 架构: 主动控制器 + 多角色&amp;#34;]
        C[&amp;#34;主动控制器&amp;lt;br/&amp;gt;编码控制面&amp;lt;br/&amp;gt;何时 rollout 何时优化&amp;#34;] --&amp;gt; P[&amp;#34;Policy 更新&amp;#34;]
        C --&amp;gt; R[&amp;#34;Rollout Worker&amp;#34;]
        C --&amp;gt; RM[&amp;#34;Reward Model&amp;#34;]
        C --&amp;gt; REF[&amp;#34;Reference Model&amp;#34;]
        R --&amp;gt;|&amp;#34;轨迹&amp;#34;| RM
        RM --&amp;gt;|&amp;#34;reward 和 advantage&amp;#34;| P
    end
    SFT ==&amp;gt;|&amp;#34;2025 年 DeepSeek-R1 与 GRPO 普及&amp;lt;br/&amp;gt;SFT 从终点变成入场券&amp;#34;| RL
    style RL fill:#fff3cd&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;落地方式是集成开源库 &lt;strong&gt;Verl&lt;/strong&gt; 的核心设施来管理 Ray actor 生命周期和 GPU 资源分配，自己只做「建模表面积」——Data/Model/Compute 抽象和内部优化。最终是混合设计：用户在 SFT 和 RL 之间切换不用换心智模型。这个取舍值得画重点：&lt;strong&gt;编排不自己造轮子，抽象层才是护城河。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;四教训二全面押注-hugging-face但模型实现自己来&#34;&gt;四、教训二：全面押注 Hugging Face，但模型实现自己来&lt;/h2&gt;
&lt;p&gt;HF Hub 事实上已是开源权重、tokenizer、配置的默认分发渠道，框架选择贴着生态走而不是另立内部标准：checkpoint 一律按 HF 格式存取，避免「围墙花园」摩擦。&lt;/p&gt;
&lt;p&gt;Tokenizer 的故事最有教训价值。早期直接绑定底层分词库（SentencePiece、tiktoken）追求控制力，结果造成&lt;strong&gt;静默的训练-推理 skew&lt;/strong&gt;：推理栈（vLLM）默认用 HF AutoTokenizer，归一化、特殊 token、chat template 的细微差异会产生不同的 token 边界——这类 mismatch 之后会以「莫名其妙的质量回退」形式冒出来，极难排查。修法：&lt;strong&gt;AutoTokenizer 成为唯一事实源&lt;/strong&gt;，上面加一层薄薄的兼容层（&lt;code&gt;BaseHFModelTokenizer&lt;/code&gt;）处理后训练特有需求（padding token、生成标记注入、语义 ID），同时保证字节级分词路径和生产一致。&lt;/p&gt;
&lt;p&gt;模型实现则相反：&lt;strong&gt;不直接用 transformers 的模型类训练&lt;/strong&gt;，而是维护自己优化的统一模型定义，能读写 HF checkpoint。这一层换来 FlexAttention、分块交叉熵、统一 MFU 核算、统一 LoRA 扩展，跨架构不用重实现；统一的模块命名还让程序化定位和替换组件（Attention、MLP、输出头）成为可能。&lt;/p&gt;
&lt;p&gt;代价很直接：支持一个新模型家族，要写 HF 参考实现到内部定义的桥。Netflix 的解法是 &lt;strong&gt;AI coding agent + logit verifier&lt;/strong&gt;：agent 自动生成大部分转换代码，验收标准是给定随机输入、内部模型的 logits 必须在容差内匹配 HF——因为验收可以机械检查，agent 能自主迭代到正确为止，大幅缩短新架构的支持周期。当前只支持显式适配过的架构（和 vLLM、SGLang、torchtitan 同款取舍），计划中的 HF fallback 后端会允许直接跑原生 transformers 模型做快速探索，代价是部分优化不生效。&lt;/p&gt;
&lt;h2 id=&#34;五教训三差异化价值投在深水区&#34;&gt;五、教训三：差异化价值投在深水区&lt;/h2&gt;
&lt;p&gt;自建框架值不值，取决于有没有超出「组装开源件」的价值。Netflix 的答案：开源换速度，重投入换两个方向——&lt;strong&gt;贴合自家负载特征的性能&lt;/strong&gt;和&lt;strong&gt;Netflix 特有的业务需求&lt;/strong&gt;。两个具体例子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;On-the-fly sequence packing。&lt;/strong&gt; FSDP 训练里长尾序列制造 straggler，快的 worker 在同步点干等。标准的离线 bin-packing 有效，但在他们的数据规模下预处理延迟可观、数据难保鲜。解法是从存储流式读样本、在内存里动态打包，打包异步跑、和 GPU 计算重叠。结果：最倾斜的数据集上有效 token 吞吐提升最高 &lt;strong&gt;4.7 倍&lt;/strong&gt;（A100 和 H200 上均验证）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;词表大小对齐 64。&lt;/strong&gt; 业务常加自定义 token 和语义 ID，某些词表大小会让 LM head 从高度优化的 cuBLAS kernel 掉到慢得多的 CUTLASS 路径，该层耗时翻三倍。框架自动把词表 pad 到 64 的倍数，开发者不需要知道这层硬件玄学。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第二个方向是支持「非标准」transformer 用例：比如基于用户交互事件序列（而非自然语言）训练的模型，需要定制的 RL 循环、对接高度定制的推理引擎、优化业务定义的指标——这些要求定制环境、奖励计算和编排模式，但性能、追踪、容错的底层保障必须一样。&lt;/p&gt;
&lt;h2 id=&#34;工程实践联想&#34;&gt;工程实践联想&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;「跑通」和「生产化」之间的鸿沟，大部分都是状态管理。&lt;/strong&gt; Loss mask、checkpoint 完整性、训练-推理一致性——每一项单看都是小细节，合起来决定系统可不可靠。这和我们写业务系统一样：demo 一周，上线半年，差的全在边界条件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;训练-推理 skew 是数据系统里「双写不一致」的 LLM 版本。&lt;/strong&gt; 两份各自「正确」的实现（训练 tokenizer vs 推理 tokenizer）因为微小差异产生静默错误。通用解法也通用：指定唯一事实源，其他都向它对齐，用兼容性测试兜底——logit verifier 本质就是契约测试。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI coding agent 最好用的地方是「验收可机械检查」的场景。&lt;/strong&gt; HF 到内部模型定义的桥接，验收是 logits 数值匹配，agent 可以放心自主迭代。反过来说，给 agent 派活之前先问：这活的验收标准能不能写成断言？写不出断言的活，agent 做完了你还得人工审。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;编排层买、抽象层造，这个取舍可以抄。&lt;/strong&gt; Verl 管 actor 生命周期，自己只做 Data/Model/Compute/Workflow 抽象。通用版：消息队列买、存储买，贴合业务的领域模型自己造——护城河在建模，不在管道。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能优化要贴着自家负载的真实分布做。&lt;/strong&gt; 4.7 倍不是来自通用调优，是来自「我们的序列长度方差极端」这个具体事实；词表对齐 64 同理。先测量自己的负载长什么样，再决定优化什么——别人的 benchmark 不构成你的优化清单。&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 规模下是工程问题。框架目标是让模型开发者不碰分布式管道。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;三个坑位&lt;/strong&gt;：数据（loss mask、packing）、模型（分片加载、大词表 logits 内存）、训练（不只是 for 循环，要编排和容错）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;四根支柱&lt;/strong&gt;：Data、Model、Compute、Workflow——第四根是 RL 逼出来的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;架构演进&lt;/strong&gt;：SFT 的「瘦 driver + SPMD」演进为 RL 的「主动控制器 + 多角色」，编排集成 Verl，自己保住抽象层。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生态策略&lt;/strong&gt;：checkpoint 和 tokenizer 全面贴 HF（AutoTokenizer 是唯一事实源，治训练-推理 skew）；模型实现自己维护换优化空间，桥接新架构用 AI agent + logit verifier 自动化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;差异化投入&lt;/strong&gt;：on-the-fly packing 最高 4.7 倍吞吐；词表对齐 64 避开 kernel 回退；支持交互序列这类非自然语言 transformer。&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/scaling-llm-post-training-at-netflix-0046f8790194&#34;&gt;Scaling LLM Post-Training at Netflix&lt;/a&gt;（原文，Baolin Li、Lingyi Liu、Binh Tang、Shaojing Li）&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/volcengine/verl&#34;&gt;Verl&lt;/a&gt;（字节跳动开源的 RL 后训练库，Netflix 用其核心设施做 actor 生命周期与 GPU 资源管理）&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://arxiv.org/abs/2501.12948&#34;&gt;DeepSeek-R1&lt;/a&gt;（GRPO 等高效 on-policy RL 方法的代表作，原文称其为「SFT 变成入场券」的转折点）&lt;/li&gt;
&lt;li&gt;Torchtune、Torchtitan（原文致谢的两个 PyTorch 原生训练参考实现）&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://thinkingmachines.ai/tinker/&#34;&gt;Thinking Machines Tinker&lt;/a&gt;（原文对比的外部微调服务）&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    <item>
      <title>Netflix 图搜索的 AI 进化笔记：从结构化 DSL 到自然语言</title>
      <link>https://ftmao.com/blog/posts/netflix-graph-search-natural-language/</link>
      <pubDate>Mon, 26 Jan 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/netflix-graph-search-natural-language/</guid>
      <description>&lt;h1 id=&#34;netflix-图搜索的-ai-进化笔记从结构化-dsl-到自然语言&#34;&gt;Netflix 图搜索的 AI 进化笔记：从结构化 DSL 到自然语言&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;原文：&lt;a href=&#34;https://netflixtechblog.com/the-ai-evolution-of-graph-search-at-netflix-d416ec5b1151&#34;&gt;The AI Evolution of Graph Search at Netflix: From Structured Queries to Natural Language&lt;/a&gt;（Alex Hutter、Bartosz Balukiewicz，2026-01-26，Graph Search 系列第四篇，前三篇讲平台本身）
目标读者：正在做或准备做 Text-to-SQL / Text-to-Query / 企业内自然语言搜索的工程师。
阅读目标：搞清楚 Netflix 这套方案里哪些是 LLM 的活、哪些是确定性工程的活，以及为什么这样分工。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一要解决的问题用户不想学你的-dsl&#34;&gt;一、要解决的问题：用户不想学你的 DSL&lt;/h2&gt;
&lt;p&gt;先把背景补齐。Netflix 内部的 Content and Business Products 应用群有几百个 UI，常见操作是过滤一张数据表。底层平台是 Graph Search：每个索引由一个 GraphQL query 定义，过滤条件用一套专门的 Graph Search Filter DSL 表达——字段名加比较或包含运算符（如 &lt;code&gt;==&lt;/code&gt;、&lt;code&gt;IN&lt;/code&gt;）组成表达式，再用逻辑运算符（如 &lt;code&gt;AND&lt;/code&gt;）拼成复杂语句。&lt;/p&gt;
&lt;p&gt;问题在于用户和 DSL 之间隔着一层应用 UI：用户点 facet、填查询构建器，应用再把这些交互翻译成 DSL。这条链路有三个毛病，原文列得很坦白：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个应用的输入组件都是各写各的，体验不一，对 DSL 的支持程度也不一，&lt;strong&gt;用户得逐个应用去学怎么用&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;有些索引有几百个可过滤字段，领域专家明明知道自己要什么，却被「填一张巨大的表单」这件事拖住；&lt;/li&gt;
&lt;li&gt;最根本的一条：&lt;strong&gt;用户用自然语言思考，不用查询构建器思考。&lt;/strong&gt; 强制他们切换上下文，就是引入摩擦。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意 Netflix 的定位决策：自然语言搜索是&lt;strong&gt;增强而不是替换&lt;/strong&gt;现有应用，把能力做成工具供各应用按自己的领域风格接入。这个定位直接决定了后面「show your work」的设计——后面细说。&lt;/p&gt;
&lt;h2 id=&#34;二先把正确拆成三层&#34;&gt;二、先把「正确」拆成三层&lt;/h2&gt;
&lt;p&gt;这篇文章我认为最有价值的部分，是它没有笼统地说「让 LLM 生成正确的查询」，而是把正确性拆成三层，每层用不同的手段治理：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;语法正确（syntactic）&lt;/strong&gt;：生成的语句能按 DSL 文法解析。这层最简单，纯粹是形式问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语义正确（semantic）&lt;/strong&gt;：生成的语句要尊重字段类型、只能用索引里真实存在的字段（不许幻觉）、字段值若受受控词表约束则只能用词表内的值。这层需要对索引本身的了解。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语用正确（pragmatic）&lt;/strong&gt;：生成的过滤器真的捕捉到了用户意图吗？这层最难——用户搜「Dark」，是指那部德国科幻剧《暗黑》，还是想找「暗黑风格的剧」？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个三分法的价值在于：&lt;strong&gt;前两层可以确定性校验，第三层不能。&lt;/strong&gt; 原文承认他们没有对语用正确性做校验，也没有找到什么指令能让 LLM 在这层「表现更好」——唯一的手段是把上下文工程做好，再用产品设计兜住。承认 LLM 做不到什么，比宣称能做更重要。&lt;/p&gt;
&lt;h2 id=&#34;三上下文工程双路-rag-裁剪上下文&#34;&gt;三、上下文工程：双路 RAG 裁剪上下文&lt;/h2&gt;
&lt;p&gt;LLM 要生成语义正确的过滤语句，必须知道索引里有哪些字段、什么类型、可取什么值。索引由 GraphQL query 定义，所以大部分元数据可以直接从 GraphQL schema 推导：字段名（来自文档路径）、描述（schema 注释）、类型、合法值（enum 或受控词表）。受控词表（controlled vocabulary）是一类特殊字段类型：由领域专家维护的有限值集合，比如国家列表；词表里的每个值有唯一标识、显示名、描述和别名（AKA，例如「romcom」对应「Romantic Comedy」）。&lt;/p&gt;
&lt;p&gt;天真的做法是把全部元数据塞进 prompt。简单场景能跑，但不扩展：&lt;strong&gt;几百个字段、上千个词表值全塞进去，延迟线性上升，正确率反而下降&lt;/strong&gt;（大海捞针问题），而且结果变得不稳定。不给值也不行——LLM 会幻觉出不存在的值。于是 Netflix 用经典的 RAG 模式做上下文裁剪，而且是&lt;strong&gt;双路&lt;/strong&gt;的：&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
    Q[&amp;#34;用户问题&amp;lt;br/&amp;gt;自然语言&amp;#34;] --&amp;gt; CH[&amp;#34;分块&amp;lt;br/&amp;gt;重叠切分策略&amp;#34;]
    subgraph FR[&amp;#34;字段 RAG&amp;#34;]
        CH --&amp;gt; VS1[&amp;#34;向量检索&amp;lt;br/&amp;gt;字段名 描述 类型的 embedding&amp;#34;]
        VS1 --&amp;gt; TOP1[&amp;#34;每块取 top K 字段&amp;#34;]
    end
    subgraph CR[&amp;#34;受控词表 RAG&amp;#34;]
        CH --&amp;gt; VS2[&amp;#34;向量检索&amp;lt;br/&amp;gt;词表值的 embedding&amp;lt;br/&amp;gt;含别名 AKA&amp;#34;]
        VS2 --&amp;gt; TOP2[&amp;#34;每块取 top K 词表值&amp;lt;br/&amp;gt;按词表类型去重&amp;#34;]
    end
    TOP1 --&amp;gt; MG[&amp;#34;合并 去重&amp;#34;]
    TOP2 --&amp;gt; MG
    MG --&amp;gt; CTX[&amp;#34;裁剪后的上下文&amp;lt;br/&amp;gt;字段定义加匹配到的合法值&amp;#34;]
    CTX --&amp;gt; LLM[&amp;#34;LLM 加指令生成 DSL&amp;#34;]
    style CTX fill:#fff3cd&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;两路各有一个容易忽略的设计细节：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;字段 RAG&lt;/strong&gt; 里，用户问题按重叠策略分块，每块独立向量检索取 top K，再合并去重。分块是为了防止长问题里后半截的意图被前半截的 embedding 稀释。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;受控词表 RAG&lt;/strong&gt; 不只是给字段补合法值，还能&lt;strong&gt;反向推断字段&lt;/strong&gt;：问题里命中了某个词表的值，就说明相关字段该进上下文，哪怕字段 RAG 没把它捞出来。词表本身通过 GraphQL 定期拉取重建索引，跟上领域变化。&lt;/li&gt;
&lt;li&gt;原文点了一句：RAG 的效果取决于一堆「杠杆」——reranking 策略、分块策略、embedding 模型的选择——系统性评估留给本系列后两篇。这是诚实的写法，也是大部分 RAG 项目真实的工作量所在。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上下文备好后，指令本身反而朴素（大意）：「给定一个自然语言问题，基于以下索引字段及其元数据，生成语法、语义、语用正确的过滤语句。」语法规则写进指令保证第一层；要求 LLM 把生成内容锚定在提供的上下文里保证第二层；第三层只靠上下文工程间接改善。&lt;/p&gt;
&lt;h2 id=&#34;四生成之后确定性校验兜底&#34;&gt;四、生成之后：确定性校验兜底&lt;/h2&gt;
&lt;p&gt;LLM 输出不是终点。Netflix 在返回结果前做确定性校验：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;语法层&lt;/strong&gt;：用为这套 DSL 自建的 AST 解析器解析，解析失败即判定生成有问题。原文提到也评估过 LLM 的结构化输出模式，但自定义 DSL 不被原生支持，效果好坏参半，所以还是走了 AST 路线。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语义层&lt;/strong&gt;：拿生成的 AST 对照索引元数据校验——字段是否存在、类型是否匹配、词表值是否合法。因为 AST 已经解析出来了、元数据在上下文工程阶段就是现成的，&lt;strong&gt;这一步几乎不增加延迟&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;幻觉处理&lt;/strong&gt;：检测到幻觉有两个出口——直接把错误返回给用户提示改写，或者把错误反馈给 LLM 自我纠正。后者增加生成耗时，要限制重试次数、谨慎使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这套「LLM 生成 + 确定性校验」的分工是全文我最喜欢的设计决策：&lt;strong&gt;LLM 负责它擅长的模糊理解，解析器和元数据负责它做不好的精确约束。&lt;/strong&gt; 校验不通过不是系统失败，是系统正常工作。&lt;/p&gt;
&lt;h2 id=&#34;五信任问题不校验语用就展示推理&#34;&gt;五、信任问题：不校验语用，就展示推理&lt;/h2&gt;
&lt;p&gt;语用正确性没法自动校验，Netflix 的选择是把判断权交还用户，两个手段：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Show your work。&lt;/strong&gt; 不把裸 DSL 字符串甩给非技术用户，而是把 AST 反射回现有的 UI 组件：LLM 生成 origin.country 等于 Germany，用户看到的「Country」下拉框就被预选成 Germany。用户一眼能看出系统理解得对不对，不对就用熟悉的 UI 控件微调。这个设计把「黑盒生成」变成了「可检查的初稿」——LLM 产出的不是答案，是表单的一次预填。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;显式实体消歧（@mentions）。&lt;/strong&gt; 像 Slack 一样，输入 @ 时从受控词表里直接搜实体，附带上映年份等元数据帮助确认。「When was @dark produced」里的 @dark 被显式锚定到 Series 词表，&lt;strong&gt;直接绕过 RAG 推断步骤、把上下文硬编码进去&lt;/strong&gt;，语用正确性和用户信任同时提升。这是用一点点用户操作换掉一整段不确定的机器推理，非常划算的交换。&lt;/p&gt;
&lt;p&gt;端到端架构把上面所有零件串起来，整体上 LLM 只占据中间一环，前后都是确定性工程：&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
    U[&amp;#34;用户问题&amp;lt;br/&amp;gt;可选 @mentions&amp;#34;] --&amp;gt; PRE[&amp;#34;预处理&amp;lt;br/&amp;gt;字段 RAG 加词表 RAG&amp;lt;br/&amp;gt;裁剪上下文&amp;#34;]
    M[&amp;#34;@mentions 显式实体&amp;#34;] -.-&amp;gt;|&amp;#34;绕过推断&amp;lt;br/&amp;gt;硬编码上下文&amp;#34;| PRE
    PRE --&amp;gt; GEN[&amp;#34;LLM 生成&amp;lt;br/&amp;gt;DSL 过滤语句&amp;#34;]
    GEN --&amp;gt; POST[&amp;#34;后处理&amp;lt;br/&amp;gt;AST 语法校验&amp;lt;br/&amp;gt;元数据语义校验&amp;#34;]
    POST --&amp;gt;|&amp;#34;检测到幻觉&amp;#34;| FB[&amp;#34;报错或反馈重试&amp;lt;br/&amp;gt;限制次数&amp;#34;]
    FB -.-&amp;gt; GEN
    POST --&amp;gt; AST[&amp;#34;返回 AST&amp;#34;]
    AST --&amp;gt; UI[&amp;#34;映射为 UI 组件&amp;lt;br/&amp;gt;Chips 和 Facets 预填&amp;#34;]
    style GEN fill:#fff3cd
    style POST fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&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; 语法、语义、语用三层各有各的治理手段，混在一起谈「准确率」只会得到一锅粥的指标。做 LLM 应用的第一件事应该是列出你这个场景的正确性分层，然后标注每层谁能负责——解析器、元数据、还是只能交给用户。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上下文是预算，不是仓库。&lt;/strong&gt; 几百个字段全塞 prompt 导致正确率下降，这个反直觉结论值得记住：RAG 裁剪不只是为了省 token 和延迟，更是为了正确率。给 LLM 的信息应该按「刚好够用」来裁剪，多给是有代价的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;确定性校验是 LLM 应用的承重墙。&lt;/strong&gt; AST 解析器加元数据校验几乎零延迟，却能拦下所有幻觉字段。凡是输出有形式结构的场景（SQL、DSL、配置、代码），都应该配一个确定性校验层；LLM 的结构化输出模式可以作为补充，但自定义 DSL 场景下别指望它替代解析器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;把生成结果反射回 UI，是最便宜的信任工程。&lt;/strong&gt; 不让用户读 DSL，而是预填他们熟悉的表单控件——生成的对错用户一眼可验，错了顺手就改。这比在 prompt 里再雕百分之几的准确率收益大得多。设计 LLM 功能时先问：用户怎么检查这个输出？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;留一个显式消歧的旁路。&lt;/strong&gt; @mentions 的思路可以泛化：凡是 RAG 推断不确定的地方，看看能不能用一点低成本的用户输入（选择器、@提及、下拉）把不确定性硬消掉。机器推理不该解决所有问题，尤其是那些用户举手之劳就能解决的问题。&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 内部几百个应用靠 Graph Search Filter DSL 过滤数据，用户被迫逐应用学习查询构建器；自然语言搜索的目标是把「用户学 DSL」反转成「系统懂自然语言」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;框架&lt;/strong&gt;：正确性拆成语法、语义、语用三层。语法靠 DSL 文法，语义靠索引元数据，语用没有自动校验手段，靠上下文工程加产品设计兜住。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;机制&lt;/strong&gt;：双路 RAG 做上下文裁剪——字段 RAG 按元数据 embedding 检索 top K 字段，受控词表 RAG 检索合法值并能反推字段；指令要求 LLM 锚定上下文生成 DSL。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;兜底&lt;/strong&gt;：AST 解析器做语法校验，元数据做语义校验，幻觉可报错或有限次反馈重试；结构化输出模式评估过但对自定义 DSL 效果参半。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;信任&lt;/strong&gt;：AST 反射回 UI 组件做「show your work」，@mentions 让用户显式锚定实体、绕过 RAG 推断。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分工原则&lt;/strong&gt;：LLM 只做中间一环的模糊理解，预处理和后处理全是确定性工程——这是全文最值得抄的架构判断。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;留白&lt;/strong&gt;：RAG 杠杆的调优、生产环境评估、NER、意图路由、查询改写都留给系列后两篇，值得跟进。&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/the-ai-evolution-of-graph-search-at-netflix-d416ec5b1151&#34;&gt;The AI Evolution of Graph Search at Netflix: From Structured Queries to Natural Language&lt;/a&gt;（本文，2026-01-26）&lt;/li&gt;
&lt;li&gt;Netflix Graph Search 系列前三篇（part 1、2、3，讲平台本身的联邦数据搜索架构，原文开头有链接）&lt;/li&gt;
&lt;li&gt;GraphQL schema 内省文档（字段元数据推导的来源）&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
  </channel>
</rss>
