解决方案一:增大Shuffle分区数(spark.sql.shuffle.partitions)的原理与适用场景

数据倾斜这个问题,说白了就是「忙的忙死,闲的闲死」。

你想想看,几百个 Task 一起跑,大部分几秒就结束了,偏偏有那么一两个 Task 要跑十几分钟甚至半小时。我刚开始调优那会儿,看到这种场景就头大——明明集群资源够用,可作业就是快不起来。

那有没有一种最简单、最暴力的解法?有,就是我今天要讲的——增大 Shuffle 分区数。

核心原理:把大包拆成小包

先看一张图,你就明白为什么增大分区能缓解倾斜。

增大 Shuffle 分区数缓解数据倾斜原理 倾斜 Key(100万条) 普通 Key A(5万条) 普通 Key B(3万条) 普通 Key C(2万条) Shuffle 分区数=200 分区1:5万条(倾斜) 分区2:5万条(倾斜) 分区3:5万条(倾斜) 分区4:5万条(倾斜) 分区5~200:正常 💡 关键理解 倾斜 Key 的 100 万条数据,原本集中在一个分区(200分区时,每个分区约 5 万条)。 如果增大到 2000 分区,每个分区只分到 500 条——倾斜被「稀释」了。

原理其实很简单:Shuffle 分区数决定了 Reduce 端有多少个 Task 来消费数据。默认值是 200,也就是说,不管你的数据量是 1GB 还是 1TB,都只开 200 个 Task 去拉取。

当某个 Key 的数据量特别大时,它会被 Hash 到同一个分区。分区数越少,每个分区摊到的数据就越多,那个倒霉的 Task 就越吃力。

反过来,你把分区数从 200 调到 2000,甚至 10000,每个分区分到的数据量就大幅下降。倾斜 Key 虽然还是比别的 Key 大,但值变小了,Task 就能扛住了。

适用场景:什么时候该用这招?

这招不是万能的,但有几个场景特别适合:

  • 倾斜程度不极端:最大 Key 的数据量是平均值的 5~10 倍,而不是 100 倍、1000 倍。比如平均每个分区 5 万条,倾斜分区 50 万条,增大分区就能搞定。
  • 集群资源充足:你有足够的 CPU 和内存来跑更多的 Task。如果集群本来就紧巴巴,增大分区反而会因调度开销拖慢作业。
  • 快速验证:你刚接手一个作业,不确定倾斜原因,想先试试最简单的方案。我一般会先调这个参数,看效果再决定要不要上更复杂的方案。
  • 数据量不大但倾斜明显:比如总数据量只有几 GB,但某个 Key 占了 80%。这时候增大分区,每个 Task 处理的数据量依然在合理范围内。

怎么用?代码就一行

// 在 SparkConf 中设置
val spark = SparkSession.builder()
  .appName("ShuffleSkewDemo")
  .config("spark.sql.shuffle.partitions", "800")
  .getOrCreate()

// 或者在 SQL 中动态设置
spark.sql("SET spark.sql.shuffle.partitions = 800")

嗯,就这么简单。但我要提醒你——别乱设。

⚠️ 常见误区

  • 分区数越大越好? 错。每个 Task 都有调度开销、序列化开销、网络开销。分区数太多,Task 处理的数据量太小,反而浪费资源。
  • 设成 10000 肯定快? 不一定。我见过有人把分区数设到 50000,结果每个 Task 只处理几百条数据,大部分时间花在调度和 Shuffle 文件读写上,作业反而变慢了。
  • 只调这一个参数就够了? 不够。增大分区只是「稀释」倾斜,不是「消除」倾斜。如果倾斜 Key 的数据量是其他 Key 的 100 倍,增大分区只能让每个分区从 100 万降到 10 万,但其他分区只有 1000 条——差距依然存在。

分区数设多少合适?

我个人的经验是:让每个 Task 处理 100MB~200MB 的数据。这个范围是经过大量实践验证的黄金区间——数据量够大,能充分利用 CPU 和内存;又不会太大,避免 OOM 或 GC 频繁。

总 Shuffle 数据量 推荐分区数范围 每个 Task 处理量
1 GB 8 ~ 16 62 ~ 125 MB
10 GB 80 ~ 160 62 ~ 125 MB
100 GB 800 ~ 1600 62 ~ 125 MB
1 TB 8000 ~ 16000 62 ~ 125 MB

当然,这只是参考值。实际项目中还要考虑:

  • 集群总核心数:分区数最好是集群可用核心数的 2~3 倍,这样能充分利用并行度。
  • 数据分布特征:如果倾斜 Key 特别大,可以适当增加分区数,让倾斜分区进一步稀释。
  • Shuffle 文件数:分区数 × Mapper 数 = Shuffle 中间文件数。分区数太大,文件数暴增,可能撑爆磁盘或 NameNode。

💡 我的调试习惯

先看 Spark UI 的 Stages 页面,找到最慢的那个 Stage。看它的 Shuffle Read 总量,除以 128MB,得到一个基准分区数。然后在这个基础上,根据倾斜程度上调 2~5 倍。比如基准是 200,倾斜明显就调到 600~1000。跑一次看看效果,再微调。

我曾经在一个 500GB 的 ETL 作业上,把分区数从 200 调到 1200,作业时间从 45 分钟降到了 18 分钟。虽然没完全消除倾斜,但效果立竿见影。

什么时候这招不灵?

说实话,这招有它的天花板。遇到下面这些情况,你就别指望它了:

  • 极端倾斜:某个 Key 占了 99% 的数据。比如 1 亿条数据里,一个 Key 就有 9900 万条。就算你把分区数调到 10000,每个分区还有 9900 条,而其他分区只有 1 条——倾斜依然严重。
  • 数据膨胀严重:Shuffle 过程中有 join、groupBy 等操作导致数据量暴增。增大分区只能缓解,不能根治。
  • 资源受限:集群只有几十个核心,你设 10000 个分区,大部分 Task 都在排队等调度,实际并行度上不去。

遇到这些情况,就得用更高级的方案了——比如加盐、广播 join、自定义分区器。这些后面几章会详细讲。

小结一下

增大 Shuffle 分区数,是解决数据倾斜的「第一板斧」。它简单、快速、无侵入,适合作为调优的第一步。但你要清楚它的边界——它只是把大问题拆成小问题,不是让问题消失。

我个人习惯是:先调分区数,看效果;效果不明显,再上其他方案。毕竟,能用一行代码解决的问题,何必写几十行呢?