61.4 数据倾斜与调优
一句话
倾斜不是 “数据多”,而是 “某个 key/分区特别多”:总数据量没变,但 99 个 task 几分钟跑完、1 个 task 跑两小时——定位靠 “各 task 输入字节的 max/avg 比”,治理靠 “加盐、预聚合、拆热 key”。
先定位,再治理
| 信号 | 看什么 | 判据 |
|---|---|---|
| task 输入字节 | Spark UI 的 Input Size / Shuffle Read Size | max/avg > 2 即有明显倾斜 |
| task 耗时 | 同一 stage 内 task 时长分布 | 少数 task 是其他的 5–10 倍 |
| 热点 key | 采样统计 top-N key 的计数 | 单个 key 占分区数据 > 30% |
| 数据分布 | 分区键的基数与频次 | 低基数或 Zipf 分布 |
实测:加盐把倾斜压下去
20 万条记录、Zipf α=1.3 的 key、10 个 reduce:
| 方案 | 最大分区 | 平均 | 倾斜比 |
|---|---|---|---|
| 未加盐 | 65,289 | 20,000 | 3.3× |
加盐 10(key#0..9) | 30,678 | 20,000 | 1.5× |
| 加盐 100 | 22,768 | 20,000 | 1.1× |
加盐的本质是 “把一个热 key 拆成 N 个”,让它们分散到不同 task;代价是读的时候要再合并(多一次聚合),所以只对 “可合并的聚合/计数” 好用,对 “按 key 的强顺序” 不适用。
-- 两阶段聚合:先打散局部聚合,再按原 key 汇总
SELECT k, sum(cnt) FROM (
SELECT concat(k, '#', cast(rand()*10 as int)) AS k, count(*) AS cnt
FROM t GROUP BY concat(k, '#', cast(rand()*10 as int))
) GROUP BY k;
六种治理手段
| 手段 | 适用 | 代价 |
|---|---|---|
| 加盐拆分 | 聚合/计数类热点 key | 多一次合并 |
| map 端预聚合 | 可结合律的聚合 | 无(应优先) |
| 广播小表 | join 小表 | 内存 |
| 拆分热 key | 大 key(如大 V 粉丝列表) | 业务改造 |
| 两阶段聚合 | SQL 层倾斜 | 多一次 shuffle |
| 单独处理热点 | 极端热点(如秒杀) | 特判逻辑 |
反模式
| 反模式 | 后果 |
|---|---|
| 不看 max/avg 就 “加资源” | 倾斜 task 仍是一个,加机器无效 |
用 rand() 打散后不再合并 | 结果错(每个子 key 各算各的) |
| 对 “有序需求” 加盐 | 顺序被打乱,业务出错 |
| 只在 SQL 层调参 | 数据分布问题不会因参数变好 |
| 忽略 “空 key / null 倾斜” | 大量 null 落到同一分区 |
| 不监控倾斜比 | 每次跑得慢都归因于 “数据量大” |
三条铁律
🔑 先量化倾斜比(max/avg),再动手。
🔑 加盐必须配 “二次合并”,否则结果错。
🔑 能预聚合就别 shuffle 全量;加盐是最后手段。
自检
Q1 为什么 “加资源” 治不了数据倾斜?
▸ 答案:因为倾斜是单个 task 数据量过大:加机器只是增加 “可并行的槽位”,而那个热 key 仍然只能由一个 task 处理(同一个 key 必须落到同一分区)。除非改变分区方式(加盐/拆分),否则热点 task 的时长不变。
Q2 加盐之后为什么必须 “二次合并”?
▸ 答案:因为加盐把 key 拆成了 key#0..N,每个子 key 各自聚合出部分结果;最终答案需要按原 key 再汇总一次。少了这一步,结果会变成 “N 份部分和”,业务上就是错的——这也是加盐最容易踩的坑。
Q3 为什么 null 值也会造成倾斜?
▸ 答案:因为 null 在哈希分区里全部落到同一个分区:如果表里有大量 null(如未登录用户、未关联订单),它们会集中到一个 task,形成 “空值热点”。常见处理是 “给 null 打随机后缀” 或 “过滤后再单独处理”。
Q4 什么情况下不能用加盐?
▸ 答案:需要按 key 保序或 key 本身有业务含义的场景:如按用户顺序回放事件、按 key 做去重。加盐会破坏顺序与唯一性语义,只能用 “拆热 key + 单独处理” 或 “两阶段聚合 + 最终排序”。
Q5 倾斜比多少算 “必须处理”?
▸ 答案:经验上 max/avg > 2 就值得看,> 5 必须处理:因为 stage 的完成时间由最慢 task 决定,3× 倾斜就意味着 “这段至少多花 2 倍时间”。但也要看绝对值:数据量很小时,倾斜的绝对浪费可以忽略。
Q6 广播小表为什么能缓解 “join 倾斜”?
▸ 答案:因为它把 join 变成 map 端本地关联:大表的每个分区各自与小表 join,不再按 key 重分布大表,热 key 也就不会集中到一个 reduce task。前提是小表能放进内存,且 join 键在小表侧没有极端热点。
Q7 如何验证 “倾斜治理有效”?
▸ 答案:看三个前后对比:倾斜比(max/avg 应下降)、stage 时长(最慢 task 应接近平均)、总 shuffle 字节(加盐可能略微增加)。只降了 “平均时长” 而 max 没降,说明治理没打中热点。