数据倾斜是什么
数据倾斜指参与同一类计算的数据在不同 key 之间分布极不均匀:绝大多数 key 只有几百条记录,个别 key 却有上千万条。分区与 task 是按 key 划分的,于是这些 key 落在的 task 成为长尾,拖住整个作业——其他 task 早已跑完,它还在读写磁盘。
怎么判断是倾斜还是别的问题
在作业监控里看 task 耗时分布:多数 task 秒级完成、少数 task 长时间运行甚至内存溢出,是典型倾斜特征。另一种是分区倾斜:源文件大小严重不均(例如一个大文件配若干小文件),表现是所有 task 都慢,需要的是重分区而不是处理热点键。
基本信息
| 表现 | 任务长尾、个别 task 内存溢出、spill 严重 |
|---|---|
| 成因 | 热点键、空键、数据分布天然不均、文件大小不均 |
| 定位手段 | task 耗时分布、按 key 计数、分区大小分布 |
| 处理次序 | 先过滤无效数据,再考虑打散或广播 |
| 平台提示 | Spark AQE 可自动处理部分 join 倾斜(默认开启) |
倾斜类型与对策
| 类型 | 典型场景 | 对策 |
|---|---|---|
| 分组聚合倾斜 | 少数 key 记录极多,如默认值、空值 | 过滤无效 key,或两阶段加盐聚合 |
| join 倾斜 | 大表关联小表且小表未广播 | 把小表广播,或对热 key 单独处理 |
| join 大键倾斜 | 两张大表按热点键关联 | 开启 AQE 倾斜 join 或手动拆分热键 |
| 分区/文件倾斜 | 源文件大小不均、分区数远大于数据量 | 重分区,调整并行度与文件切分 |
常见处理手段
- 先过滤:把空键、默认值、测试账号等无效数据剔除,往往能直接消掉大部分倾斜
- 加盐两阶段聚合:给热键加随机后缀做局部聚合,再去后缀合并
- 广播小表:小表放进内存,避免按 key 重新分布大表数据
- 单独处理热键:把热点 key 拎出来单独算,其余走常规路径,最后合并
- 调整并行度与重分区:应对文件级或分区级倾斜
- 开启 AQE:让引擎在运行期检测并拆分过大的分区
两阶段加盐聚合
两阶段加盐聚合
-- 阶段一:对热键加随机后缀,把同一个键打散到多个 task 做局部聚合
CREATE TABLE tmp_agg AS
SELECT concat(k, '#', cast(floor(rand() * 10) as string)) AS k_salt,
sum(v) AS part_sum
FROM src
GROUP BY concat(k, '#', cast(floor(rand() * 10) as string));
-- 阶段二:去掉后缀把局部结果合并起来
SELECT split(k_salt, '#')[0] AS k, sum(part_sum) AS total
FROM tmp_agg
GROUP BY split(k_salt, '#')[0];广播小表与开启 AQE
广播小表与开启 AQE
-- 手动提示广播:小表进内存,避免大表做 shuffle 重分布
SELECT /*+ BROADCAST(c) */ o.order_id, c.channel_name
FROM dwd_order o LEFT JOIN dim_channel c ON o.channel_code = c.channel_code;
-- 运行期自适应:自动合并小分区并拆分倾斜分区(Spark 3.2 起默认开启)
SET spark.sql.adaptive.enabled = true;
SET spark.sql.adaptive.skewJoin.enabled = true;
SET spark.sql.autoBroadcastJoinThreshold = 10485760; -- 广播阈值,默认 10MB加盐只能解决「计算被打散」,不能解决「结果本身不完整」:如果业务上这些键真的需要集中处理,加盐后仍需一次收敛步骤,整体耗时不会消失,只是从单点长尾变成可并行。
加盐用的分隔符若与业务 key 本身冲突(key 里含有同一个字符),阶段二的拆分就会解析出错误的键,结果与预期完全不符;选一个业务数据中不会出现的分隔符。