面试官问'数据倾斜怎么解决'——只会说加盐的人已经淘汰了
本文目录
先说结论
数据倾斜的本质是:某个Key的数据量远大于其他Key,导致处理这个Key的Task成为瓶颈。所有的解决方案都是围绕一个目标:让热点Key的数据分散到多个Task上并行处理。
解决数据倾斜有5种武器,不是只有加盐:Broadcast Join、AQE自动倾斜处理、加盐打散、两阶段聚合、过滤异常数据。不同场景用不同的方案,选错了反而更慢。
先排查是不是脏数据导致的倾斜:生产环境中,有相当一部分数据倾斜是因为脏数据——比如NULL值、测试数据、爬虫流量。这种情况先清洗数据就能解决,不需要上复杂方案。
先把这段讲顺;面试官追问时,再展开下面的原理和细节。
详细讲解
数据倾斜是大数据面试出现频率最高的技术题,没有之一。但很多人一上来就说'加盐打散',面试官追问'什么场景加盐不管用呢?'就卡住了。面试官想听的不是一个方案,而是你的完整排查思路:怎么发现倾斜→倾斜在哪个环节→什么原因导致的→选择什么方案→为什么选这个方案。我在生产环境处理过好几次数据倾斜,今天把完整的思路和实战经验分享出来。
怎么发现数据倾斜:排查三板斧
很多人知道数据倾斜的解决方案,但不知道怎么定位。面试时如果你能说清楚排查过程,比直接说方案更加分。
// 排查三板斧
// ① Spark UI看Task耗时分布
// 打开Spark UI → Stages → 点进最慢的Stage → 看Task列表
// 如果大部分Task 30秒完成,有个别Task跑了10分钟 → 典型倾斜
// 关注指标:Duration、Shuffle Read Size、Records
// ② 看数据分布
// 找出倾斜的Key和它的数据量
SELECT key_column, COUNT(*) AS cnt
FROM target_table
GROUP BY key_column
ORDER BY cnt DESC
LIMIT 20;
// ③ 判断是脏数据还是业务特性
// 如果Top Key是NULL / 空字符串 / 测试ID → 脏数据,直接过滤
// 如果Top Key是正常的业务值(如美国、热门商品)→ 业务特性,需要技术方案
方案一:Broadcast Join(最优先考虑)
如果数据倾斜发生在JOIN环节,而且有一张表比较小(几百MB以内),首选Broadcast Join。原理很简单:把小表广播到所有节点,大表不需要Shuffle,直接在本地JOIN。
-- 方法1:Spark SQL的Hint
SELECT /*+ BROADCAST(small_table) */
a.order_id, a.amount, b.product_name
FROM orders a
JOIN products b ON a.product_id = b.product_id;
-- 方法2:调大自动广播阈值
SET spark.sql.autoBroadcastJoinThreshold = 200m;
-- 默认10MB,200MB以下的表自动广播
-- 适用条件:小表 < 200-500MB
-- 优点:完全避免Shuffle,不存在倾斜问题
-- 局限:两张都是大表时不可用
我的经验:在某出海硬件品牌,我把autoBroadcastJoinThreshold从默认的10MB调到200MB后,有3个核心作业的维度表JOIN自动变成了Broadcast,作业时间减少40%。这是投入产出比最高的优化。
方案二:AQE自动倾斜处理(Spark 3.0+)
如果你用的是Spark 3.0以上版本,AQE(Adaptive Query Execution)可以自动处理数据倾斜。它的原理是:在运行时检测到某个分区数据量远大于其他分区时,自动把大分区拆成多个小分区。
-- 开启AQE(Spark 3.0+默认开启,Databricks默认开启)
SET spark.sql.adaptive.enabled = true;
SET spark.sql.adaptive.skewJoin.enabled = true;
-- 倾斜判断条件(可调整)
-- 分区大小 > 中位数 × skewedPartitionFactor(默认5)
-- 且 分区大小 > skewedPartitionThresholdInBytes(默认256MB)
SET spark.sql.adaptive.skewJoin.skewedPartitionFactor = 5;
SET spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes = 256m;
-- 优点:完全自动,不需要改SQL
-- 局限:只处理Sort Merge Join的倾斜,
-- 对GROUP BY的聚合倾斜不处理
我在某出海硬件品牌遇到的设备激活报表倾斜(美国数据占60%),就是AQE自动处理的。从40分钟降到8分钟,一行代码都没改。但注意,AQE只能处理JOIN倾斜,如果你的倾斜发生在GROUP BY聚合环节,需要用下面的方案。
方案三:加盐打散(适合GROUP BY倾斜)
加盐是最经典的手动倾斜解决方案,适合GROUP BY聚合场景。原理是给热点Key加一个随机后缀,把一个大分区拆成多个小分区。
-- 两阶段聚合:先加盐打散聚合,再去盐汇总
-- 第一阶段:给热点Key加随机后缀,分散到多个分区
SELECT
CONCAT(country, '_', CAST(FLOOR(RAND() * 10) AS STRING)) AS salted_key,
SUM(amount) AS partial_sum,
COUNT(*) AS partial_count
FROM orders
GROUP BY CONCAT(country, '_', CAST(FLOOR(RAND() * 10) AS STRING));
-- 第二阶段:去掉盐值,汇总最终结果
SELECT
SPLIT(salted_key, '_')[0] AS country,
SUM(partial_sum) AS total_amount,
SUM(partial_count) AS total_count
FROM first_stage_result
GROUP BY SPLIT(salted_key, '_')[0];
-- 效果:原来1个Task处理美国60%的数据
-- 现在分散到10个Task各处理6%,并行度提升10倍
适用场景:GROUP BY聚合的倾斜,特别是COUNT、SUM等可分拆的聚合操作。
不适用:COUNT DISTINCT不能直接加盐(去重逻辑不能分两阶段),需要改成approx_count_distinct或用其他方案。
方案四:过滤异常数据(最容易被忽略)
生产环境中,很多数据倾斜是脏数据导致的。比如NULL值、空字符串、测试设备ID等。这种情况不需要加盐,直接过滤就行。
-- 先检查是否有异常Key
SELECT device_id, COUNT(*) AS cnt
FROM events
GROUP BY device_id
ORDER BY cnt DESC LIMIT 10;
-- 如果发现某个device_id占了30%的数据,先查看是否为测试设备
-- 我在出海硬件公司遇到过:某个测试设备的device_id占了30%的数据
-- 解决方案:
-- ① 如果是测试数据,直接过滤
SELECT * FROM events
WHERE device_id NOT IN ('test_device_001', 'test_device_002')
AND device_id IS NOT NULL;
-- ② 如果NULL值太多,单独处理
-- 把NULL值的记录单独走一个分支,非NULL的正常处理
我在某出海硬件品牌遇到的真实情况:某个测试设备的device_id在事件表中占了总数据量的30%。这不是业务特性,是测试数据没清理。过滤掉之后,原来的倾斜问题就不存在了,比任何技术方案都简单直接。
五种方案的对比和选择指南
| 方案 | 适用场景 | 优点 | 局限 |
|---|---|---|---|
| Broadcast Join | JOIN倾斜,有一张小表(<500MB) | 完全避免Shuffle,效果最好 | 两张大表不适用 |
| AQE自动处理 | Spark 3.0+ JOIN倾斜 | 全自动,不改SQL | 只处理JOIN倾斜,不处理聚合倾斜 |
| 加盐打散 | GROUP BY聚合倾斜 | 通用性强,适合SUM/COUNT | COUNT DISTINCT不能直接用,SQL复杂度增加 |
| 两阶段聚合 | 大规模聚合倾斜 | 效果好,原理清晰 | 需要两次Shuffle,对网络要求高 |
| 过滤异常数据 | 脏数据导致的倾斜 | 最简单直接 | 只适用于脏数据场景 |
面试时的推荐讲法:先说排查思路(Spark UI + 数据分布检查),再按优先级推荐方案:过滤脏数据 → Broadcast Join → AQE → 加盐/两阶段聚合。这个顺序体现了'从简单到复杂'的工程思维。
面试高频追问
追问一:JOIN倾斜和GROUP BY倾斜有什么区别?处理方式有什么不同?
JOIN倾斜是因为某个Join Key的数据量在大表中占比过高,导致处理这个Key的Task数据量远大于其他Task。处理方式首选Broadcast Join(如果有小表)或AQE。GROUP BY倾斜是因为某个聚合Key的数据量远大于其他Key。处理方式首选加盐打散或两阶段聚合。两种倾斜的排查方法一样——看Spark UI的Task耗时分布和数据量分布——但解决方案不同。
追问二:加盐的盐值设多大合适?设太大有什么问题?
盐值通常设10-100之间。设太小达不到打散效果,设太大会产生过多的小分区,第二阶段的聚合成本增加。我的经验是:先看热点Key的数据量是平均值的多少倍,如果是10倍就加盐10,如果是50倍就加盐50。目标是让打散后每个分区的数据量接近平均水平。设太大的问题是:第二阶段的GROUP BY会有更多的分区需要聚合,增加了一次额外的Shuffle开销。
追问三:如果JOIN的两张表都很大,都有倾斜,怎么办?
这是最复杂的场景。我的思路是三步走:第一步,看能不能通过谓词下推提前过滤数据减小表的大小;第二步,如果过滤后还是很大,考虑对倾斜的Key和非倾斜的Key分开处理——倾斜的Key单独拎出来做Broadcast Join(只广播这些Key对应的数据),非倾斜的Key走正常的Sort Merge Join;第三步,如果数据量实在太大,考虑从架构上优化,比如预先把大表按Key做预聚合,减少参与JOIN的数据量。
实战踩坑
我在某知名出海硬件品牌遇到过一个'假倾斜'的情况。有个作业突然变慢,我看Spark UI发现某个Task的数据量是其他Task的8倍。按经验判断是数据倾斜,我准备加盐打散。
但在排查数据分布时,我发现这个Task处理的Key并不集中在某一个值上,而是因为AQE在合并小分区时,把几个小分区合成了一个大分区。这不是数据倾斜,是分区合并导致的不均匀。
-- 排查过程
-- 看到某个Task的Shuffle Read Size是其他的8倍
-- 但检查Key分布发现并没有明显的热点Key
-- 原因:AQE的coalescePartitions把多个小分区合并了
-- 合并逻辑是按顺序合并,导致某些合并后的分区偏大
-- 解决:调整AQE的合并参数
SET spark.sql.adaptive.advisoryPartitionSizeInBytes = 128m;
-- 限制合并后的分区不超过128MB
-- 调整后分区大小趋于均匀,作业恢复正常
教训:不是所有的Task耗时不均匀都是数据倾斜。定位问题时一定要看数据分布,确认是某个Key的数据量过大,还是分区策略导致的不均匀。对症下药才有效。
总结
数据倾斜的排查思路:Spark UI看Task分布 → 查数据分布找热点Key → 判断是脏数据还是业务特性。解决方案按优先级:过滤脏数据(最简单)→ Broadcast Join(JOIN场景首选)→ AQE自动处理(Spark 3.0+)→ 加盐打散(聚合场景)。面试时先说排查过程再说方案,展示你的完整思路而不是背答案。记住:会定位问题比会背方案值钱得多。
下一篇聊一个项目经验中必考的选型问题:面试官问你'为什么选Spark/Flink'——怎么回答才不像在背官方文档?
你在生产环境遇到过什么样的数据倾斜?用了什么方案解决的?评论区聊聊~
关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长
项目经验面试通关手册
第6-10篇:技术选型、架构设计与实战踩坑
Wood聊大数据
9年大数据老兵,聊技术、聊职场、聊成长