<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Flink on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/tags/flink/</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/flink/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>三万个 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>
    
  </channel>
</rss>
