Spark 批处理调优这点事:资源怎么要、Shuffle 怎么省、序列化怎么选?我用这些年踩过的坑告诉你
大家好,这里是大数据圈里“被 Yarn 吊着打过、和 Spark 吵过架”的 Echo_Wish。
今天咱们来聊聊 Spark 批处理作业的调优。别听到“调优”两个字就觉得高大上,它其实本质就是三句话:
资源别瞎要、Shuffle 别乱搞、序列化别拖后腿。
你能把这三件事整明白,70% 的性能问题都会给你让路。剩下 30% 基本靠缘分和运维同事心情。
这篇文章,我想和你聊点真东西:你能立刻用、立刻见效、能救你半夜线上跑不动作业的那种优化技巧。
一、资源调优:要得对比要得多更重要
Spark 作业的资源问题,大部分不是资源少,而是资源分配的方式完全不对。
1. Executor 数量和 CPU 核心到底怎么配?
这里给你一个我自己线上测试无数次得出的“黄金思路”:
Executor 内核数别超过 5~6 个
因为再多 GC 压力大、上下文切换更大、反而越跑越慢。Executor 数量比核心数量更重要
小 Executor + 多 Executor 往往比 大 Executor + 少 Executor 更稳。
例如,你的集群有 50 核想全用上,不要这样配:
# 典型的错误配置:又大又笨
--executor-cores 10
--num-executors 5
应该这样:
# 典型的性能更优配置:小而精,数量多
--executor-cores 5
--num-executors 10
为什么?
因为大 Executor 会导致:
- GC 时间飙升
- Shuffle 文件争抢
- Task 并发不均衡
而小 Executor 则让资源调度更灵活、稳定性更高。
2. Executor 内存不是越大越好
很多朋友喜欢直接开 20G、30G 内存的 Executor,我只能说:
你这是把自己的作业推进了 GC 炼狱。
合理内存一般在 6~12G 之间,特别是 ETL 型作业。
万一你非要调大?那至少要打开:
--conf spark.memory.fraction=0.6
--conf spark.memory.storageFraction=0.3
否则别怪 Spark 动不动 OOM。
二、Shuffle 调优:能绕开就绕开,绕不开就优化
Shuffle 是 Spark 性能最大的杀手,没有之一。
凡是经过 Shuffle 的地方,都会:
- 打断 pipeline
- 写磁盘、读磁盘
- 网络传输
- 排序 & 聚合
你要做的不是“优化 Shuffle”,而是“减少 Shuffle”!
1. 尽量避免两个最致命的操作:groupByKey 和 join
groupByKey 是性能灾难,因为它会把 key 相同的所有 value 拉到同一个 Executor。
举个例子:
// 千万别这么写!这是 shuffle 地狱!
rdd.groupByKey()
正确写法应该使用 reduceByKey / aggregateByKey:
// reduceByKey 会在 Map 端先进行预聚合,极大减少 shuffle 数据量
rdd.reduceByKey(_ + _)
2. join 一定要提前 broadcast 小表,减少 shuffle
传统 join 如下,一定会 shuffle:
df1.join(df2, "id")
但如果 df2 是小表(小于 300MB),你应该这样写:
import org.apache.spark.sql.functions.broadcast
df1.join(broadcast(df2), "id")
本质就是:凡是小表 join,大胆 broadcast。
3. Shuffle 分区(spark.sql.shuffle.partitions)是性能关键
Spark 默认是 200 分区,这个值对大多数作业来说过大或过小。
我的经验:
| 数据量 | 推荐分区数 |
|---|---|
| < 10GB | 50~100 |
| 10~200GB | 200~500 |
| > 200GB | 500~2000 |
设置方式:
--conf spark.sql.shuffle.partitions=300
千万不要盲目调大分区,否则:
- 分区太多 → 调度时间爆炸
- 分区太少 → 单分区数据倾斜
三、序列化:Spark 性能的隐形加速器
Spark 默认使用 Java 序列化,效率低、体积大,非常影响 shuffle 和 cache 效率。
一句话:强烈建议使用 Kryo。
开启 Kryo:
--conf spark.serializer=org.apache.spark.serializer.KryoSerializer
--conf spark.kryo.registrationRequired=false
如果你有自定义 class,可以手动注册,加速更多:
val conf = new SparkConf()
conf.registerKryoClasses(Array(classOf[MyClass]))
为什么 Kryo 这么重要?
- 序列化速度比 Java 快 5~10 倍
- 体积小 30%~60%
- 直接提升 shuffle 和 cache 性能
我自己线上 ETL 作业切到 Kryo 后,整体性能提升了 25% 左右,非常划算。
四、一个真实案例:从 1 小时优化到 14 分钟
某业务的明细拉链作业每日增量约 80GB,最初 60 分钟跑不完。
我调优后压到了 14 分钟。优化手段如下:
1. 资源重配
从:
--executor-cores=8
--executor-memory=20G
--num-executors=10
改成:
--executor-cores=5
--executor-memory=10G
--num-executors=18
GC 秒降。
2. Broadcast 小表
原来:
df1.join(df2, Seq("uid", "date"))
改为:
df1.join(broadcast(df2), Seq("uid", "date"))
Shuffle 直接少一半。
3. Spark Shuffle 分区调小
从默认 200 → 调整为 300
结合 80GB 数据量效果刚刚好。
4. 序列化改为 Kryo
直接减少 shuffle 文件大小,带来 10~15% 性能收益。
五、最后说一句:调优没有银弹,但有套路
Spark 调优看起来复杂,其实就三个方向:
- 让资源配置更合理
- 减少 Shuffle,或者让 Shuffle 更轻量
- 让数据体积更小,序列化更高效
你只要掌握这三点,任何批处理作业你都能找到优化点。
而调优最核心的本质就是一句话:
你要理解 Spark 的执行方式,而不是调一堆参数希望它变快。
代码写得再优雅、框架再先进,只要你无脑 groupByKey、无脑大 Executor、无脑默认序列化,性能肯定救不回来。