数据倾斜
总结
| 记忆点 | 内容 |
|---|---|
| 倾斜本质 | 少数Key数据量远大于其他,拖慢整个任务 |
| NULL倾斜 | COALESCE随机打散或拆分处理 |
| 小表JOIN | MapJoin广播,不走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直接填默认值。」 |
|---|