订阅更新

数据倾斜

数仓建模手册 第 17 题 约 5 分钟读完

本文目录
  1. 总结
  2. 什么是数据倾斜?
  3. 三种常见倾斜场景
  4. 四种解决方案
  5. 怎么发现倾斜?

总结

记忆点内容
倾斜本质少数Key数据量远大于其他,拖慢整个任务
NULL倾斜COALESCE随机打散或拆分处理
小表JOINMapJoin广播,不走Shuffle
热点Key加盐打散(两阶段聚合)
发现方法Task耗时不均匀,Web UI看数据量分布

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

进阶 | 大数据面试 · 数仓建模系列

面试真题
你们数仓ETL任务有没有遇到过数据倾斜?是怎么发现和解决的?

什么是数据倾斜?

一句话:大部分数据集中在少数几个Key上,导致个别Task处理的数据量远大于其他Task,整个任务被拖慢。

举个例子:按城市分组统计GMV,北京、上海的订单占了80%,其他300个城市只占20%。Shuffle之后处理北京的那个Task要算几千万条数据,而处理拉萨的Task只有几百条。整个任务的耗时取决于最慢的那个Task——木桶原理。

Wood的实战经验
我在元宇宙社交公司做实时数仓时,有个Flink任务按user_id做keyBy,发现有个「系统用户」承载了所有未登录流量,一个Key的数据量是其他Key的1000倍,直接把一个TaskManager的内存打爆了。
这个问题排查了一整天。表面上看是OOM,实际上是数据倾斜。后来把「系统用户」的流量单独拆出来处理,问题就解决了。

三种常见倾斜场景

场景原因表现
NULL值倾斜大量JOIN键为NULL所有NULL分到同一个Reducer
热点Key倾斜某些Key数据量极大(大V、系统账号)个别Task耗时是平均的100倍
JOIN倾斜大表JOIN大表,某些Key两边都很多笛卡尔积爆炸+OOM

四种解决方案

方案一:NULL值单独处理

最简单的倾斜场景。两种处理方式:把NULL随机打散(COALESCE(user_id, CONCAT('null_', RAND()))),或者更干净地拆成两部分:non-NULL正常JOIN,NULL部分不JOIN直接填默认值。我推荐后者,逻辑更清晰。

方案二:MapJoin(小表广播)

维度表小于500MB时,直接广播到Map端,不走Shuffle,自然不倾斜。在Hive里设置SET hive.auto.convert.join=true即可自动开启。这是最省心的方案,适用于大表JOIN小表的场景。

-- 小表会被加载到内存,不走Shuffle
SET hive.auto.convert.join=true;
SET hive.mapjoin.smalltable.filesize=500000000; -- 500MB
SELECT /*+ MAPJOIN(dim) */ f.*, d.name
FROM fact f JOIN dim d ON f.user_id = d.user_id;

方案三:加盐打散(两阶段聚合)

热点Key的核心解法。思路是给热点Key加随机后缀,分散到多个Reducer分别求和,然后去掉后缀做最终汇总。

-- 第一步:加盐聚合(打散到10个Reducer)
SELECT CONCAT(city, '_', FLOOR(RAND()*10)) AS city_salt,
SUM(amount) AS partial_sum
FROM fact_order
GROUP BY CONCAT(city, '_', FLOOR(RAND()*10));
-- 第二步:去盐汇总
SELECT SPLIT(city_salt, '_')[0] AS city,
SUM(partial_sum) AS total_amount
FROM step1_result
GROUP BY SPLIT(city_salt, '_')[0];

方案四:倾斜Key单独处理

热点Key和普通Key分开跑,最后UNION ALL。热点Key可以用MapJoin或更高并行度,普通Key正常JOIN。适用于已知哪些Key是热点的场景。

怎么发现倾斜?

看任务监控:如果大部分Task在5分钟内完成,但有1-2个Task跑了50分钟,基本就是倾斜了。Hive/Spark的Web UI里能看到每个Task的数据量分布。Flink的话看反压监控,如果某个subtask反压严重而其他正常,大概率是倾斜。

面试加分提示
高分回答要场景+发现方式+具体方案:
「我遇到过一次GROUP BY倾斜。按城市统计GMV,北京和上海的数据占了80%,其他城市很少。任务大部分Task 3分钟完成,但北京那个跑了40分钟。我用两阶段聚合解决:第一步给城市Key加随机后缀打散到10个Reducer分别求和,第二步去掉后缀做最终汇总。优化后任务从40分钟降到6分钟。」
「还遇到过NULL值倾斜:LEFT JOIN时大量user_id为NULL的记录全分到同一个Reducer。解决方案是把NULL记录拆出来单独处理,不走JOIN直接填默认值。」
刷题进度保存在当前浏览器