<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>批处理 on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/tags/%E6%89%B9%E5%A4%84%E7%90%86/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Fri, 02 Oct 2026 19:00:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/%E6%89%B9%E5%A4%84%E7%90%86/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>Spark 与 Flink 入门：批与流的两种血统，视频公司里各干什么活</title>
      <link>https://ftmao.com/blog/posts/spark-flink-basics-video-company/</link>
      <pubDate>Fri, 02 Oct 2026 19:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/spark-flink-basics-video-company/</guid>
      <description>&lt;h1 id=&#34;spark-与-flink-入门批与流的两种血统视频公司里各干什么活&#34;&gt;Spark 与 Flink 入门：批与流的两种血统，视频公司里各干什么活&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;目标读者：四类人——&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;后端/数据工程师&lt;/strong&gt;：要上手 Spark 或 Flink，需要心智模型加能跑的代码（看&amp;quot;基础&amp;quot;两节，附 Java API 示例）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;架构师/技术负责人&lt;/strong&gt;：在给数据平台选型，想知道两者的边界和硬件投入（看&amp;quot;血统对比&amp;quot;&amp;ldquo;算力规格&amp;quot;和&amp;quot;选型决策&amp;rdquo;）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运维/SRE&lt;/strong&gt;：要看监控什么、出事怎么处理（看&amp;quot;监控&amp;quot;和&amp;quot;故障与恢复&amp;quot;）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;正在踩坑的人&lt;/strong&gt;：直接翻到文末两家的避坑清单。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;阅读前提：了解 Kafka 是什么即可，不需要大数据背景。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;前两篇讲推荐 Feed 架构时，Spark 和 Flink 反复出现：Spark 做离线训练，Flink 做近线特征。有读者问：Spark 不就是内存分布式计算吗，和 Flink 功能差不多？&lt;/p&gt;
&lt;p&gt;看着像，血统完全不同。这篇把两者各自的基础概念讲清，然后放进一家类 Netflix 的视频公司里看它们分别干什么活，最后附上两家的避坑指南。&lt;/p&gt;
&lt;h2 id=&#34;spark-基础批处理血统的内存计算引擎&#34;&gt;Spark 基础：批处理血统的内存计算引擎&lt;/h2&gt;
&lt;p&gt;Spark 在 2010 年前后接棒 Hadoop MapReduce，核心卖点一句话：&lt;strong&gt;把中间结果放内存，迭代计算不用反复落盘&lt;/strong&gt;。MapReduce 每轮迭代都要读写 HDFS，Spark 把数据缓存进内存，迭代型任务（机器学习训练要扫数据几十遍）快了十到一百倍。&lt;/p&gt;
&lt;p&gt;三个核心概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RDD / DataFrame&lt;/strong&gt;：数据被抽象成分布式集合，切成分区（partition）摊在集群各节点上。RDD 是原始 API，现在日常用的是 DataFrame——带结构（schema）的表，配合 Spark SQL 直接写 SQL。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DAG 执行&lt;/strong&gt;：你对数据做的一串操作（filter、join、groupBy）不会立即执行，而是攒成一张有向无环图；遇到&amp;quot;要结果&amp;quot;的动作（count、写盘）才触发，调度器把图切成 stage、分发执行。惰性求值让引擎有机会做全局优化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shuffle&lt;/strong&gt;：数据按 key 重新分发的操作（groupBy、join 都会触发），是 Spark 作业里最贵的一步——数据要跨节点搬运、序列化、落盘。后面避坑指南里一半的坑都和它有关。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;血统决定视角：在 Spark 眼里，数据是一批一批的&lt;/strong&gt;——有界的、静态的数据集。它的流处理能力（Structured Streaming）是后来在批的骨架上加的：把数据流切成一小批一小批，攒一批算一次（micro-batch）。延迟天然是秒级到分钟级。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;上手：一个 ETL 作业的 Java 写法&lt;/strong&gt;。场景：把昨天的播放原始日志清洗成&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;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-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;SparkSession spark &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; SparkSession.&lt;span style=&#34;color:#a6e22e&#34;&gt;builder&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:#a6e22e&#34;&gt;appName&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;play-events-etl&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:#a6e22e&#34;&gt;getOrCreate&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&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:#75715e&#34;&gt;// 读：数据湖里昨天的原始播放事件（分区表）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Dataset&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;Row&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; events &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; spark.&lt;span style=&#34;color:#a6e22e&#34;&gt;read&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:#a6e22e&#34;&gt;parquet&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;s3://warehouse/raw/play_events/dt=2026-10-01/&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&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:#75715e&#34;&gt;// 处理：过滤脏数据、按 event_id 去重、换算播放分钟数&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Dataset&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;Row&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; cleaned &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; events
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;filter&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;duration &amp;gt; 0 AND user_id IS NOT NULL&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:#a6e22e&#34;&gt;dropDuplicates&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;event_id&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:#a6e22e&#34;&gt;withColumn&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;play_minutes&amp;#34;&lt;/span&gt;, col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;duration&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;divide&lt;/span&gt;(60));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&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:#75715e&#34;&gt;// join 内容维度表补标题和类目（小表广播，避免 shuffle）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Dataset&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;Row&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; enriched &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; cleaned
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;join&lt;/span&gt;(broadcast(spark.&lt;span style=&#34;color:#a6e22e&#34;&gt;read&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;parquet&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;s3://warehouse/dim/videos/&amp;#34;&lt;/span&gt;)), &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;video_id&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&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:#75715e&#34;&gt;// 聚合出结果并写回数仓（coalesce 控制输出文件数，防止小文件）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;enriched.&lt;span style=&#34;color:#a6e22e&#34;&gt;groupBy&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;video_id&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;category&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:#a6e22e&#34;&gt;agg&lt;/span&gt;(count(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;*&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;alias&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;plays&amp;#34;&lt;/span&gt;), sum(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;play_minutes&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;alias&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;total_minutes&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:#a6e22e&#34;&gt;coalesce&lt;/span&gt;(4)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;write&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:#a6e22e&#34;&gt;mode&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;overwrite&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:#a6e22e&#34;&gt;parquet&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;s3://warehouse/agg/daily_video_plays/dt=2026-10-01/&amp;#34;&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;strong&gt;什么都不会立刻发生&lt;/strong&gt;——filter、join、groupBy 只是在攒 DAG，最后的 &lt;code&gt;write()&lt;/code&gt; 这个动作才触发真正的执行。这就是前面说的惰性求值。&lt;/p&gt;
&lt;p&gt;这段代码里还有一件&amp;quot;看不见&amp;quot;的事值得说破：&lt;strong&gt;分区是自动切的，你全程没有指定过它&lt;/strong&gt;。分区的三个来源：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;读入分区由文件决定&lt;/strong&gt;：读 parquet 时 Spark 按文件切块，&lt;code&gt;spark.sql.files.maxPartitionBytes&lt;/code&gt;（默认 128MB）决定每块多大，每块一个分区。路径里的 &lt;code&gt;dt=2026-10-01/&lt;/code&gt; 本身就是物理分区，Spark 只扫这一天的目录，其余按分区裁剪跳过；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;shuffle 后分区由配置决定&lt;/strong&gt;：&lt;code&gt;groupBy&lt;/code&gt; 触发数据重分发，之后的分区数由 &lt;code&gt;spark.sql.shuffle.partitions&lt;/code&gt; 控制，&lt;strong&gt;默认 200&lt;/strong&gt;——小作业跑完写出 200 个小文件，罪魁祸首就是它；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;手动干预只有两个 API&lt;/strong&gt;：&lt;code&gt;repartition(n)&lt;/code&gt; 重新洗牌（会产生一次 shuffle，贵），&lt;code&gt;coalesce(n)&lt;/code&gt; 只合并不洗牌（便宜）。上面代码写出前的 &lt;code&gt;coalesce(4)&lt;/code&gt; 就是故意把默认的 200 个分区合并成 4 个输出文件。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;那&amp;quot;拉起几十台机器并行计算&amp;quot;体现在哪？答案是：不体现在代码里，体现在物理执行里。&lt;/strong&gt; 同一份代码在一台笔记本上（local 模式）和一百台机器上跑，一行都不用改——分布式是部署特征，不是代码特征。看这段 ETL 提交到集群时实际发生什么：&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-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;spark-submit &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  --class com.example.PlayEventsETL &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  --master yarn &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  --num-executors &lt;span style=&#34;color:#ae81ff&#34;&gt;50&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\ &lt;/span&gt;       &lt;span style=&#34;color:#75715e&#34;&gt;# 拉起 50 个 executor 进程，分布在集群各节点&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  --executor-cores &lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\ &lt;/span&gt;       &lt;span style=&#34;color:#75715e&#34;&gt;# 每个 executor 5 核&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  --executor-memory 20g &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  --driver-memory 8g &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  app.jar&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;这里的 &lt;code&gt;app.jar&lt;/code&gt; 是&lt;strong&gt;你自己的应用代码打包产物&lt;/strong&gt;：前面那段 ETL 的 Java 代码用 Maven/Gradle 编译，打成包含全部依赖的 fat jar（Maven 用 shade 插件、Gradle 用 shadow 插件），&lt;code&gt;--class&lt;/code&gt; 指定里面的入口类。提交时这个 jar 被上传到集群，driver 拿到后把代码分发到各 executor——&lt;strong&gt;executor 上跑的 task，执行的就是这个 jar 里的字节码&lt;/strong&gt;。所以 Spark 作业的交付物就是一个 jar：改代码 = 重新打包重新提交。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;生产环境里谁来&amp;quot;敲&amp;quot;这个 spark-submit？&lt;/strong&gt; 凌晨两点跑批不是值班同学手动敲命令。业界的提交方式按层次分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SparkLauncher&lt;/strong&gt;：Spark 官方自带的 Java API，在 Java 进程里编程式提交作业，还返回句柄可轮询状态——自研平台的标准做法：&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;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-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;SparkAppHandle handle &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; SparkLauncher()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;setAppResource&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;app.jar&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:#a6e22e&#34;&gt;setMainClass&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;com.example.PlayEventsETL&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:#a6e22e&#34;&gt;setMaster&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;yarn&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:#a6e22e&#34;&gt;setConf&lt;/span&gt;(SparkLauncher.&lt;span style=&#34;color:#a6e22e&#34;&gt;EXECUTOR_MEMORY&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;20g&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:#a6e22e&#34;&gt;startApplication&lt;/span&gt;();   &lt;span style=&#34;color:#f92672&#34;&gt;//&lt;/span&gt; 异步返回&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;，&lt;/span&gt;拿 handle 跟踪作业状态&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Apache Livy&lt;/strong&gt;：把提交做成 REST 服务——HTTP POST 一个 jar、轮询状态、拉日志。Web 系统里嵌入 Spark 提交能力的标准件；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调度系统&lt;/strong&gt;：Apache DolphinScheduler、Airflow 这类工作流平台，管的是&amp;quot;凌晨 2 点跑 ETL、成功了再跑模型训练&amp;quot;这种依赖编排，底层调的仍然是 spark-submit 或 Livy；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;云厂商 API&lt;/strong&gt;：EMR Steps、Databricks Jobs 这类托管服务，提交动作变成一个云 API 调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;顺带纠个常见的偏：Apache Commons 系列里没有 Spark 专用提交工具——Commons Exec 只是通用的外部进程包装器，有些自研平台拿它包 spark-submit 命令行（拿退出码、控超时），能用但不是主流。&lt;strong&gt;想在 Java 里提交作业，正路是 SparkLauncher 或 Livy。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;50 个 executor × 5 核 = &lt;strong&gt;250 个任务槽位同时干活，1TB 堆内存的总池子&lt;/strong&gt;。作业跑起来后，逻辑计划被拆成物理 stage，每个分区对应一个 task，task 被调度到空闲槽位上：&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;Stage 1 读取+过滤&amp;lt;br/&amp;gt;2TB parquet 按 128MB 切块&amp;lt;br/&amp;gt;约 16000 个分区 = 16000 个 task&amp;#34;] --&amp;gt; B[&amp;#34;Stage 2 join&amp;lt;br/&amp;gt;维度表广播到每个 executor&amp;lt;br/&amp;gt;无 shuffle&amp;#34;]
    B --&amp;gt; C[&amp;#34;shuffle 重分发&amp;lt;br/&amp;gt;按 video_id 搬运数据&amp;#34;]
    C --&amp;gt; D[&amp;#34;Stage 3 聚合+写盘&amp;lt;br/&amp;gt;200 个 task 各自聚合自己那份&amp;#34;]
    A -.-&amp;gt;|&amp;#34;250 个槽位并发&amp;lt;br/&amp;gt;16000 个 task 分批消化&amp;#34;| A&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;几个数字感：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一天 100 亿条播放事件约 2TB parquet，切成约 16000 个分区，每个分区一个 task——&lt;strong&gt;250 个槽位并发，分批消化完这 16000 个 task&lt;/strong&gt;，这就是&amp;quot;拉起多个节点&amp;quot;的物理形态；&lt;/li&gt;
&lt;li&gt;谁挂了谁重试：某个 executor 宕机，它跑过的 task 在别的节点上按血统重算，作业整体不停；&lt;/li&gt;
&lt;li&gt;想更快就加机器：&lt;code&gt;--num-executors 100&lt;/code&gt;，槽位翻倍，16000 个 task 消化速度就近似翻倍——&lt;strong&gt;这就是水平扩展：代码不变，吞吐随机器数线性涨&lt;/strong&gt;（直到被 shuffle 或存储 IO 卡住）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ALS 训练是同一个道理，只是把&amp;quot;一批 task&amp;quot;换成&amp;quot;几十轮、每轮一批 task&amp;quot;：每一轮迭代都是一次全集群的分布式计算，中间结果留在内存里不落盘——这正是 Spark 相比 MapReduce 快的根源。&lt;/p&gt;
&lt;p&gt;所以读 Spark 代码的正确姿势是：&lt;strong&gt;代码是蓝图，集群按分区把蓝图实体化成成千上万的 task&lt;/strong&gt;。你在代码里看不到&amp;quot;多节点&amp;quot;，正如你在建筑图纸上看不到施工队——但图纸上的每一堵墙，都会变成某个工人在某个脚手架上的具体动作。&lt;/p&gt;
&lt;p&gt;机器学习也一样是 DataFrame 风格的 API。Spark MLlib 里自带 ALS（交替最小二乘，就是矩阵分解），拿交互数据训练推荐模型只要几行：&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-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ALS als &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; ALS()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;setMaxIter&lt;/span&gt;(10)          &lt;span style=&#34;color:#75715e&#34;&gt;// 迭代轮数：交替优化用户矩阵和物品矩阵&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:#a6e22e&#34;&gt;setRegParam&lt;/span&gt;(0.&lt;span style=&#34;color:#a6e22e&#34;&gt;1&lt;/span&gt;)        &lt;span style=&#34;color:#75715e&#34;&gt;// 正则化，防过拟合&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:#a6e22e&#34;&gt;setUserCol&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&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:#a6e22e&#34;&gt;setItemCol&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;video_id&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:#a6e22e&#34;&gt;setRatingCol&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;score&amp;#34;&lt;/span&gt;)   &lt;span style=&#34;color:#75715e&#34;&gt;// 行为强度：完播=3 点赞=2 点击=1&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:#a6e22e&#34;&gt;setColdStartStrategy&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;drop&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ALSModel model &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; als.&lt;span style=&#34;color:#a6e22e&#34;&gt;fit&lt;/span&gt;(playEvents);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 输出用户向量和物品向量，供线上召回使用&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;model.&lt;span style=&#34;color:#a6e22e&#34;&gt;userFactors&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;write&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;parquet&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;s3://warehouse/rec/user_factors/&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;model.&lt;span style=&#34;color:#a6e22e&#34;&gt;itemFactors&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;write&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;parquet&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;s3://warehouse/rec/item_factors/&amp;#34;&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;fit()&lt;/code&gt; 跑完，&lt;code&gt;userFactors&lt;/code&gt; 就是前面推荐架构文章里说的&amp;quot;每个用户一个几百维的向量&amp;quot;——概念和代码在这里接上了。&lt;/p&gt;
&lt;h2 id=&#34;flink-基础流处理血统的计算引擎&#34;&gt;Flink 基础：流处理血统的计算引擎&lt;/h2&gt;
&lt;p&gt;Flink 的出发点反过来：&lt;strong&gt;世界本质是流，批只是流的特例&lt;/strong&gt;（有界流）。数据逐条进来逐条处理，毫秒到秒级延迟。&lt;/p&gt;
&lt;p&gt;四个核心概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;事件时间与水位线（event time &amp;amp; watermark）&lt;/strong&gt;：事件&amp;quot;发生的时间&amp;quot;和&amp;quot;到达系统的时间&amp;quot;经常不同（用户手机离线两小时，数据才补传上来）。Flink 允许按发生时间计算，水位线是&amp;quot;我认为早于 t 的事件基本都到齐了&amp;quot;的声明，晚到超过容忍度的数据进侧输出流。&lt;strong&gt;这是流处理区别于批处理最本质的概念&lt;/strong&gt;——批处理里根本没有&amp;quot;数据晚到&amp;quot;这个问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态（state）&lt;/strong&gt;：算子可以持有状态，比如&amp;quot;最近一小时的点击数&amp;quot;。状态按 key 分布在各并行实例上，大作业可以到 TB 级。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;checkpoint / exactly-once&lt;/strong&gt;：定期给所有算子的状态做全局一致性快照，故障后从快照恢复，保证每条数据恰好被计算一次。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;savepoint&lt;/strong&gt;：手动触发的快照，用于升级和扩缩容——代价是&amp;quot;停作业、改并行度、从快照重启&amp;quot;，这也是之前那篇 &lt;a href=&#34;https://ftmao.com/blog/blog/posts/flink-autoscaling-netflix-two-autoscalers/&#34;&gt;Netflix 给 Flink 做自动扩缩容&lt;/a&gt;那么难的原因。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;上手：实时热榜的 Java 写法&lt;/strong&gt;。场景：统计每个视频最近 10 分钟的播放量，每分钟刷新一次（滑动窗口）：&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-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;StreamExecutionEnvironment env &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; StreamExecutionEnvironment.&lt;span style=&#34;color:#a6e22e&#34;&gt;getExecutionEnvironment&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:#75715e&#34;&gt;// 每 60 秒做一次 checkpoint，故障恢复就回到最近一次快照&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;env.&lt;span style=&#34;color:#a6e22e&#34;&gt;enableCheckpointing&lt;/span&gt;(60_000);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&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:#75715e&#34;&gt;// 数据源：Kafka&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;KafkaSource&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;PlayEvent&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; source &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; KafkaSource.&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;PlayEvent&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt;builder()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;setBootstrapServers&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;kafka:9092&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:#a6e22e&#34;&gt;setTopics&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;play-events&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:#a6e22e&#34;&gt;setGroupId&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;hot-ranking&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:#a6e22e&#34;&gt;setValueOnlyDeserializer&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; PlayEventSchema())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;build&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&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:#75715e&#34;&gt;// 关键一步：声明用事件时间，容忍 5 分钟乱序&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:#75715e&#34;&gt;// 手机离线两小时补传的数据，仍然算回&amp;#34;发生那一刻&amp;#34;的窗口&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;WatermarkStrategy&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;PlayEvent&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; wm &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; WatermarkStrategy
&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;&amp;lt;&lt;/span&gt;PlayEvent&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt;forBoundedOutOfOrderness(Duration.&lt;span style=&#34;color:#a6e22e&#34;&gt;ofMinutes&lt;/span&gt;(5))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;withTimestampAssigner&lt;/span&gt;((event, ts) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; event.&lt;span style=&#34;color:#a6e22e&#34;&gt;getEventTime&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;DataStream&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;PlayEvent&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; events &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; env.&lt;span style=&#34;color:#a6e22e&#34;&gt;fromSource&lt;/span&gt;(source, wm, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;kafka-play-events&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;events
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;keyBy&lt;/span&gt;(PlayEvent::getVideoId)                                  &lt;span style=&#34;color:#75715e&#34;&gt;// 按视频分流&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:#a6e22e&#34;&gt;window&lt;/span&gt;(SlidingEventTimeWindows.&lt;span style=&#34;color:#a6e22e&#34;&gt;of&lt;/span&gt;(Time.&lt;span style=&#34;color:#a6e22e&#34;&gt;minutes&lt;/span&gt;(10), Time.&lt;span style=&#34;color:#a6e22e&#34;&gt;minutes&lt;/span&gt;(1)))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;aggregate&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; PlayCounter())                                  &lt;span style=&#34;color:#75715e&#34;&gt;// 窗口内计数&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:#a6e22e&#34;&gt;addSink&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; RedisSink&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&amp;gt;&lt;/span&gt;());                                   &lt;span style=&#34;color:#f92672&#34;&gt;//&lt;/span&gt; 结果写 Redis&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;，&lt;/span&gt;榜单服务读&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;这段代码里藏着 Flink 思维方式的全部要点：&lt;code&gt;keyBy&lt;/code&gt; 决定状态按什么分发（每个 video_id 一份计数状态），&lt;code&gt;window&lt;/code&gt; 按事件时间划分窗口而不是按到达时间，水位线声明&amp;quot;晚到 5 分钟以内的数据我等你&amp;quot;。&lt;/p&gt;
&lt;p&gt;状态的 TTL 管理（避坑指南第一条）在代码里长这样：&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-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;StateTtlConfig ttl &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; StateTtlConfig.&lt;span style=&#34;color:#a6e22e&#34;&gt;newBuilder&lt;/span&gt;(Time.&lt;span style=&#34;color:#a6e22e&#34;&gt;days&lt;/span&gt;(7))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;setUpdateType&lt;/span&gt;(StateTtlConfig.&lt;span style=&#34;color:#a6e22e&#34;&gt;UpdateType&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;OnCreateAndWrite&lt;/span&gt;)   &lt;span style=&#34;color:#75715e&#34;&gt;// 写入时刷新过期时间&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:#a6e22e&#34;&gt;setStateVisibility&lt;/span&gt;(StateTtlConfig.&lt;span style=&#34;color:#a6e22e&#34;&gt;StateVisibility&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;NeverReturnExpired&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:#a6e22e&#34;&gt;build&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ValueStateDescriptor&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;Long&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; desc &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; ValueStateDescriptor&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&amp;gt;&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;play-count&amp;#34;&lt;/span&gt;, Long.&lt;span style=&#34;color:#a6e22e&#34;&gt;class&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;desc.&lt;span style=&#34;color:#a6e22e&#34;&gt;enableTimeToLive&lt;/span&gt;(ttl);   &lt;span style=&#34;color:#f92672&#34;&gt;//&lt;/span&gt; 状态 7 天不活跃自动清理&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;，&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;strong&gt;部署：对应的启动命令&lt;/strong&gt;。Flink 的交付物同样是一个 fat jar，但启动命令和 Spark 有一个本质区别——&lt;strong&gt;spark-submit 起的作业算完就退出，flink run 起的作业是常驻的&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;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-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 提交常驻流作业到 YARN（跑在 K8s 上则换成 -t kubernetes-application）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;flink run-application &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -t yarn-application &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -c com.example.HotRankingJob &lt;span style=&#34;color:#ae81ff&#34;&gt;\ &lt;/span&gt;     &lt;span style=&#34;color:#75715e&#34;&gt;# main 入口类&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -p &lt;span style=&#34;color:#ae81ff&#34;&gt;64&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\ &lt;/span&gt;                            &lt;span style=&#34;color:#75715e&#34;&gt;# 全局并行度：占用 64 个 slot&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  hot-ranking.jar
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&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:#75715e&#34;&gt;# 升级代码或改并行度的标准动作：先打 savepoint，再从快照恢复&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;flink stop &amp;lt;jobId&amp;gt; --savepointPath s3://checkpoints/hot-ranking/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;flink run-application &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -t yarn-application &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -c com.example.HotRankingJob &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -s s3://checkpoints/hot-ranking/savepoint-abc123 &lt;span style=&#34;color:#ae81ff&#34;&gt;\ &lt;/span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;# 从这份状态恢复&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -p &lt;span style=&#34;color:#ae81ff&#34;&gt;128&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\ &lt;/span&gt;                                            &lt;span style=&#34;color:#75715e&#34;&gt;# 并行度可以顺便改&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  hot-ranking.jar&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;对照着看两套命令的分工：Spark 用 &lt;code&gt;--num-executors / --executor-cores&lt;/code&gt; 申请资源（因为要装下一批又一批的 task）；Flink 用 &lt;code&gt;-p&lt;/code&gt; 声明并行度（因为作业结构固定，并行度就是常驻 worker 数）。而 &lt;code&gt;-s&lt;/code&gt; 恢复 savepoint 这个参数是 Flink 独有的——Spark 批作业没有&amp;quot;带着状态继续跑&amp;quot;的概念，它每次都是从头算一批新的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;生产环境谁来提交 Flink 作业？&lt;/strong&gt; 和 Spark 不完全对称——Flink 没有一个像 SparkLauncher 那样常用的官方 Java 提交 API，业界的答案按部署形态分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Flink Kubernetes Operator（当前主流）&lt;/strong&gt;：作业定义成一份 &lt;code&gt;FlinkDeployment&lt;/code&gt; YAML（K8s 自定义资源），&lt;code&gt;kubectl apply&lt;/code&gt; 即提交。Operator 托管整个生命周期：拉起、监控、&lt;strong&gt;升级时自动做 savepoint 并从快照恢复&lt;/strong&gt;——上面手动敲的 &lt;code&gt;flink stop --savepointPath&lt;/code&gt; 那套动作，它全自动；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Apache StreamPark（原 StreamX）&lt;/strong&gt;：作业管理平台，Web 界面管理 jar 上传、配置、启停和 savepoint，国内团队用得多；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ververica Platform&lt;/strong&gt;：Flink 创始团队公司的平台产品（有社区版）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JobManager REST API&lt;/strong&gt;：直接 HTTP 上传 jar 再触发 run——Flink Web UI 的提交走的就是它，自研平台想自己包一层时用；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;YARN 模式&lt;/strong&gt;：就是上面那条 &lt;code&gt;flink run-application&lt;/code&gt; 命令，CLI 直连，老集群常见。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;趋势很明确：&lt;strong&gt;K8s Operator 是官方主推方向&lt;/strong&gt;，新平台直接从这里起步，别再自建提交流程。&lt;/p&gt;
&lt;h2 id=&#34;血统对比一张表&#34;&gt;血统对比：一张表&lt;/h2&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;Spark&lt;/th&gt;
					&lt;th&gt;Flink&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&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;tr&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;状态管理&lt;/td&gt;
					&lt;td&gt;弱（流场景下才需要，能力有限）&lt;/td&gt;
					&lt;td&gt;一等公民（keyed state、TB 级）&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;tr&gt;
					&lt;td&gt;生态主场&lt;/td&gt;
					&lt;td&gt;数仓 SQL、MLlib、Python 数据科学&lt;/td&gt;
					&lt;td&gt;实时管道、实时数仓、CEP&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;思维负担&lt;/td&gt;
					&lt;td&gt;低：写 SQL / DataFrame 像写单机代码&lt;/td&gt;
					&lt;td&gt;高：要时刻想着时间、状态、背压&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;一句话记忆：&lt;strong&gt;Spark 把流当批做，Flink 把批当流做；前者简单但延迟高，后者精确但心智负担重。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;视频公司里各干什么活&#34;&gt;视频公司里各干什么活&lt;/h2&gt;
&lt;p&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
    A[&amp;#34;用户行为事件&amp;lt;br/&amp;gt;播放 暂停 点赞 卡顿&amp;#34;] --&amp;gt; B[&amp;#34;Kafka 事件总线&amp;#34;]
    B --&amp;gt; C[&amp;#34;Flink 实时链路&amp;lt;br/&amp;gt;秒级生效&amp;#34;]
    B --&amp;gt; D[&amp;#34;落地数仓&amp;lt;br/&amp;gt;Iceberg 数据湖&amp;#34;]
    D --&amp;gt; E[&amp;#34;Spark 离线链路&amp;lt;br/&amp;gt;小时级 天级&amp;#34;]
    C --&amp;gt; F[&amp;#34;在线服务&amp;lt;br/&amp;gt;画像 热榜 监控告警&amp;#34;]
    E --&amp;gt; G[&amp;#34;模型 报表 实验分析&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;Flink 的活——&amp;ldquo;这件事现在就要知道&amp;rdquo;：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;实时画像更新&lt;/strong&gt;：用户每看一条视频，行为事件进 Kafka，Flink 作业滚动更新&amp;quot;最近 N 次行为的加权兴趣向量&amp;quot;，秒级生效。下一刷推荐就变。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实时热榜&lt;/strong&gt;：按事件时间滑动窗口统计各视频的播放增速，每分钟刷新 Trending 榜单。注意必须用事件时间——用户手机离线补传的数据不能算到&amp;quot;现在&amp;quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;播放质量监控（QoS）&lt;/strong&gt;：卡顿率、首帧时间、播放失败率按地区/运营商/客户端版本实时聚合，超过阈值就告警。某运营商骨干网抽风，值班同学三分钟内就该收到电话。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实时风控&lt;/strong&gt;：同一设备十分钟内播放 200 个视频？异常刷量实时拦截，不等第二天对账。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Spark 的活——&amp;ldquo;这件事今天算完就行&amp;rdquo;：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数仓 ETL&lt;/strong&gt;：Kafka 落地到 Iceberg 的原始日志，Spark 按天清洗、去重、补维度（join 用户表、内容表），产出标准明细表。全公司数据分析的地基。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推荐模型训练&lt;/strong&gt;：拿过去 30 天的用户-内容交互矩阵，训练排序模型（交互矩阵怎么变成用户偏好，详见&lt;a href=&#34;https://ftmao.com/blog/blog/posts/personalized-infinite-feed-architecture/&#34;&gt;推荐 Feed 架构&lt;/a&gt;里&amp;quot;用户偏好怎么算出来&amp;quot;一节）。训练要扫数据几十遍，正是 Spark 内存迭代的甜点场景。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;经营报表&lt;/strong&gt;：每日播放时长、新增会员、留存率——管理层早上九点要看的看板，Spark 凌晨跑批产出。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A/B 实验分析&lt;/strong&gt;：实验组对照组的指标对比、显著性检验，数据科学家写 Spark SQL + Python 完成。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;同一条数据的两次旅程&lt;/strong&gt;：用户晚上 8 点点开一条视频——这个事件两秒内被 Flink 消费，画像更新、热榜计数、质量监控全部生效；凌晨 2 点，同一个事件（已从 Kafka 落盘）被 Spark 的 ETL 作业清洗进数仓，早上 6 点进入模型训练数据。&lt;strong&gt;实时链路管&amp;quot;现在&amp;quot;，离线链路管&amp;quot;沉淀&amp;quot;，谁也别抢谁的活。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;算力规格硬件怎么配&#34;&gt;算力规格：硬件怎么配&lt;/h2&gt;
&lt;p&gt;先给结论再讲推理：&lt;strong&gt;大数据集群的钱主要花在内存和 SSD 上，CPU 反而不是瓶颈的主角&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spark 节点的典型规格&lt;/strong&gt;（按 2026 年的行情）：&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;Worker 节点&lt;/td&gt;
					&lt;td&gt;32-64 核 CPU、128-256GB 内存、4-8 块 NVMe SSD 做本地 shuffle 盘、10-25Gbps 网卡&lt;/td&gt;
					&lt;td&gt;shuffle 数据写本地盘，SSD 比内存更影响大 join 的稳定性&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Executor 切分&lt;/td&gt;
					&lt;td&gt;每个 executor 4-5 核、16-24GB 堆内存&lt;/td&gt;
					&lt;td&gt;一台 32 核 128GB 的机器切 5-6 个 executor；executor 太大 GC 停顿长，太小则 broadcast 等开销被重复多份&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Driver&lt;/td&gt;
					&lt;td&gt;8-16GB 内存起步&lt;/td&gt;
					&lt;td&gt;调度 + 收集元数据；大 job 的执行计划、broadcast 变量都压在这里&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Flink 节点的典型规格&lt;/strong&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;TaskManager&lt;/td&gt;
					&lt;td&gt;8-16 核、32-64GB 内存、&lt;strong&gt;本地 NVMe SSD 必须有&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;大状态用 RocksDB state backend，状态落在本地盘，SSD 的随机读写直接决定状态访问性能&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;slot 划分&lt;/td&gt;
					&lt;td&gt;slot 数 = CPU 核数（1:1 起步）&lt;/td&gt;
					&lt;td&gt;并行度不能超过总 slot 数；一个 slot 一个线程，避免 CPU 争抢&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;JobManager&lt;/td&gt;
					&lt;td&gt;4 核 8GB 起步，HA 模式至少两个&lt;/td&gt;
					&lt;td&gt;协调 checkpoint，不搬数据，轻&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;checkpoint 存储&lt;/td&gt;
					&lt;td&gt;S3 / HDFS / 对象存储&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;，把数字走一遍：假设行为事件峰值 10 万条/秒、单条 1KB，实时链路（画像 + 热榜 + 监控）合计维护 500GB 状态。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;网络：10 万 × 1KB = 100MB/s，25Gbps 网卡毫无压力——&lt;strong&gt;数据管道的瓶颈很少是网络&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;状态：按单个 TaskManager 管理 64-128GB RocksDB 状态比较稳妥的经验值，500GB 状态需要 4-8 台 TaskManager；&lt;/li&gt;
&lt;li&gt;CPU：窗口聚合类作业每条事件的计算很轻，10 万条/秒摊到 64 个核上每核每秒 1500 条，富余；&lt;/li&gt;
&lt;li&gt;所以这套链路 8 台&amp;quot;16 核 64GB + NVMe&amp;quot;的机器跑得很从容，再留 30% 峰值冗余即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;反过来，凌晨的 Spark 训练作业要扫 30 天 × 百 TB 级日志，它的瓶颈是 &lt;strong&gt;shuffle 吞吐和内存&lt;/strong&gt;，机器数量按&amp;quot;凌晨 6 点前必须跑完&amp;quot;倒推——离线集群的规格是被截止时间定义的。&lt;/p&gt;
&lt;h2 id=&#34;监控两套引擎各看什么&#34;&gt;监控：两套引擎各看什么&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Spark 监控的主入口是 Spark UI&lt;/strong&gt;（每个作业一个 Web 界面），配合 Prometheus 抓指标。值班时按这个顺序看：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stages 页&lt;/strong&gt;：哪个 stage 慢、shuffle read/write 多大——shuffle spill（内存放不下溢写磁盘）是性能第一警报；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tasks 页的任务时长分布&lt;/strong&gt;：p50 是 2 分钟、max 是 40 分钟 → 数据倾斜，不用怀疑；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Executors 页&lt;/strong&gt;：GC 时间占比超过 10% 说明内存配置或数据结构有问题；executor lost 频繁出现说明节点或内存超卖；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Event Log&lt;/strong&gt; 持久化到 HDFS/S3，作业挂了也能回放分析——务必开启，默认不一定开。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Flink 监控的主入口是 Web UI + 指标上报&lt;/strong&gt;（Prometheus reporter），核心看四组：&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;code&gt;backPressuredTimeMsPerSecond&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;哪个算子被顶住&lt;/td&gt;
					&lt;td&gt;持续 &amp;gt;50% 就要查&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;checkpoint &lt;code&gt;lastCheckpointDuration&lt;/code&gt; / &lt;code&gt;lastCheckpointSize&lt;/code&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;Kafka 消费 lag（&lt;code&gt;records-lag&lt;/code&gt;）&lt;/td&gt;
					&lt;td&gt;消费追不上生产 = 延迟在堆积&lt;/td&gt;
					&lt;td&gt;持续增长 10 分钟告警&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;&lt;strong&gt;告警的验收标准只有一条：每个告警都必须对应一个动作。&lt;/strong&gt;&amp;ldquo;CPU 高&amp;quot;不是告警，&amp;ldquo;热榜作业 Kafka lag 连续增长 10 分钟&amp;quot;才是——前者看了也不知道干嘛，后者直接指向扩容或查背压。&lt;/p&gt;
&lt;h2 id=&#34;故障与恢复出事时的标准动作&#34;&gt;故障与恢复：出事时的标准动作&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Spark 的故障模型是&amp;quot;重算&amp;rdquo;&lt;/strong&gt;，血统里带着 MapReduce 的基因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;task 失败 → 自动重试（默认 4 次，&lt;code&gt;spark.task.maxFailures&lt;/code&gt; 可调），重试时只重算这个分区；&lt;/li&gt;
&lt;li&gt;executor 挂了 → 它算过的分区&lt;strong&gt;按 lineage（血统）重新算&lt;/strong&gt;——RDD 天生记得自己从哪算来的，这就是当年 RDD 论文的核心思想；&lt;/li&gt;
&lt;li&gt;个别 task 特别慢（straggler）→ 开推测执行（&lt;code&gt;spark.speculation=true&lt;/code&gt;），同样的任务在别的节点再跑一份，谁先完用谁；&lt;/li&gt;
&lt;li&gt;driver 挂了 → 最麻烦，批作业整体重跑；所以重要的 ETL 要幂等（写分区用 overwrite 而不是 append），重跑不出脏数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Flink 的故障模型是&amp;quot;回滚&amp;rdquo;&lt;/strong&gt;，回到最近一次 checkpoint：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TaskManager 挂了 → 作业自动从最近的成功 checkpoint 恢复，Kafka source 回到快照时记录的 offset 重新消费，配合两阶段提交的 sink 保证恰好一次；&lt;/li&gt;
&lt;li&gt;重启策略别用默认：生产上推荐 &lt;code&gt;failure-rate&lt;/code&gt;（10 分钟内允许失败 N 次，超过才放弃）配合重启间隔，防止依赖的 Kafka/Redis 抖动时作业无脑高频重启把对方打垮；&lt;/li&gt;
&lt;li&gt;JobManager 挂 → 必须配 HA（ZooKeeper 或 K8s 选主），否则协调者单点；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;checkpoint 是自动的、savepoint 是手动的&lt;/strong&gt;：代码升级、改并行度必须先做 savepoint 再停作业，从 savepoint 启动新版本。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;一个标准故障处置剧本&lt;/strong&gt;（实时画像更新作业 lag 暴涨）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;看 Flink UI 背压页——找到链条上第一个变红的算子；&lt;/li&gt;
&lt;li&gt;是某个下游慢（比如 Redis 抖动）→ 修下游，作业自己追上来；&lt;/li&gt;
&lt;li&gt;是数据洪峰 → 临时调大并行度（savepoint → 重启 → 恢复），或者干脆等洪峰过去，Kafka 里的数据丢不了；&lt;/li&gt;
&lt;li&gt;恢复后盯 Kafka lag 回落到正常水位，确认 checkpoint 恢复绿色；&lt;/li&gt;
&lt;li&gt;事后复盘：这次故障该变成哪条告警规则？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注意底气来自哪里：&lt;strong&gt;Kafka 保留了原始事件，Flink 保留了状态快照&lt;/strong&gt;——前者保证&amp;quot;数据丢不了&amp;quot;，后者保证&amp;quot;进度丢不了&amp;quot;。两个都在，故障就只是插曲不是事故。&lt;/p&gt;
&lt;h3 id=&#34;kafka--flink-组合失败后到底会不会重复会不会丢&#34;&gt;Kafka + Flink 组合：失败后到底会不会重复、会不会丢&lt;/h3&gt;
&lt;p&gt;这是面试和值班都绕不开的问题，拆开两个角色看就清楚了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kafka 侧：消息几乎不可能&amp;quot;被消费掉&amp;quot;。&lt;/strong&gt; Kafka 是持久化日志，消费&lt;strong&gt;不删除&lt;/strong&gt;数据，保留期（比如 7 天）内随便重读。Flink 的 Kafka source 消费时不逐条 ack，只在 checkpoint 里记录各分区的 offset。所以消息丢失只有两种可能：保留期过了，或者 Kafka 集群自己故障。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Flink 侧：重启 = 状态回滚 + offset 回拨，两件事同步发生。&lt;/strong&gt; 故障后从最近一次成功的 checkpoint 恢复，同一时刻两个动作：&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;故障发生&amp;lt;br/&amp;gt;部分事件已消费已计算&amp;#34;] --&amp;gt; B[&amp;#34;从最近 checkpoint 恢复&amp;#34;]
    B --&amp;gt; C[&amp;#34;Kafka offset 回拨&amp;lt;br/&amp;gt;到快照时刻&amp;#34;]
    B --&amp;gt; D[&amp;#34;算子状态回滚&amp;lt;br/&amp;gt;到快照时刻&amp;#34;]
    C --&amp;gt; E[&amp;#34;快照之后的事件重新消费&amp;#34;]
    D --&amp;gt; E
    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;假设 checkpoint 间隔 60 秒：故障意味着&lt;strong&gt;最多重放最近 60 秒的事件&lt;/strong&gt;——重放不是丢失，是进度倒退后追赶。由于状态和 offset 同步回滚，窗口计数这类状态计算在内部是精确的（exactly-once）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但&amp;quot;对外不重复&amp;quot;取决于 sink，这是最容易踩的边界：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;sink 类型&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;两阶段提交 sink&lt;/td&gt;
					&lt;td&gt;exactly-once&lt;/td&gt;
					&lt;td&gt;Kafka sink（事务）、Iceberg&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;幂等写入&lt;/td&gt;
					&lt;td&gt;等效 exactly-once&lt;/td&gt;
					&lt;td&gt;Redis 按 key 覆盖写、MySQL upsert&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;普通追加&lt;/td&gt;
					&lt;td&gt;at-least-once，&lt;strong&gt;会重复&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;直接 insert 进 MySQL、对外发 HTTP 回调&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;重放的那 60 秒里，如果 sink 是普通 insert，下游表里就会出现重复行——作业内部算得再精确，写出去的那一刻语义就降级了。这就是&amp;quot;exactly-once 只在链路两端都配合时才成立&amp;quot;的含义。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;真正会丢、会重的边界情况清单：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;没开 checkpoint&lt;/strong&gt;：offset 只能周期性提交到 Kafka，重启从提交点读——退化成 at-least-once，且恢复语义模糊；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kafka 保留期 &amp;lt; 故障停机时间&lt;/strong&gt;：作业停 8 天、Kafka 只留 7 天，恢复时最早的数据真没了；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步 IO 和侧输出不参与 checkpoint&lt;/strong&gt;：算子里偷偷调外部接口、写侧输出流，这些动作不在快照里，重放时会重复执行；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;savepoint 与升级不兼容&lt;/strong&gt;：改了状态结构（比如给状态类加了字段）还想从旧 savepoint 恢复，直接恢复失败——升级前想清楚状态兼容性。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;多节点会不会重复消费同一个分区消费组的互斥语义&#34;&gt;多节点会不会重复消费同一个分区：消费组的互斥语义&lt;/h3&gt;
&lt;p&gt;顺带的另一个常见疑问：Flink 作业有几十个 source subtask 在多个节点上跑，会不会两个节点同时读同一个 Kafka 分区、把数据算两遍？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不会，这是 Kafka 消费组协议内建的互斥保证，不需要你做任何事：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同一作业的所有 source subtask 用同一个 &lt;code&gt;group.id&lt;/code&gt; 注册，Kafka 的组协调器把分区&lt;strong&gt;互斥分配&lt;/strong&gt;——每个分区同一时刻只属于组内一个消费者。200 个分区、64 个 subtask，每个 subtask 分 3-4 个，互不重叠；&lt;/li&gt;
&lt;li&gt;节点挂了触发 rebalance，分区重新分配，但交接是&lt;strong&gt;排他的&lt;/strong&gt;：旧持有者被踢出组之后，新持有者才接手，不存在两个节点同时持有同一分区的窗口；&lt;/li&gt;
&lt;li&gt;所以同一作业内&amp;quot;重复消费&amp;quot;只有一种情况：故障恢复时的&lt;strong&gt;时间维度重放&lt;/strong&gt;（上一节讲的回滚），不存在&amp;quot;空间维度&amp;quot;的并行重复。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两个相关的配置纪律：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;source 并行度不要超过分区数&lt;/strong&gt;。并行度 64、分区只有 32 个时，32 个 subtask 永远分不到分区、空转白占资源。反过来分区数多于并行度没问题，一个 subtask 可以吃多个分区。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不同作业必须用不同的 &lt;code&gt;group.id&lt;/code&gt;&lt;/strong&gt;。消费组之间是广播语义——画像作业和热榜作业各自完整消费同一个 topic，互不干扰，这是特性。但两个本该独立的作业如果误配成同一个 group.id，就会&lt;strong&gt;瓜分分区、各自只读到一半数据&lt;/strong&gt;，这种 bug 症状隐蔽（数据看着都在流，就是量不对），值得配进上线检查清单。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;数据源不止-kafka还能从哪读api-数据怎么办&#34;&gt;数据源不止 Kafka：还能从哪读，API 数据怎么办&lt;/h3&gt;
&lt;p&gt;Flink 的 source 生态按类别列全：&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;Kafka、Pulsar、Kinesis、RocketMQ（社区）&lt;/td&gt;
					&lt;td&gt;实时事件流的主力入口&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;数据库 CDC&lt;/td&gt;
					&lt;td&gt;MySQL CDC、PostgreSQL CDC、MongoDB CDC（Flink CDC 项目）&lt;/td&gt;
					&lt;td&gt;直接订阅 binlog，数据库变更实时变成流——替代&amp;quot;定时全量导库&amp;quot;，是这几年增量最大的方向&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;文件/对象存储&lt;/td&gt;
					&lt;td&gt;FileSource（S3 / HDFS / 本地），支持批读和持续监听目录&lt;/td&gt;
					&lt;td&gt;批处理、日志文件实时采集&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;JDBC&lt;/td&gt;
					&lt;td&gt;Table API 的 JDBC connector&lt;/td&gt;
					&lt;td&gt;轮询式读关系库，伪实时&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;测试&lt;/td&gt;
					&lt;td&gt;DataGen 数据生成器、socket 流&lt;/td&gt;
					&lt;td&gt;本地开发与压测&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;HTTP API 提供的数据怎么办？&lt;/strong&gt; 核心发行版里没有内置的 HTTP source，分两种情况：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;情况一：API 是数据源（想持续消费一个 REST 接口）&lt;/strong&gt;。正确姿势不是让 Flink 直接轮询 API，而是&lt;strong&gt;中间加一层 Kafka&lt;/strong&gt;：一个轻量采集服务定时调 API、把结果写进 Kafka，Flink 消费 Kafka。原因正是前两节的内容——Kafka 提供可回放、削峰、多消费者，API 直连则什么都没有：Flink 作业一挂，这段时间的 API 数据就永远丢了。非要直连也行：实现自定义 Source 接口自己轮询，但 offset 管理、故障恢复语义全得自己做，一般不值。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;情况二：API 是维表（事件流经过时补充字段）&lt;/strong&gt;。这是更常见的需求：每条事件只有 &lt;code&gt;video_id&lt;/code&gt;，需要调内容服务拿标题、类目、时长。Flink 有标准件——&lt;strong&gt;异步 IO（Async I/O）&lt;/strong&gt;：在算子里并发调用 HTTP 接口、回调组装结果，不阻塞主流；Table API 里对应的写法是 lookup join。配一层本地缓存（Guava/Caffeine）挡掉热点 key 的重复调用，避免把对方服务打挂。注意它的语义是&amp;quot;查询&amp;quot;不是&amp;quot;消费&amp;quot;——只在事件流经过时被动调用，永远不会主动拉数据。&lt;/p&gt;
&lt;h2 id=&#34;spark-避坑指南&#34;&gt;Spark 避坑指南&lt;/h2&gt;
&lt;p&gt;按踩坑频率排序：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;数据倾斜是第一杀手&lt;/strong&gt;。某个 key 的数据量特别大（比如头部视频占了 10% 播放量），groupBy/join 时对应分区要处理别人几十倍的数据，99% 的任务十分钟跑完，最后 1% 卡两小时。解法：给热点 key 加盐（key 后面拼随机后缀打散）、先聚合再关联、小表广播（broadcast join）避免 shuffle。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;小文件问题&lt;/strong&gt;。ETL 作业输出几万个几 KB 的小文件，下游读取慢、元数据服务被打爆。解法：写出前 &lt;code&gt;coalesce&lt;/code&gt;/&lt;code&gt;repartition&lt;/code&gt; 控制分区数，或开自适应合并。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;shuffle 爆炸&lt;/strong&gt;。join 两张大表，中间数据量是笛卡尔级增长。先看能不能把一张表过滤小再 join，能 broadcast 就 broadcast。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;滥用 cache&lt;/strong&gt;。&lt;code&gt;.cache()&lt;/code&gt; 不是免费的，内存不够会挤占执行内存反而更慢。只缓存&amp;quot;会被复用多次&amp;quot;的中间结果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分区数拍脑袋&lt;/strong&gt;。分区太少 → 单任务处理量太大 OOM；太多 → 任务调度开销压垮 driver。经验值：单个分区处理 128MB 上下。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;driver OOM&lt;/strong&gt;。&lt;code&gt;collect()&lt;/code&gt; 把全量数据拉回 driver——生产事故的经典配方。要看几行数据用 &lt;code&gt;show()&lt;/code&gt;/&lt;code&gt;take()&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;flink-避坑指南&#34;&gt;Flink 避坑指南&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;状态不设 TTL 无限膨胀&lt;/strong&gt;。keyed state 默认永不过期，跑一个月状态涨到 TB 级，checkpoint 从秒级拖到分钟级最后超时失败。用户维度状态几乎总该设 TTL（比如 7 天）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;checkpoint 间隔拍太短&lt;/strong&gt;。间隔小于一次 checkpoint 本身的耗时，系统永远在做快照没有空算数据。先看一次 checkpoint 要多久，间隔至少留它几倍。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;背压不会看&lt;/strong&gt;。作业 lag 持续上涨，先去 UI 看背压面板：红色背压的算子就是瓶颈，瓶颈的上游全会被&amp;quot;传染&amp;quot;成红色——&lt;strong&gt;找到链条上第一个变红的，那才是真正的病灶&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;事件时间和处理时间混用&lt;/strong&gt;。用处理时间算&amp;quot;每分钟播放量&amp;quot;，用户手机补传的数据会把昨天的量记到今天。涉及&amp;quot;按发生时间统计&amp;quot;的需求，必须事件时间 + 水位线，并接受乱序容忍度的权衡。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;扩缩容当成无状态服务做&lt;/strong&gt;。改并行度 = savepoint + 停作业 + 重启，大状态作业分钟级中断。扩缩容策略要保守，这个坑的完整故事见 &lt;a href=&#34;https://ftmao.com/blog/blog/posts/flink-autoscaling-netflix-two-autoscalers/&#34;&gt;Netflix 两代 autoscaler&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;exactly-once 当默认值&lt;/strong&gt;。端到端 exactly-once 要求 sink 支持两阶段提交，代价是吞吐下降。很多监控类场景 at-least-once + 下游幂等就够了，别为不需要的语义付费。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;通用坑：用 Spark 微批硬扛秒级需求&lt;/strong&gt;。需求方说&amp;quot;准实时&amp;quot;，你拿 Structured Streaming 把触发间隔压到秒级——能跑，但状态管理和乱序处理的坑会一个个冒出来。先问清楚：延迟到底是秒级还是分钟级需求？分钟级 Spark 很舒服，秒级请直接上 Flink，别硬扛。&lt;/p&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&gt;数据是有界的、延迟容忍分钟级以上 → Spark
数据是持续流、延迟要求秒级、或需要复杂状态/事件时间 → Flink&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;但必须诚实：&lt;strong&gt;边界在模糊化&lt;/strong&gt;。Spark 能做流、Flink 能做批，两个社区都在往对方的领地走；Flink 的 Table API 写 SQL 也越来越像 Spark SQL。真实的选型经常是这样决定的：团队现有栈是什么、招聘市场上谁会、云厂商托管服务谁更便宜。技术血统决定的是&amp;quot;哪个更顺&amp;quot;，不是&amp;quot;哪个能做&amp;quot;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;国内的实际格局&lt;/strong&gt;（2020 年代中期的现状，供校准）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;新上实时项目基本是 Flink&lt;/strong&gt;。阿里 2019 年收购 Flink 母公司后全力主推（内部版 Blink 撑双 11 实时大屏），字节、美团、腾讯、快手的实时平台全部 Flink 化，新立项用 Spark Streaming 的已很难见到；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存量离线仍是 Spark 的天下&lt;/strong&gt;：数仓 ETL、T+1 报表、Hive 承接，生态太厚，替换动力不足；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;两边都在被挤压&lt;/strong&gt;：Spark 的机器学习场景在流失到 Python 原生生态（PyTorch/Ray）；Flink 在进攻离线——&amp;ldquo;流批一体&amp;rdquo;（一套 SQL 跑流也跑批）配合 Paimon 这类流式湖格式在落地，但离线存量太重，替换以年计；&lt;/li&gt;
&lt;li&gt;一句话：&lt;strong&gt;新实时 = Flink，存量离线 = Spark，交界处（实时数仓）在打架&lt;/strong&gt;。求职市场同构：实时岗位 Flink 是硬通货，离线岗位 Spark/Hive 是基础盘。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;一句话收拢：&lt;strong&gt;Spark 回答&amp;quot;这批数据算出来是什么&amp;quot;，Flink 回答&amp;quot;这件事现在发生了什么&amp;quot;&lt;/strong&gt;；先想清楚你的问题是哪一种，再选工具——反过来拿着工具定义问题，是大数据平台翻车的最常见起点。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;参考：Apache Spark 与 Apache Flink 官方文档；Structured Streaming 与 Flink DataStream 的设计文档。Netflix 场景参考其技术博客公开的数据平台架构。&lt;/em&gt;&lt;/p&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>
    
  </channel>
</rss>
