1. Shuffle数据倾斜的本质
数据倾斜这个问题,我入行第三年才真正「吃透」。当时线上一个 30 分钟的任务,跑了一个半小时还没结束,最后 OOM 挂掉了。排查下来,就是某个 key 的数据量是其他 key 的几百倍。嗯,从那天起,我养成了一个习惯:但凡涉及 Shuffle 的任务,先看一眼 key 分布。
1.1 什么是数据倾斜
说白了,数据倾斜就是数据分布不均匀。在分布式计算里,数据被分到不同的分区去处理。理想情况下,每个分区的数据量差不多,大家同时跑完。但现实往往是——某个分区扛了 80% 的数据,其他分区闲得发慌。
核心定义:数据倾斜是指在分布式计算过程中,数据在各个分区之间的分布严重不均,导致部分任务处理的数据量远大于其他任务的现象。
举个例子。你有一张订单表,按用户 ID 做聚合。普通用户一年下 10 单,但某个大客户一年下了 100 万单。这个用户的 ID 就会成为「倾斜 key」。所有跟它相关的计算,都会压到同一个分区上。
为什么会这样?因为分布式计算的分区策略,通常是基于 key 的哈希值取模。同一个 key 永远落在同一个分区。key 本身的数据量越大,那个分区就越倒霉。
1.2 Shuffle 阶段的工作原理
要理解数据倾斜,必须先搞懂 Shuffle。Shuffle 是 Spark 里最「重」的阶段,也是问题最多的地方。
我习惯把 Shuffle 拆成三个子阶段来看:
- Map 端写数据——每个 Map 任务处理完数据后,按分区号排序,写到本地磁盘。
- 网络传输——Reduce 任务从各个 Map 任务的磁盘上拉取属于自己的数据。
- Reduce 端聚合——Reduce 任务拿到数据后,做分组、聚合、排序等操作。
你想想看,如果某个 key 的数据量特别大,它在 Map 端写的时候,对应的分区文件就特别大。到了网络传输阶段,那个分区的数据要占用大量带宽。最后到 Reduce 端,那个任务的内存压力直接爆表。
1.3 数据倾斜的典型表现
数据倾斜不会直接告诉你「我倾斜了」。它通常通过两种现象暴露出来:OOM 和 任务长尾。
1.3.1 OOM(内存溢出)
这是最要命的。某个 Executor 突然挂掉,日志里写着 java.lang.OutOfMemoryError。为什么会 OOM?
我遇到过这样一个案例:一个 groupBy 操作,某个 key 对应的 value 列表有 200 万条。Spark 默认的 Shuffle 读缓冲区是 48MB,但那个分区拉过来的数据有 2GB。缓冲区根本装不下,直接撑爆。
避坑指南:我曾经在调优时只关注了 spark.executor.memory,忽略了 Shuffle 读缓冲区的大小。结果内存给了 8GB,任务还是 OOM。后来才发现,spark.shuffle.file.buffer 和 spark.reducer.maxSizeInFlight 这两个参数才是关键。
OOM 的典型场景:
- groupByKey / reduceByKey——某个 key 的 value 列表过大
- join——大表 join 小表,但小表某个 key 的数据量异常大
- distinct / countDistinct——倾斜 key 导致单个分区需要处理海量去重
- 窗口函数——
row_number() over(partition by ...)中分区键倾斜
1.3.2 任务长尾
长尾比 OOM 更隐蔽。任务没有挂,但就是跑不完。你打开 Spark UI,看到 99% 的任务已经完成,剩下 1% 的任务进度条卡在 99%。
我记得有一次帮业务团队排查,一个 200 个分区的任务,199 个分区在 5 分钟内跑完,最后一个分区跑了 45 分钟。那个分区处理的数据量,是其他分区的 30 倍。
| 指标 | 正常分区 | 倾斜分区 | 差距倍数 |
|---|---|---|---|
| 数据量(条) | 50 万 | 1500 万 | 30x |
| Shuffle 读(MB) | 120 | 3600 | 30x |
| 执行时间 | 4 分 20 秒 | 43 分 10 秒 | 10x |
| GC 时间 | 12 秒 | 8 分 30 秒 | 42x |
长尾带来的连锁反应:
- 资源浪费——其他 Executor 空闲,但集群资源被长尾任务占着
- 延迟放大——下游任务依赖这个 stage 的输出,整个 DAG 被拖慢
- 稳定性下降——长尾任务更容易触发 GC,GC 又进一步拖慢处理速度,形成恶性循环
快速判断方法:在 Spark UI 的 Stages 页面,看「Shuffle Read Size / Records」这一列。如果某个任务的数值明显高于中位数(比如 5 倍以上),基本可以断定存在数据倾斜。
1.4 数据倾斜的根因分类
根据我的经验,数据倾斜的根因可以归为三类。搞清楚是哪一类,才能对症下药。
- Key 本身倾斜——数据天然分布不均,比如热点用户、热门商品、空值 key
- 分区策略不当——默认的 hash 分区无法应对倾斜 key,需要自定义分区器
- 业务逻辑导致——比如 join 时小表广播失败、groupBy 的维度粒度过粗
你想想看,第一类是最常见的。空值 key 尤其坑人。我曾经遇到一个 case,某个字段 60% 是 null,groupBy 之后所有 null 都聚到一个分区,那个分区直接炸了。解决方案很简单——给 null 加随机后缀。