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 作业从提交到结束,经历了什么?我习惯把它拆成四个阶段:

  1. 提交阶段:Driver 解析用户代码,构建 DAG(有向无环图)。
  2. 调度阶段:DAGScheduler 将 DAG 切分成 Stage,TaskScheduler 把 Task 分发到 Executor。
  3. 执行阶段:Executor 拉取数据、执行计算、落盘中间结果。
  4. 收尾阶段:结果聚合、资源释放、日志清理。

每个阶段都有调优点。比如提交阶段,你写的 groupByKey 和 reduceByKey,生成的 DAG 完全不同。我见过有人用 groupByKey 处理 10 亿条数据,shuffle 写了几百 GB 磁盘,换成 reduceByKey 后,数据量直接降到十分之一。

一个小技巧:在 Spark UI 的 DAG 可视化页面,看一眼 Stage 数量。如果 Stage 超过 10 个,大概率有优化空间。

下面这张图,帮你快速理解作业的生命周期:

Spark 作业生命周期 提交阶段 用户代码 → DAG 调度阶段 DAG → Stage → Task 执行阶段 计算 + Shuffle + 落盘 收尾阶段 结果聚合 + 释放 各阶段调优要点: • 提交阶段:避免宽依赖算子(如 groupByKey),优先用 reduceByKey / aggregateByKey • 调度阶段:控制并行度(spark.sql.shuffle.partitions),减少 Stage 数量 • 执行阶段:监控 GC 和 Shuffle 读写,合理分配内存比例 • 收尾阶段:及时清理缓存,避免资源泄漏

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 是瓶颈,资源是兜底。顺序搞反了,事倍功半。

好了,这一章咱们把调优的全景理清了。后面每一章都会深入一个具体场景,从参数到代码,从原理到实战,一步步带你成为团队里那个「能搞定问题」的人。