<?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/%E5%A4%A7%E6%95%B0%E6%8D%AE/</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/%E5%A4%A7%E6%95%B0%E6%8D%AE/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>Spark 从入门到精通：架构原理、RDD/DataFrame、调优踩坑与电商实战全指南</title>
      <link>https://ftmao.com/blog/posts/spark-beginner-to-mastery/</link>
      <pubDate>Wed, 26 Oct 2022 09:30:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/spark-beginner-to-mastery/</guid>
      <description>&lt;h1 id=&#34;spark-从入门到精通架构原理rdddataframe调优踩坑与电商实战全指南&#34;&gt;Spark 从入门到精通：架构原理、RDD/DataFrame、调优踩坑与电商实战全指南&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;目标读者：写过一两年后端（Java/Scala/Python 皆可）、第一次要碰大数据的同学；或者用 PySpark 写过一些脚本、但一遇到性能问题就抓瞎的同学。
写这篇文章时（2022 年 10 月），Spark 3.3 刚发布，DataFrame 早已是绝对主流，Flink 在实时方向上攻城略地。但离线批处理、数仓 ETL、特征工程这些重活，Spark 依然是绕不开的那座山。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&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 LR
    A[&amp;#34;一 为什么是 Spark&amp;lt;br/&amp;gt;从单机到分布式&amp;#34;] --&amp;gt; B[&amp;#34;二 架构与原理&amp;lt;br/&amp;gt;Driver Executor DAG&amp;#34;]
    B --&amp;gt; C[&amp;#34;三 RDD 与 DataFrame&amp;lt;br/&amp;gt;两代 API 的分水岭&amp;#34;]
    C --&amp;gt; D[&amp;#34;四 电商实战&amp;lt;br/&amp;gt;宽表 漏斗 特征&amp;#34;]
    D --&amp;gt; E[&amp;#34;五 调优与踩坑&amp;lt;br/&amp;gt;倾斜 OOM 小文件&amp;#34;]
    E --&amp;gt; F[&amp;#34;六 AI 时代&amp;lt;br/&amp;gt;少写代码 多写提示词&amp;#34;]
    style F fill:#e8f4e8&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;为什么是这个顺序？第一篇解决「&lt;strong&gt;为什么要用它&lt;/strong&gt;」；第二篇解决「&lt;strong&gt;它在集群里怎么跑&lt;/strong&gt;」——不理解这个，写出来的代码就是玄学；第三篇解决「&lt;strong&gt;代码怎么写&lt;/strong&gt;」；第四篇用真实业务把知识串起来；第五篇是血泪史，生产环境和本地 Demo 是两个物种；第六篇聊怎么让 AI 替你干活。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第一篇-为什么是-spark一台机器装不下的时代&#34;&gt;第一篇 为什么是 Spark：一台机器装不下的时代&lt;/h2&gt;
&lt;h3 id=&#34;11-一个电商场景把问题引出来&#34;&gt;1.1 一个电商场景把问题引出来&lt;/h3&gt;
&lt;p&gt;假设你在一家中型电商公司做后端。双十一刚过，老板要看一份报表：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;昨天所有下单用户，按城市分组，统计成交额、下单次数、客单价，再看这批用户里有多少是 30 天内的新客。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;订单表 8 亿行，用户表 4000 万行，行为日志（埋点）一天 20 亿条。MySQL 里跑这个 SQL？&lt;code&gt;GROUP BY&lt;/code&gt; 一个 8 亿行的表，跑一晚上 MySQL 先把你杀了——不，是 DBA 先把你杀了。&lt;/p&gt;
&lt;p&gt;你想到的办法是「把数据导出来，写个 Python 脚本跑」。8 亿行订单数据，CSV 导出大概 400GB。你的开发机内存 32GB。&lt;strong&gt;装不下，这就是大数据的全部问题。&lt;/strong&gt;&lt;/p&gt;
&lt;p&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 TB
    subgraph 思路一[&amp;#34;思路一：垂直扩展（Stack Overflow 式的天真）&amp;#34;]
        A1[&amp;#34;换个 1TB 内存的机器&amp;#34;] --&amp;gt; A2[&amp;#34;贵得离谱&amp;lt;br/&amp;gt;且有物理上限&amp;#34;]
    end
    subgraph 思路二[&amp;#34;思路二：水平扩展（唯一出路）&amp;#34;]
        B1[&amp;#34;把 400GB 切成 1000 份&amp;#34;] --&amp;gt; B2[&amp;#34;1000 台机器&amp;lt;br/&amp;gt;每台处理 400MB&amp;#34;]
        B2 --&amp;gt; B3[&amp;#34;各自算局部结果&amp;lt;br/&amp;gt;最后合并&amp;#34;]
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;思路二就是分布式计算。但自己写 1000 台机器的协调代码？分发任务、处理机器挂掉、合并结果、处理网络超时……你会写出第二个 Hadoop。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spark 就是替你把这些脏活全包了&lt;/strong&gt;：你只写「对这批数据做什么变换」，它负责切分、调度、容错、合并。&lt;/p&gt;
&lt;h3 id=&#34;12-spark-在生态里的位置&#34;&gt;1.2 Spark 在生态里的位置&lt;/h3&gt;
&lt;p&gt;新人最容易被一堆名词搞晕：Hadoop、HDFS、YARN、Hive、Spark、Flink。一张图说清楚：&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 TB
    subgraph 存储层
        HDFS[&amp;#34;HDFS&amp;lt;br/&amp;gt;分布式文件系统&amp;lt;br/&amp;gt;（把文件切块存到多台机器）&amp;#34;]
        S3[&amp;#34;S3 / OSS&amp;lt;br/&amp;gt;对象存储&amp;#34;]
    end
    subgraph 资源层
        YARN[&amp;#34;YARN / K8s&amp;lt;br/&amp;gt;集群资源调度&amp;lt;br/&amp;gt;（给任务分配 CPU 和内存）&amp;#34;]
    end
    subgraph 计算层
        MR[&amp;#34;MapReduce&amp;lt;br/&amp;gt;老一辈 磁盘IO 慢&amp;#34;]
        SPARK[&amp;#34;Spark&amp;lt;br/&amp;gt;内存计算 快&amp;lt;br/&amp;gt;本篇主角&amp;#34;]
        FLINK[&amp;#34;Flink&amp;lt;br/&amp;gt;流处理专精&amp;#34;]
    end
    subgraph 应用层
        HIVE[&amp;#34;Hive/Spark SQL&amp;lt;br/&amp;gt;SQL 写报表&amp;#34;]
        DS[&amp;#34;数据科学&amp;lt;br/&amp;gt;MLlib 特征工程&amp;#34;]
    end
    HDFS --&amp;gt; YARN
    S3 --&amp;gt; YARN
    YARN --&amp;gt; MR
    YARN --&amp;gt; SPARK
    YARN --&amp;gt; FLINK
    SPARK --&amp;gt; HIVE
    SPARK --&amp;gt; DS
    style SPARK fill:#ffd699&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;几个关键认知：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Spark 不是存储系统&lt;/strong&gt;，它没有自己的存储，数据放在 HDFS、Hive 表、S3 甚至 MySQL 里，它读进来算完再写回去。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spark 也不是资源调度器&lt;/strong&gt;，它跑在 YARN 或 Kubernetes 上，向它们「申请」机器。&lt;/li&gt;
&lt;li&gt;Spark 相对 MapReduce 的快，核心就一句话：&lt;strong&gt;中间结果放内存，不落地磁盘&lt;/strong&gt;。MapReduce 每步都要写磁盘，迭代式计算（比如机器学习跑 50 轮迭代）就慢到不可用。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;踩坑预告&lt;/strong&gt;：很多人学 Spark 的第一步是去装 Hadoop 全家桶，装了一周还没跑起来 Hello World。其实本地学习完全不需要——Spark 有 &lt;code&gt;local&lt;/code&gt; 模式， pip 装个 &lt;code&gt;pyspark&lt;/code&gt; 就能跑。下一篇的 Demo 全是单机可运行的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第二篇-架构与原理代码提交之后到底发生了什么&#34;&gt;第二篇 架构与原理：代码提交之后到底发生了什么&lt;/h2&gt;
&lt;p&gt;这是最重要的一篇。Spark 调优 80% 靠对这套架构的直觉，剩下 20% 才是参数。&lt;/p&gt;
&lt;h3 id=&#34;21-一次-spark-任务的全貌&#34;&gt;2.1 一次 Spark 任务的全貌&lt;/h3&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 TB
    CODE[&amp;#34;你的代码&amp;lt;br/&amp;gt;spark.read... .groupBy... .write...&amp;#34;] --&amp;gt; DRV
    subgraph Cluster[&amp;#34;集群（YARN / K8s）&amp;#34;]
        subgraph DRV[&amp;#34;Driver（司机/总指挥）&amp;#34;]
            DAG[&amp;#34;DAG Scheduler&amp;lt;br/&amp;gt;把代码翻译成执行计划&amp;lt;br/&amp;gt;切成若干 Stage&amp;#34;]
            TS[&amp;#34;Task Scheduler&amp;lt;br/&amp;gt;把 Stage 拆成 Task&amp;lt;br/&amp;gt;派发给 Executor&amp;#34;]
        end
        subgraph EXE[&amp;#34;Executor × N（干活的工人）&amp;#34;]
            T1[&amp;#34;Task 1&amp;lt;br/&amp;gt;处理 Partition 1&amp;#34;]
            T2[&amp;#34;Task 2&amp;lt;br/&amp;gt;处理 Partition 2&amp;#34;]
            T3[&amp;#34;Task 3&amp;lt;br/&amp;gt;处理 Partition 3&amp;#34;]
            CACHE[&amp;#34;内存/磁盘&amp;lt;br/&amp;gt;缓存中间数据&amp;#34;]
        end
    end
    TS --&amp;gt; T1
    TS --&amp;gt; T2
    TS --&amp;gt; T3
    T1 -.shuffle 阶段互相交换数据.-&amp;gt; T2&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;关键角色就三个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Driver&lt;/strong&gt;：你的 &lt;code&gt;main&lt;/code&gt; 函数（或 PySpark 脚本）运行的地方。它不干活，它&lt;strong&gt;发号施令&lt;/strong&gt;——分析你的代码、生成执行计划、把任务派发出去、回收结果。Driver 挂了，整个任务就完了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Executor&lt;/strong&gt;：集群里真正干活的 JVM 进程，每个 Executor 有若干核，每个核同时跑一个 Task。数据是**分区（Partition）**存储的，一个 Task 处理一个 Partition。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shuffle&lt;/strong&gt;：当操作需要跨机器汇总数据（&lt;code&gt;groupBy&lt;/code&gt;、&lt;code&gt;join&lt;/code&gt;、&lt;code&gt;distinct&lt;/code&gt;），数据就要在网络上重新分发——这叫 Shuffle。&lt;strong&gt;Shuffle 是 Spark 任务慢的第一大原因&lt;/strong&gt;，后文反复出现。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;22-懒执行lazy-evaluation为什么你的代码没报错却在运行时炸了&#34;&gt;2.2 懒执行（Lazy Evaluation）：为什么你的代码&amp;quot;没报错&amp;quot;却在运行时炸了&lt;/h3&gt;
&lt;p&gt;新手最常踩的认知坑：&lt;strong&gt;Spark 代码写下去并不会立刻执行&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-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;df &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;read&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;csv(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;orders.csv&amp;#34;&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;df2 &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; df&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(df&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;amount &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;100&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;df3 &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; df2&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;groupBy(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;city&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;sum(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount&amp;#34;&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;df3&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;write&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;save(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;output&amp;#34;&lt;/span&gt;)                   &lt;span style=&#34;color:#75715e&#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;Transformation（变换）&lt;/strong&gt;，只是往计划里加节点；&lt;code&gt;write&lt;/code&gt;、&lt;code&gt;count&lt;/code&gt;、&lt;code&gt;collect&lt;/code&gt;、&lt;code&gt;show&lt;/code&gt; 这些叫 &lt;strong&gt;Action（行动）&lt;/strong&gt;，碰到 Action，Driver 才把整条链路翻译成 DAG（有向无环图），切成 Stage，真正派发执行。&lt;/p&gt;
&lt;p&gt;这个设计的好处是 &lt;strong&gt;优化&lt;/strong&gt;：Spark 的 Catalyst 优化器能看到整条链路，比如发现你过滤条件写晚了，它可以自动把 &lt;code&gt;filter&lt;/code&gt; 下推到读数据时就做（Predicate Pushdown），少读一半数据。&lt;/p&gt;
&lt;p&gt;但坑也随之而来：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事 1：错误行号的谎言&lt;/strong&gt;
刚转 Spark 时我排查一个任务失败，日志说第 87 行 &lt;code&gt;df.write.save()&lt;/code&gt; 报错 &amp;ldquo;文件不存在&amp;rdquo;。我盯着 87 行看了半小时——write 语法没问题啊！后来才明白：&lt;strong&gt;真正的错误发生在第 12 行的 &lt;code&gt;spark.read&lt;/code&gt;（路径写错了），但因为是懒执行，直到 87 行触发 Action 才爆出来&lt;/strong&gt;。看 Spark 报错，永远要从日志里的执行计划往上找 Transformation 的源头，而不是看报错行号。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;23-stage-是怎么切出来的宽依赖与窄依赖&#34;&gt;2.3 Stage 是怎么切出来的：宽依赖与窄依赖&lt;/h3&gt;
&lt;p&gt;理解 Shuffle 就理解了 Stage 的切分：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph Stage1[&amp;#34;Stage 1（窄依赖，流水线执行）&amp;#34;]
        P1[&amp;#34;Partition 1&amp;lt;br/&amp;gt;读文件&amp;#34;] --&amp;gt; F1[&amp;#34;filter&amp;lt;br/&amp;gt;过滤&amp;#34;]
        P2[&amp;#34;Partition 2&amp;lt;br/&amp;gt;读文件&amp;#34;] --&amp;gt; F2[&amp;#34;filter&amp;lt;br/&amp;gt;过滤&amp;#34;]
    end
    subgraph SHUFFLE[&amp;#34;⚡ Shuffle（跨网络搬数据）&amp;#34;]
        direction TB
        S[&amp;#34;按 city 重新分区&amp;lt;br/&amp;gt;同 city 的数据&amp;lt;br/&amp;gt;汇聚到同一 Partition&amp;#34;]
    end
    subgraph Stage2[&amp;#34;Stage 2&amp;#34;]
        G1[&amp;#34;Partition A&amp;lt;br/&amp;gt;北京的订单&amp;lt;br/&amp;gt;sum(amount)&amp;#34;]
        G2[&amp;#34;Partition B&amp;lt;br/&amp;gt;上海的订单&amp;lt;br/&amp;gt;sum(amount)&amp;#34;]
    end
    F1 --&amp;gt; SHUFFLE
    F2 --&amp;gt; SHUFFLE
    SHUFFLE --&amp;gt; G1
    SHUFFLE --&amp;gt; G2
    style SHUFFLE fill:#ffcccc&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;窄依赖&lt;/strong&gt;（filter、map）：子 Partition 只依赖父的一个 Partition，可以像流水线一样在同一台机器一口气算完，不分 Stage。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;宽依赖&lt;/strong&gt;（groupBy、join、distinct）：子 Partition 需要父的&lt;strong&gt;所有&lt;/strong&gt; Partition 的数据，必须 Shuffle，Stage 在这里断开。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;一个 Stage 内的失败重试只重跑这个 Stage&lt;/strong&gt;。所以 Spark 的容错单位是 Stage——这也是为什么 Spark 会把 Shuffle 的中间结果落盘（对，嘴上说自己内存计算，Shuffle 还是要写磁盘的），挂了能从这里恢复，而不是从头再来。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第三篇-rdd-与-dataframe两代-api-的分水岭&#34;&gt;第三篇 RDD 与 DataFrame：两代 API 的分水岭&lt;/h2&gt;
&lt;h3 id=&#34;31-rdd能看清每个零件但要自己造发动机&#34;&gt;3.1 RDD：能看清每个零件，但要自己造发动机&lt;/h3&gt;
&lt;p&gt;RDD（弹性分布式数据集）是 Spark 1.x 的核心抽象。用它写上面的需求：&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-scala&#34; data-lang=&#34;scala&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// RDD 风格：你告诉 Spark 每一步&amp;#34;怎么做&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;orders
&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;.&lt;/span&gt;filter&lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;_&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;split&lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;,&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;)(&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;).&lt;/span&gt;toDouble &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;)&lt;/span&gt;     &lt;span style=&#34;color:#75715e&#34;&gt;// 自己解析 CSV，自己转类型
&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;.&lt;/span&gt;map&lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;line &lt;span style=&#34;color:#66d9ef&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;line&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;split&lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;,&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;)(&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;),&lt;/span&gt;           &lt;span style=&#34;color:#75715e&#34;&gt;// 自己取城市字段做 key
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                line&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;split&lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;,&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;)(&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;).&lt;/span&gt;toDouble&lt;span style=&#34;color:#f92672&#34;&gt;))&lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;// 自己取金额做 value
&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;.&lt;/span&gt;reduceByKey&lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;_&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;_&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;)&lt;/span&gt;                          &lt;span style=&#34;color:#75715e&#34;&gt;// 按 key 聚合
&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;.&lt;/span&gt;saveAsTextFile&lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;output&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#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;RDD 的问题是：&lt;strong&gt;Spark 看不懂你在干什么&lt;/strong&gt;。&lt;code&gt;filter(_.split(&amp;quot;,&amp;quot;)(5).toDouble &amp;gt; 100)&lt;/code&gt; 对优化器来说就是一个黑盒 lambda，它不知道你在过滤哪列，没法做谓词下推，没法选更好的 join 策略。而且 JVM 里每一行都是一个 Java 对象，序列化开销大、GC 压力巨大。&lt;/p&gt;
&lt;h3 id=&#34;32-dataframe你只说要什么优化器替你想怎么做&#34;&gt;3.2 DataFrame：你只说&amp;quot;要什么&amp;quot;，优化器替你想&amp;quot;怎么做&amp;quot;&lt;/h3&gt;
&lt;p&gt;DataFrame（Spark 1.3 引入，2.0 后与 Dataset 合并）给数据加了&lt;strong&gt;Schema（表结构）&lt;/strong&gt;，从此 Spark 能看懂你的意图：&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-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# DataFrame 风格：声明式&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;read
&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;.&lt;/span&gt;schema(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_id STRING, city STRING, amount DOUBLE&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;.&lt;/span&gt;csv(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;orders.csv&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;.&lt;/span&gt;filter(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount &amp;gt; 100&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;.&lt;/span&gt;groupBy(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;city&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;.&lt;/span&gt;sum(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount&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;.&lt;/span&gt;write&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;parquet(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;output&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;两者差距有多大？同样的逻辑，DataFrame 通常比 RDD &lt;strong&gt;快 2~10 倍&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 TB
    subgraph RDD路径[&amp;#34;RDD：信任程序员&amp;#34;]
        R1[&amp;#34;你的 lambda&amp;#34;] --&amp;gt; R2[&amp;#34;逐行 Java 对象&amp;lt;br/&amp;gt;序列化/GC 开销大&amp;lt;br/&amp;gt;优化器爱莫能助&amp;#34;]
    end
    subgraph DF路径[&amp;#34;DataFrame：信任优化器&amp;#34;]
        D1[&amp;#34;声明式表达式&amp;#34;] --&amp;gt; D2[&amp;#34;Catalyst 优化器&amp;lt;br/&amp;gt;逻辑计划优化&amp;lt;br/&amp;gt;谓词下推/列裁剪&amp;#34;]
        D2 --&amp;gt; D3[&amp;#34;Tungsten 执行引擎&amp;lt;br/&amp;gt;二进制内存布局&amp;lt;br/&amp;gt;代码生成 绕开 GC&amp;#34;]
    end
    style DF路径 fill:#e8f4e8&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;一句话结论（2022 年视角）&lt;/strong&gt;：新业务&lt;strong&gt;永远用 DataFrame/Spark SQL 写&lt;/strong&gt;，RDD 只在两种情况下出现——操作的是非结构化数据（比如解析一堆不规则日志），或者需要逐分区精细控制（&lt;code&gt;mapPartitions&lt;/code&gt; 里建数据库连接池这种）。面试让你手写 RDD wordcount 是考察理解，不是让你生产上这么写。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;33-本地就能跑的完整-demo&#34;&gt;3.3 本地就能跑的完整 Demo&lt;/h3&gt;
&lt;p&gt;别急着上集群。装个 Python 环境就能跑：&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;pip install pyspark&lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt;3.3.0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&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-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# demo01_basic.py&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;from&lt;/span&gt; pyspark.sql &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; SparkSession
&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;from&lt;/span&gt; pyspark.sql &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; functions &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; F
&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;spark &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (SparkSession&lt;span style=&#34;color:#f92672&#34;&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:#f92672&#34;&gt;.&lt;/span&gt;appName(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;demo01&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;.&lt;/span&gt;master(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;local[*]&amp;#34;&lt;/span&gt;)      &lt;span style=&#34;color:#75715e&#34;&gt;# 本地模式，用满 CPU 核&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;.&lt;/span&gt;getOrCreate())
&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;# 造数据：100 万行模拟订单&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;orders &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;range(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;1_000_000&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;.&lt;/span&gt;select(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;concat(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;lit(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ORD&amp;#34;&lt;/span&gt;), F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_id&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              (F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;%&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;50000&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&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;              F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;element_at(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                  F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;array([F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;lit(c) &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; c &lt;span style=&#34;color:#f92672&#34;&gt;in&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:#e6db74&#34;&gt;&amp;#34;北京&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;上海&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;广州&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;深圳&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#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;                  (F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;%&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;cast(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;int&amp;#34;&lt;/span&gt;))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;city&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              (F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;rand() &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;2000&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;date_add(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;lit(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2022-10-25&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;-&lt;/span&gt;(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;%&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;30&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;cast(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;int&amp;#34;&lt;/span&gt;))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_date&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;result &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (orders
&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;.&lt;/span&gt;filter(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;100&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;.&lt;/span&gt;groupBy(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;city&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;.&lt;/span&gt;agg(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;count(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;*&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_cnt&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;               F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;round(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;sum(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;), &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;gmv&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;               F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;round(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;avg(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;), &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;avg_price&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;.&lt;/span&gt;orderBy(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;desc(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;gmv&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;result&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;show()          &lt;span style=&#34;color:#75715e&#34;&gt;# Action：真正触发执行&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;result&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;write&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;mode(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;overwrite&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;parquet(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;output/city_gmv&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;stop()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&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;python demo01_basic.py&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;输出：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;+----+---------+-----------+---------+
|city|order_cnt|        gmv|avg_price|
+----+---------+-----------+---------+
|  深圳|   190231|...        |  1050.12|
|  北京|   190087|...        |  1049.87|
...
+----+---------+-----------+---------+&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这个 Demo 麻雀虽小，五脏俱全：&lt;code&gt;spark.range&lt;/code&gt; 造数、&lt;code&gt;element_at&lt;/code&gt; 数组取值、&lt;code&gt;rand&lt;/code&gt; 随机数、聚合多指标、写 Parquet。本地玩熟了，语法迁移到集群上&lt;strong&gt;一行都不用改&lt;/strong&gt;——这就是 Spark 的优雅之处。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事 2：Windows 上的 winutils.exe&lt;/strong&gt;
我在 Windows 笔记本上第一次跑 PySpark，直接 &lt;code&gt;winutils.exe&lt;/code&gt; 缺失报错 + 一堆权限异常。解决办法是下载对应 Hadoop 版本的 &lt;code&gt;winutils.exe&lt;/code&gt; 放到 &lt;code&gt;%HADOOP_HOME%\bin&lt;/code&gt; 并设置环境变量。后来学乖了：&lt;strong&gt;学习期直接 WSL2 或者 Docker 起个 Linux 环境&lt;/strong&gt;，少生一肚子气。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第四篇-电商实战把知识串成业务&#34;&gt;第四篇 电商实战：把知识串成业务&lt;/h2&gt;
&lt;p&gt;这一篇用三个电商真实场景，把前面的知识点全部用起来。这三个场景也是面试里最高频的&amp;quot;你做过什么项目&amp;quot;素材。&lt;/p&gt;
&lt;h3 id=&#34;41-场景一订单宽表每日-etl&#34;&gt;4.1 场景一：订单宽表（每日 ETL）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;业务&lt;/strong&gt;：订单数据分散在三张表——订单主表（order_id、user_id、amount、pay_time）、订单明细表（order_id、sku_id、quantity）、商品维表（sku_id、category、brand）。数据仓库要求每天凌晨产出一张&amp;quot;订单明细宽表&amp;quot;，下游 BI 直接查它。&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-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# demo02_dwd_wide_table.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;orders &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;table(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ods.orders&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dt&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2022-10-25&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;items  &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;table(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ods.order_items&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dt&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2022-10-25&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;skus   &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;table(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dim.sku&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dt&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2022-10-25&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;wide &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (items
&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;.&lt;/span&gt;join(orders, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_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:#f92672&#34;&gt;.&lt;/span&gt;join(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;broadcast(skus), &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sku_id&amp;#34;&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:#f92672&#34;&gt;.&lt;/span&gt;select(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_id&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sku_id&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;category&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;brand&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:#e6db74&#34;&gt;&amp;#34;quantity&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;pay_time&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;.&lt;/span&gt;withColumn(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dt&amp;#34;&lt;/span&gt;, F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;lit(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2022-10-25&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;(wide&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;write
&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;.&lt;/span&gt;mode(&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:#f92672&#34;&gt;.&lt;/span&gt;partitionBy(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dt&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;.&lt;/span&gt;parquet(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;warehouse/dwd/order_detail_wide&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;知识点：Broadcast Join&lt;/strong&gt;。普通 join 两张表都要 Shuffle；但如果一张表很小（商品维表几十万行，几十 MB），Spark 可以把它&lt;strong&gt;整个复制到每个 Executor 的内存里&lt;/strong&gt;，大表原地不动就能完成 join——Shuffle 直接消失，速度提升一个数量级。&lt;/p&gt;
&lt;p&gt;判断标准：小表 &amp;lt; &lt;code&gt;spark.sql.autoBroadcastJoinThreshold&lt;/code&gt;（默认 10MB，生产常调到 100MB~500MB）。写 &lt;code&gt;F.broadcast(skus)&lt;/code&gt; 是显式强制，防止优化器估算失误。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph 普通Join[&amp;#34;普通 Sort-Merge Join（两张表都 Shuffle）&amp;#34;]
        A1[&amp;#34;大表 Partition&amp;#34;] -.网络搬家.-&amp;gt; J1[&amp;#34;Join 节点&amp;#34;]
        A2[&amp;#34;维表 Partition&amp;#34;] -.网络搬家.-&amp;gt; J1
    end
    subgraph 广播Join[&amp;#34;Broadcast Join（只有小表复制）&amp;#34;]
        B1[&amp;#34;大表 Partition 1&amp;lt;br/&amp;gt;原地不动&amp;#34;] --&amp;gt; C1[&amp;#34;本地 Join&amp;lt;br/&amp;gt;查内存里的小表&amp;#34;]
        B2[&amp;#34;大表 Partition 2&amp;lt;br/&amp;gt;原地不动&amp;#34;] --&amp;gt; C2[&amp;#34;本地 Join&amp;lt;br/&amp;gt;查内存里的小表&amp;#34;]
        SM[&amp;#34;小表整表复制&amp;lt;br/&amp;gt;→ 每个 Executor&amp;#34;] --&amp;gt; C1
        SM --&amp;gt; C2
    end
    style 广播Join fill:#e8f4e8&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;42-场景二用户行为漏斗转化率分析&#34;&gt;4.2 场景二：用户行为漏斗（转化率分析）&lt;/h3&gt;
&lt;p&gt;&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;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-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# demo03_funnel.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;events &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;table(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ods.user_events&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dt&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2022-10-25&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:#75715e&#34;&gt;# events 字段：user_id, event_type(view/click/cart/order/pay), ts&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;first_ts &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:#f92672&#34;&gt;.&lt;/span&gt;groupBy(&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:#f92672&#34;&gt;.&lt;/span&gt;agg(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;min(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;event_type&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;view&amp;#34;&lt;/span&gt;,  F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ts&amp;#34;&lt;/span&gt;)))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_view&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;min(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;event_type&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;click&amp;#34;&lt;/span&gt;, F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ts&amp;#34;&lt;/span&gt;)))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_click&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;min(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;event_type&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;cart&amp;#34;&lt;/span&gt;,  F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ts&amp;#34;&lt;/span&gt;)))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_cart&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;min(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;event_type&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order&amp;#34;&lt;/span&gt;, F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ts&amp;#34;&lt;/span&gt;)))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_order&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;min(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;event_type&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;pay&amp;#34;&lt;/span&gt;,   F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ts&amp;#34;&lt;/span&gt;)))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_pay&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;# 严格漏斗：必须按时间顺序走到该步才算转化&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;funnel &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; first_ts&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;agg(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;count(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_view&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#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;    F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;count(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_click&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_view&amp;#34;&lt;/span&gt;), &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#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;    F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;count(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_cart&amp;#34;&lt;/span&gt;)  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_click&amp;#34;&lt;/span&gt;), &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#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;    F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;count(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_order&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_cart&amp;#34;&lt;/span&gt;),  &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#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;    F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;count(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_pay&amp;#34;&lt;/span&gt;)   &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_order&amp;#34;&lt;/span&gt;), &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#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;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;funnel&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;show()&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;code&gt;groupBy&lt;/code&gt; + 条件聚合（&lt;code&gt;F.when&lt;/code&gt; inside &lt;code&gt;agg&lt;/code&gt;）是漏斗分析的标准套路&lt;/strong&gt;——一行 SQL 出五步漏斗，不用写五次 count 再 join。这也是经典面试题。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事 3：漏斗口径的战争&lt;/strong&gt;
我第一版漏斗上线后，运营说数据和他们对不上。查了半天，发现分歧在口径：我写的是&amp;quot;严格顺序漏斗&amp;quot;（必须 view 之后才 click 才算），运营期望的是&amp;quot;存在性漏斗&amp;quot;（只要当天两个行为都发生过就算）。&lt;strong&gt;大数据开发一半的返工不是技术问题，是口径没对齐&lt;/strong&gt;。现在的习惯：开工前把每一步的定义写成一行白话发群里，确认了再写代码。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;43-场景三推荐特征工程用户画像&#34;&gt;4.3 场景三：推荐特征工程（用户画像）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;业务&lt;/strong&gt;：算法团队要用户特征——每个用户近 30 天的购买次数、客单价、最偏爱类目、最近一次购买距今天数（RFM 模型的雏形）。&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-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# demo04_user_features.py&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;from&lt;/span&gt; pyspark.sql.window &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; Window
&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;orders_30d &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;table(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dwd.order_detail_wide&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dt&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;between(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2022-09-26&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2022-10-25&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;# 基础聚合特征&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;agg_feat &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (orders_30d
&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;.&lt;/span&gt;groupBy(&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:#f92672&#34;&gt;.&lt;/span&gt;agg(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;countDistinct(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_id&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_cnt_30d&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;round(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;avg(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;), &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;avg_amount_30d&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;max(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;pay_time&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;last_pay_time&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;# 最偏爱类目：窗口函数按类目的购买金额排序取第一&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;w &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Window&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;partitionBy(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;orderBy(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;desc(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;cat_amount&amp;#34;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;top_cat &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (orders_30d
&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;.&lt;/span&gt;groupBy(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_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:#f92672&#34;&gt;.&lt;/span&gt;agg(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;sum(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;cat_amount&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;.&lt;/span&gt;withColumn(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;rn&amp;#34;&lt;/span&gt;, F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;row_number()&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;over(w))
&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;.&lt;/span&gt;filter(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;rn&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;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:#f92672&#34;&gt;.&lt;/span&gt;select(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&amp;#34;&lt;/span&gt;, F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;category&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;top_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&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;features &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (agg_feat
&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;.&lt;/span&gt;join(top_cat, &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:#f92672&#34;&gt;.&lt;/span&gt;withColumn(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;days_since_last_pay&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                        F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;datediff(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;lit(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2022-10-26&amp;#34;&lt;/span&gt;), F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;last_pay_time&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;features&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;write&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;mode(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;overwrite&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;parquet(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;features/user_rfm/dt=2022-10-25&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;（&lt;code&gt;Window.partitionBy(...).orderBy(...)&lt;/code&gt; + &lt;code&gt;row_number&lt;/code&gt;）是&amp;quot;每组取 TopN&amp;quot;的标准解法，Hive SQL 里也是同一句，大数据面试必考。&lt;/p&gt;
&lt;p&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 LR
    subgraph 数据源
        DB[&amp;#34;业务 MySQL&amp;lt;br/&amp;gt;订单/明细/商品&amp;#34;]
        LOG[&amp;#34;埋点日志&amp;lt;br/&amp;gt;曝光/点击/加购&amp;#34;]
    end
    DB --&amp;gt;|Sqoop/DataX 同步| ODS
    LOG --&amp;gt;|Flume/直接落盘| ODS
    subgraph 数仓[&amp;#34;数仓分层（Hive on Spark）&amp;#34;]
        ODS[&amp;#34;ODS 贴源层&amp;lt;br/&amp;gt;原样搬运&amp;#34;] --&amp;gt; DWD[&amp;#34;DWD 明细层&amp;lt;br/&amp;gt;清洗 关联 宽表&amp;lt;br/&amp;gt;★ 场景一&amp;#34;]
        DWD --&amp;gt; DWS[&amp;#34;DWS 汇总层&amp;lt;br/&amp;gt;按主题聚合&amp;lt;br/&amp;gt;★ 场景二漏斗&amp;#34;]
        DWS --&amp;gt; ADS[&amp;#34;ADS 应用层&amp;lt;br/&amp;gt;直接服务业务&amp;lt;br/&amp;gt;★ 场景三特征&amp;#34;]
    end
    ADS --&amp;gt; BI[&amp;#34;BI 报表&amp;#34;]
    ADS --&amp;gt; ALG[&amp;#34;推荐算法&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;第五篇-调优与踩坑生产环境才是真的战场&#34;&gt;第五篇 调优与踩坑：生产环境才是真的战场&lt;/h2&gt;
&lt;p&gt;本地 Demo 岁月静好，上了生产环境才知道什么叫残酷。下面三个坑，每一个我都在凌晨两点处理过。&lt;/p&gt;
&lt;h3 id=&#34;51-天坑一数据倾斜一个-task-跑三小时其他三分钟&#34;&gt;5.1 天坑一：数据倾斜（一个 Task 跑三小时，其他三分钟）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：任务 99% 的 Stage 秒过，最后一个 Stage 的 200 个 Task 里，199 个 5 分钟跑完，&lt;strong&gt;1 个跑了 3 小时还在爬&lt;/strong&gt;。Spark UI 上看，那个 Task 的 Shuffle Read 是别人的几百倍。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：某个 key 的数据量碾压其他 key。电商里最典型的就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;大促时某个爆款商品（iPhone 发售当天）的订单明细占全站 30%；&lt;/li&gt;
&lt;li&gt;维表 join 时空值/默认值 key——脏数据的 &lt;code&gt;user_id = null&lt;/code&gt; 或 &lt;code&gt;&#39;-1&#39;&lt;/code&gt; 有上千万条，全挤到一个 Partition。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;排查&lt;/strong&gt;：Spark UI → Stages → 看 Task 的 Summary Metrics，Max 时间远大于 Median（中位数），就是倾斜。&lt;/p&gt;
&lt;p&gt;&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;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-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 招式1：过滤异常 key（脏数据导致的倾斜，90% 的情况）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;df &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; df&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;isNotNull() &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt; (F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;-1&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;# 招式2：热点 key 加盐打散（真热点，比如爆款商品）&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;# 给大表的热点 key 加随机后缀 0~9，小表对应 key 复制 10 份&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;salted &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; big&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;withColumn(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;join_key&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sku_id&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;爆款SKU&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;concat(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sku_id&amp;#34;&lt;/span&gt;), F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;lit(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;_&amp;#34;&lt;/span&gt;), (F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;rand()&lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;cast(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;int&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;.&lt;/span&gt;otherwise(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sku_id&amp;#34;&lt;/span&gt;)))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;small_exploded &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; small&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;withColumn(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;join_key&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sku_id&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;爆款SKU&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;concat(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sku_id&amp;#34;&lt;/span&gt;), F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;lit(&lt;span style=&#34;color:#e6db74&#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;                    F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;explode(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;array([F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;lit(i) &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; i &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; range(&lt;span style=&#34;color:#ae81ff&#34;&gt;10&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;.&lt;/span&gt;otherwise(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sku_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;# 招式3：开启 AQE 自动处理（Spark 3.x 的救星）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;conf&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;set(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;spark.sql.adaptive.enabled&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;true&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;conf&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;set(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;spark.sql.adaptive.skewJoin.enabled&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;true&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事 4：AQE 救了我一个通宵&lt;/strong&gt;
2022 年上半年迁移 Spark 3.x 后开了 AQE（Adaptive Query Execution），它能&lt;strong&gt;在运行时&lt;/strong&gt;发现倾斜的 Partition 并自动拆分。以前要手工调参数 &lt;code&gt;spark.sql.shuffle.partitions&lt;/code&gt; 反复试的任务，AQE 直接自己搞定了。所以第一建议永远是：&lt;strong&gt;先升级到 Spark 3.x 并开启 AQE，再用脑子去调&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&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 TB
    subgraph 倾斜前[&amp;#34;倾斜：一个 Partition 吞掉 40% 数据&amp;#34;]
        K1[&amp;#34;Partition 1: 50MB&amp;#34;] 
        K2[&amp;#34;Partition 2: 8GB 😱&amp;#34;] 
        K3[&amp;#34;Partition 3: 50MB&amp;#34;]
    end
    subgraph 倾斜后[&amp;#34;AQE 拆分后：化整为零&amp;#34;]
        L1[&amp;#34;Partition 1: 50MB&amp;#34;]
        L2a[&amp;#34;Partition 2a: 200MB&amp;#34;]
        L2b[&amp;#34;Partition 2b: 200MB&amp;#34;]
        L2c[&amp;#34;Partition 2c: 200MB&amp;lt;br/&amp;gt;...共拆 40 份&amp;#34;]
        L3[&amp;#34;Partition 3: 50MB&amp;#34;]
    end
    倾斜前 --&amp;gt;|&amp;#34;AQE 运行时检测拆分&amp;#34;| 倾斜后
    style K2 fill:#ffcccc&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;52-天坑二oom内存不够但到底谁不够&#34;&gt;5.2 天坑二：OOM（内存不够，但到底谁不够）&lt;/h3&gt;
&lt;p&gt;Spark 的 OOM 分两种，报错长得像，病因完全不同：&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;ExecutorLostFailure&lt;/code&gt; / 容器被 kill / &lt;code&gt;java.lang.OutOfMemoryError&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;Executor 内存不够&lt;/strong&gt;：数据太多、缓存太大&lt;/td&gt;
					&lt;td&gt;加 &lt;code&gt;executor-memory&lt;/code&gt;、增加分区数摊薄数据、减少 &lt;code&gt;cache&lt;/code&gt; 滥用&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Driver 端 OOM&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;Driver 内存不够&lt;/strong&gt;：几乎永远是 &lt;code&gt;collect()&lt;/code&gt; / &lt;code&gt;toPandas()&lt;/code&gt; 把全量数据拉回 Driver&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;不要用 collect 收大数据！&lt;/strong&gt; 用 &lt;code&gt;take(100)&lt;/code&gt; 抽样看，用 &lt;code&gt;write&lt;/code&gt; 落盘&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事 5：一个 collect 拖垮了整个集群调度&lt;/strong&gt;
同事为了&amp;quot;快速验证&amp;quot;，在生产任务里写了 &lt;code&gt;df.collect()&lt;/code&gt; 然后 len() 一下——那张表 2 亿行。Driver 内存从 4G 一路撑爆，任务挂掉不说，还因为是调度系统的重要任务，报警电话打到了总监那里。&lt;strong&gt;从此组里 Code Review 第一条红线：搜索代码里有没有 &lt;code&gt;collect()&lt;/code&gt;、&lt;code&gt;toPandas()&lt;/code&gt;，有就必须说明数据量上限&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;53-天坑三小文件灾难hdfs-的慢性病&#34;&gt;5.3 天坑三：小文件灾难（HDFS 的慢性病）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：任务很快跑完，但写出的结果目录里是 &lt;strong&gt;20000 个 100KB 的小文件&lt;/strong&gt;。然后：Hive 查这张表慢得离谱，NameNode 内存报警，下游任务读它要启动几万个 Task。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：200 个分区 × 每次写 100 个文件 = 每天积累，几个月后就是几百万小文件。HDFS 的 NameNode 把每个文件的元数据放内存，小文件多是集群的头号慢性病。&lt;/p&gt;
&lt;p&gt;&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;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-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 写出前合并分区（注意：coalesce 不 Shuffle，repartition 会 Shuffle）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;df&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;coalesce(&lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;write&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;parquet(&lt;span style=&#34;color:#f92672&#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;df&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;repartition(&lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dt&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;write&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;parquet(&lt;span style=&#34;color:#f92672&#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;
&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;# 根治：开启 AQE 的 Shuffle 分区自动合并，从源头减少小文件&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;conf&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;set(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;spark.sql.adaptive.coalescePartitions.enabled&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;true&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;54-一张排障决策图&#34;&gt;5.4 一张排障决策图&lt;/h3&gt;
&lt;p&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
    Q1{&amp;#34;任务挂了还是慢？&amp;#34;}
    Q1 --&amp;gt;|挂了| Q2{&amp;#34;Driver 日志里&amp;lt;br/&amp;gt;OOM 关键字？&amp;#34;}
    Q2 --&amp;gt;|Executor 端| F1[&amp;#34;加 executor-memory&amp;lt;br/&amp;gt;或增大分区数&amp;#34;]
    Q2 --&amp;gt;|Driver 端| F2[&amp;#34;查 collect/toPandas&amp;lt;br/&amp;gt;换成 write 或 take&amp;#34;]
    Q2 --&amp;gt;|文件不存在等| F3[&amp;#34;懒执行报错错位&amp;lt;br/&amp;gt;从执行计划往上找源头&amp;#34;]
    Q1 --&amp;gt;|慢| Q3{&amp;#34;Spark UI 看 Stage：&amp;lt;br/&amp;gt;个别 Task 特别慢？&amp;#34;}
    Q3 --&amp;gt;|是 Max远大于Median| F4[&amp;#34;数据倾斜&amp;lt;br/&amp;gt;过滤脏key/加盐/AQE&amp;#34;]
    Q3 --&amp;gt;|否 全都慢| Q4{&amp;#34;Shuffle Read/Write&amp;lt;br/&amp;gt;数据量巨大？&amp;#34;}
    Q4 --&amp;gt;|是| F5[&amp;#34;能广播的小表广播&amp;lt;br/&amp;gt;列裁剪 过滤下推&amp;lt;br/&amp;gt;提前聚合减少数据量&amp;#34;]
    Q4 --&amp;gt;|否| F6[&amp;#34;查上游：小文件太多？&amp;lt;br/&amp;gt;查集群：资源不够排队？&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;第六篇-ai-时代少写代码完成大数据开发&#34;&gt;第六篇 AI 时代：少写代码，完成大数据开发&lt;/h2&gt;
&lt;p&gt;时间快进到 AI 编程工具普及之后。回头看，Spark 开发是&lt;strong&gt;最适合被 AI 改造&lt;/strong&gt;的领域之一，因为它的代码模式极其规整：读、变换、聚合、写，套路就那么多。&lt;/p&gt;
&lt;h3 id=&#34;61-ai-能替你做什么不能替你做什么&#34;&gt;6.1 AI 能替你做什么，不能替你做什么&lt;/h3&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 AI擅长[&amp;#34;AI 做得比人好&amp;#34;]
        A1[&amp;#34;自然语言 → PySpark/SQL 代码&amp;lt;br/&amp;gt;按用户30天行为算RFM特征 一句话出码&amp;#34;]
        A2[&amp;#34;样板代码生成&amp;lt;br/&amp;gt;Schema 定义 造数脚本 单元测试&amp;#34;]
        A3[&amp;#34;报错诊断&amp;lt;br/&amp;gt;贴堆栈 直接定位到懒执行错位/倾斜&amp;#34;]
        A4[&amp;#34;口径文档化&amp;lt;br/&amp;gt;把代码翻译成业务能看懂的白话&amp;#34;]
    end
    subgraph AI不擅长[&amp;#34;人必须自己把关&amp;#34;]
        B1[&amp;#34;业务口径&amp;lt;br/&amp;gt;漏斗是严格顺序还是存在性&amp;lt;br/&amp;gt;AI 不知道你老板要什么&amp;#34;]
        B2[&amp;#34;性能直觉&amp;lt;br/&amp;gt;哪里该广播 哪里会倾斜&amp;lt;br/&amp;gt;要看真实数据分布&amp;#34;]
        B3[&amp;#34;生产决策&amp;lt;br/&amp;gt;资源参数 重跑策略 数据回补&amp;#34;]
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;我的实际工作流变化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;以前&lt;/strong&gt;：写「用户漏斗分析」= 查文档 30 分钟 + 写代码 40 分钟 + 调试 30 分钟。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;现在&lt;/strong&gt;：给 AI 描述清楚——表名、字段、漏斗定义（严格按时间顺序）、输出格式——10 秒出代码，我花 20 分钟 review 口径和边界条件。&lt;strong&gt;总耗时砍到三分之一&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但注意：&lt;strong&gt;省下来的时间全部应该砸在口径确认和数据校验上&lt;/strong&gt;。AI 写的代码语法几乎不会错，但它不知道你们的 &lt;code&gt;user_id = &#39;-1&#39;&lt;/code&gt; 是脏数据、不知道运营要的是存在性漏斗。这正是第五篇那些坑的价值——&lt;strong&gt;AI 时代，踩坑经验不是贬值了，而是成了你最贵的资产&lt;/strong&gt;，因为代码人人都能生成，能判断代码对不对的人变稀缺了。&lt;/p&gt;
&lt;h3 id=&#34;62-具体怎么干三个提效姿势&#34;&gt;6.2 具体怎么干：三个提效姿势&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;姿势一：需求描述模板化。&lt;/strong&gt; 给 AI 下指令时固定四段式：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;背景：表 ods.user_events，字段 user_id/event_type/ts，按天分区 dt
任务：计算 2022-10-25 的转化漏斗 曝光→点击→加购→下单→支付
口径：严格顺序漏斗，后一步的最早时间必须大于前一步
输出：一行五列，列名用中文&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;第四篇场景三的完整代码，用这样的提示词一次就能生成得八九不离十。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;姿势二：报错即提示词。&lt;/strong&gt; Spark 报错堆栈又长又唬人，直接整段贴给 AI，附上你的 Transformation 代码，它对「懒执行错位」「ClassNotFound（依赖没打包）」「分区越界」这类经典错误的诊断准确率很高。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;姿势三：让 AI 写测试和校验脚本。&lt;/strong&gt; 大数据开发最缺的就是测试——数据对不对以前靠人肉抽查。现在让 AI 顺手生成校验 SQL（行数对比、NULL 比例、金额总和与上游对账），质量反而比手工时代更好了。&lt;/p&gt;
&lt;h3 id=&#34;63-冷静的一句话&#34;&gt;6.3 冷静的一句话&lt;/h3&gt;
&lt;p&gt;AI 不会让你&amp;quot;少学 Spark&amp;quot;，它让&lt;strong&gt;学习曲线的重心变了&lt;/strong&gt;：以前背 API 占 60% 的时间，现在这部分 AI 补上了；剩下 40%——架构直觉、业务口径、排障经验——变成了 100% 的核心竞争力。&lt;strong&gt;入门变容易了，精通的标准反而更高了。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;思考与感悟&#34;&gt;思考与感悟&lt;/h2&gt;
&lt;p&gt;写完这篇文章，有几句话想单独说：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 学 Spark 的本质是学&amp;quot;分布式的世界观&amp;quot;。&lt;/strong&gt; 单机时代的直觉——数据随手可得、循环遍历随意、内存无限——全都要扔掉。你要习惯一种新的思维方式：数据是流动的、分区是平行的、Shuffle 是昂贵的、失败是必然的（所以容错设计是一等公民）。这套世界观建立起来之后，学 Flink、学 Kafka、学 ClickHouse 都会很快，因为它们要解决的是同一族问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 调优经验 = 疼痛的记忆。&lt;/strong&gt; 没有人看文档能学会处理数据倾斜，都是被凌晨两点的报警电话教会的。所以别怕踩坑，踩了记下来——我这篇第五篇的每个坑，当年都值一个通宵。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 离业务近一点，离技术远一点。&lt;/strong&gt; 做了几年大数据，最大的感悟是：把 ETL 跑得快不算本事，&lt;strong&gt;知道这张表给谁用、口径对不对、错了谁会亏钱&lt;/strong&gt;才是本事。电商场景里，GMV 算错 1% 可能就是一个部门的年终奖。技术只是手段，数据是业务的镜像。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. AI 时代最值得押注的能力是&amp;quot;判断力&amp;quot;。&lt;/strong&gt; 生成代码会越来越便宜，判断&amp;quot;这段代码对不对、这个口径该不该这么定、这个架构扛不扛得住双十一&amp;quot;会越来越贵。把 AI 当成一个能力极强但没有常识的实习生：活让它干，锅你来背，所以你必须看得懂它干的活。&lt;/p&gt;
&lt;p&gt;与君共勉。&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>
