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