Spark SQL 小表聚合也跑不满:shuffle 分区数 200 的默认值要先算一算
先说一个常见场景
每天凌晨跑一张订单汇总:事实表按日期过滤后只剩几十万行,再按商品类目、渠道分组求和。数据不大,集群也不忙,可作业还是要跑好几分钟。打开 Spark UI 看 Stage 详情,会发现一个很典型的画面:聚合那一步起了 200 个任务,大部分任务只处理几 MB 数据、几秒就结束,整个 Stage 却要等最慢的那几个任务收尾,CPU 利用率始终上不去。
问题往往不在 SQL 写得对不对,而在一个很多人从没改过的参数上:spark.sql.shuffle.partitions。官方文档给它的默认值是 200,意思是每次 shuffle(分组、去重、排序、大表连接都会触发)都把数据切成 200 份,不管你的数据是 10 MB 还是 10 TB。这个默认值只是一个通用起点,照搬到自己的作业上,常常两头都不讨好。
一、200 太多和太少,都是同一种错
先把两种反方向的症状分清楚,调参时才不会来回摇摆。
数据量小的时候,分区太多是纯开销。几十万行数据被硬切成 200 份,每份只有几千行,任务启动、调度和 shuffle 文件读写的开销反而占了大头。更麻烦的是写出阶段:200 个分区常常对应 200 个小文件,下游再读这张表时又要面对一堆小文件,等于把一次调参失误传给了下一个作业。
数据量大的时候,分区太少是真风险。同样是 200 份,shuffle 数据量涨到几百 GB,每个分区就要扛几个 GB。单个任务内存吃紧就容易溢写到磁盘,速度一下子掉下来;倾斜的 key 再集中到某几个分区,整个 Stage 会被个别长尾任务拖住,表现为大部分任务早已结束、几个任务迟迟不完。
所以判断标准不是“200 大不大”,而是“每个分区最终要处理多少数据”。这个量级合适,任务数才合理。
二、先算再设:用一次实测结果定分区数
不需要凭感觉猜,直接用一次代表性运行的结果来算:
目标分区数 ≈ Shuffle 写出总量 ÷ 单分区目标大小
单分区目标大小取 128 MB 是一个常用的经验起点——它和 Spark 读文件时默认的最大分区字节数(spark.sql.files.maxPartitionBytes,默认 128 MB)处在同一个量级,便于对照理解,不是官方对 shuffle 分区的硬性规定。
举两个对照的例子。某次聚合的 Shuffle 写出总量是 12 GB,12 GB 除以 128 MB 约等于 96,取整设为 100 左右就比较合理,远比默认的 200 省任务开销。如果另一张大表的 Shuffle 写出总量是 640 GB,同样算法得到 5120,那就该把分区数往上调到 5000 上下,而不是继续用 200 硬扛。
落到代码里,PySpark 和 Spark SQL 的写法都可以直接抄用:
spark.conf.set("spark.sql.shuffle.partitions", "100")
result = (
orders.filter("order_date = '2026-10-28'")
.groupBy("category", "channel")
.agg({"amount": "sum", "order_id": "count"})
)
result.write.mode("overwrite").parquet("/warehouse/daily_order_summary")
SET spark.sql.shuffle.partitions = 100;
SELECT
category,
channel,
SUM(amount) AS total_amount,
COUNT(order_id) AS order_cnt
FROM orders
WHERE order_date = '2026-10-28'
GROUP BY category, channel;
这个参数要在查询执行前设置,对已开始的 Stage 不生效。改完后去 Spark UI 核对任务数与单任务数据量是否符合预期。
三、开了 AQE,也别把初始值设得太随意
Spark 3.2 起,自适应查询执行(AQE)默认开启,spark.sql.adaptive.enabled 的默认值为 true。AQE 有一个很实用的能力:合并 shuffle 后的小分区。也就是说,你可以先把初始分区数设得偏大一些,让 AQE 在运行时根据实际数据量把相邻的小分组合并,省去为每个作业精确算数的麻烦。
但这里有一个容易踩的默认项。AQE 合并分区时参考的目标大小是 spark.sql.adaptive.advisoryPartitionSizeInBytes,默认 64 MB;可另一个参数 spark.sql.adaptive.coalescePartitions.parallelismFirst 的默认值是 true,开启后合并时会忽略 64 MB 这个目标,只保证不低于 1 MB 的最小分区大小,目的是尽可能保住并行度。结果就是小分区可能合并得不够彻底,任务数仍然偏多。官方文档建议,在资源紧张的共享集群上把这个参数设为 false,让合并真正朝着目标大小走:
spark.conf.set("spark.sql.adaptive.enabled", "true")
spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "true")
spark.conf.set("spark.sql.adaptive.coalescePartitions.parallelismFirst", "false")
spark.conf.set("spark.sql.adaptive.advisoryPartitionSizeInBytes", "134217728")
spark.conf.set("spark.sql.shuffle.partitions", "1000")
这套组合的逻辑是:初始分区数给足上限,AQE 再按 128 MB 的目标往下合并。反过来要记住,AQE 只能合并、不能拆分一个已经过大的分区。所以初始值设得太小、大分区已经形成时,AQE 也救不回来,大数据量作业还是要先按第二节的公式把初始值算够。
四、分区数调对了,倾斜还要单独看
分区数合理,不等于倾斜消失。AQE 能够拆分倾斜的 shuffle 分区,但它主要针对连接场景;分组聚合里某个类目订单量一家独大,照样会形成长尾任务。这时候先别急着继续加分区数,那样只是把倾斜的分区切得更碎、并不能让同一个 key 的数据分开处理。
判断方法很直接:看 Stage 里各个任务的耗时和 Shuffle 读入量分布。如果绝大多数任务几秒结束、个别任务耗时是中位数的几倍甚至十几倍,且读入量也明显偏大,那就是倾斜,不是分区数问题。分组聚合的倾斜通常要在业务层处理,例如先把热点 key 单独拎出来聚合、再与普通 key 的结果合并;连接场景则先确认 AQE 的倾斜优化是否生效,再考虑加盐打散等手段。
五、上线前,用三组指标验收一次
改完参数不要只看总耗时变没变,总耗时受集群负载影响太大。建议固定看三组指标:
- Stage 任务数是否等于预期,分区数改动是否真的生效;
- 单任务的 Shuffle 读写量是否均匀,有没有个别任务读入量远超中位数;
- 写出文件数有没有跟着变好:小表聚合写出后不应再出现几百个 KB 级小文件,大表写出也不应出现单个文件异常偏大的情况。
同一份数据、同一条 SQL,分别用默认值 200 和调整后的值各跑一次,对比这三组指标,调参结论才站得住。以后数据量翻倍时,按同样的公式重算一遍即可,不用重新摸索。
小结
顺序记住就行:先看 Shuffle 写出总量,按 128 MB 经验目标算出分区数 → 小数据往下调、大数据往上调,不要照搬默认的 200 → 开了 AQE 就让它合并小分区,共享集群上注意把 parallelismFirst 设为 false → 最后用任务耗时和 Shuffle 读入量的分布单独排查倾斜。分区数不是越大越好,也不是越小越好,而是和你的数据量对得上才好。
- 点赞
- 收藏
- 关注作者
评论(0)