1. Spark调优全景:为什么调优是团队核心技能
说实话,我刚带团队那会儿,最怕听到一句话——「作业跑不动,加资源啊」。
加资源当然能解决问题,但成本呢?一个夜维任务从 2 小时优化到 20 分钟,省下的可不是几台机器,是整个团队的迭代节奏。我见过太多团队,Spark 集群规模翻了三倍,业务响应速度反而慢了。为什么?因为没人真正理解作业在「怎么跑」。
调优不是锦上添花,是团队能不能扛住业务增长的底线技能。今天咱们就把这件事聊透。
1.1 为什么调优是团队核心技能
先看几个真实场景:
- 数据倾斜:一个 join 把 200 个 task 压到 2 个跑,其他 198 个干等。我遇到过某电商大促报表,就因为一个热点商品 ID,集群 80% 的资源在空转。
- 小文件爆炸:每天写入几万个 parquet 文件,元数据服务直接打挂。运维半夜打电话,说 Hive 查不了表了。
- OOM 频发:executor 内存设得很大,GC 时间比计算时间还长。你以为在并行计算?其实 JVM 在疯狂 Full GC。
这些坑,加机器解决不了。只有懂调优的人,才能用最少的资源跑出最快的作业。说白了,调优能力决定了你在团队里是「背锅的」还是「救火的」。
核心观点:调优不是优化一个参数,是理解 Spark 作业的「生命周期」,在每一个环节做对决策。
1.2 Spark作业的生命周期
一个 Spark 作业从提交到结束,经历了什么?我习惯把它拆成四个阶段:
- 提交阶段:Driver 解析用户代码,构建 DAG(有向无环图)。
- 调度阶段:DAGScheduler 将 DAG 切分成 Stage,TaskScheduler 把 Task 分发到 Executor。
- 执行阶段:Executor 拉取数据、执行计算、落盘中间结果。
- 收尾阶段:结果聚合、资源释放、日志清理。
每个阶段都有调优点。比如提交阶段,你写的 groupByKey 和 reduceByKey,生成的 DAG 完全不同。我见过有人用 groupByKey 处理 10 亿条数据,shuffle 写了几百 GB 磁盘,换成 reduceByKey 后,数据量直接降到十分之一。
一个小技巧:在 Spark UI 的 DAG 可视化页面,看一眼 Stage 数量。如果 Stage 超过 10 个,大概率有优化空间。
下面这张图,帮你快速理解作业的生命周期:
1.3 调优的三大维度
调优不是玄学,是有章可循的。我把它归纳为三个维度:资源、代码、Shuffle。这三个维度互相影响,但每个维度都有独立的优化手段。
1.3.1 资源调优
资源调优是基础。你想想看,executor 数量、内存大小、CPU 核数,这些参数配不对,后面代码写得再好也白搭。
| 参数 | 默认值 | 调优建议 |
|---|---|---|
spark.executor.memory |
1g | 根据数据量调整,一般 4g-32g |
spark.executor.cores |
1 | 建议 2-5,避免过多导致 I/O 竞争 |
spark.executor.instances |
2 | 动态分配时设置最大最小值 |
spark.memory.fraction |
0.6 | 执行和存储内存比例,Shuffle 大时适当提高 |
注意:不要盲目加大内存。我曾经遇到一个团队,把 executor 内存从 8g 加到 32g,结果 GC 停顿从 2 秒变成 15 秒。内存越大,Full GC 越频繁。合适的才是最好的。
1.3.2 代码调优
代码调优是性价比最高的。改一行代码,可能比加十台机器还管用。
- 用对算子:
reduceByKey优于groupByKey,mapPartitions优于map。 - 避免重复计算:用
cache或persist缓存中间结果。 - 减少序列化开销:使用 Kryo 序列化,比 Java 默认的快 10 倍。
- 过滤无用数据:尽早做
filter,减少后续处理的数据量。
举个例子,我优化过一个用户画像作业:
// 优化前
val result = data.groupByKey().mapValues(_.sum)
// 优化后
val result = data.reduceByKey(_ + _)
就这一行改动,Shuffle 数据量从 200GB 降到 20GB,作业时间从 40 分钟降到 8 分钟。你想想看,这比调参数快多了。
1.3.3 Shuffle 调优
Shuffle 是 Spark 性能的「万恶之源」。为什么?因为 Shuffle 涉及磁盘 I/O、网络传输、序列化/反序列化,任何一个环节出问题,整个作业就卡住了。
Shuffle 调优的核心参数:
| 参数 | 作用 | 调优方向 |
|---|---|---|
spark.shuffle.partitions |
Shuffle 后的分区数 | 设为 CPU 核数的 2-3 倍 |
spark.shuffle.file.buffer |
Shuffle 写缓冲区大小 | 默认 32k,可调至 64k-128k |
spark.reducer.maxSizeInFlight |
Reducer 拉取数据缓冲区 | 默认 48m,网络好时可调大 |
spark.shuffle.sort.bypassMergeThreshold |
小分区数时跳过排序 | 默认 200,分区少时可调大 |
避坑指南:我曾经调一个 500 核的集群,spark.shuffle.partitions 设成 2000,结果每个 task 处理的数据量太小,调度开销比计算还大。后来改成 1000,性能反而提升了 30%。记住,分区数不是越大越好。
1.4 三个维度的关系
资源、代码、Shuffle 不是孤立的。举个例子:
- 代码写得烂 → Shuffle 数据量大 → 需要更多资源 → 资源不够就 OOM。
- 资源配得少 → 并行度低 → 每个 task 处理数据多 → Shuffle 压力大。
- Shuffle 参数不对 → 磁盘 I/O 瓶颈 → CPU 空转 → 资源浪费。
所以调优的时候,我习惯先看代码,再看 Shuffle,最后调资源。代码是源头,Shuffle 是瓶颈,资源是兜底。顺序搞反了,事倍功半。
好了,这一章咱们把调优的全景理清了。后面每一章都会深入一个具体场景,从参数到代码,从原理到实战,一步步带你成为团队里那个「能搞定问题」的人。