1. 一道Hive面试题背后的数据倾斜排查思路
面试题这种东西,做过的人都知道,真正拉开差距的从来不是“你会不会写SQL”,而是“线上跑崩了你怎么查”。我面过不少人,也被人面过,Hive这块最容易被追问的就是数据倾斜。题目本身可能就一句话:“有个任务跑了六个小时没出结果,你怎么处理?”但这一句话背后,考的是你对执行计划、分区分桶、Join策略、参数调优这一整套东西的理解。这篇就借着一道典型的Hive面试题,把数据倾斜从现象到根因到解决方案完整拆一遍,顺便把面试里那些容易被忽略的细节补上。
先说清楚这道题大概长什么样。常见的版本是:两张表做Join,一张大表一张小表,或者两张都是大表但某几个Key的数据量特别大,任务卡在Reduce阶段不动,日志里能看到某个Reducer处理的数据量远超其他Reducer。面试官想听的不是“加个参数就好了”,而是你如何定位、如何判断倾斜类型、如何选择方案。下面我按实际排查的顺序来讲,这套思路在真实工作里同样适用。
1.1 为什么数据倾斜是Hive面试的必考题
Hive本质上把SQL翻译成MapReduce任务,Map阶段做过滤和投影,Reduce阶段做聚合和Join。数据倾斜的本质是Key的分布不均匀,导致某个或某几个Reducer要处理的数据量是其他Reducer的几十倍甚至上百倍。整个任务的完成时间取决于最慢的那个Reducer,所以一个Reducer卡住,整个任务就卡住。
面试官爱考这个,是因为它同时考察几个能力:你懂不懂MapReduce的执行模型、会不会看执行计划、知不知道常见的调优参数、有没有实际处理过线上问题。光背参数没用,因为面试官会追问“为什么这个参数能解决”“什么情况下这个参数反而没用”。我见过不少人能说出set hive.groupby.skewindata=true,但问他这个参数具体做了什么就答不上来了。
这道题在面试里的变体很多,比如“大表Join小表为什么会倾斜”“Group By为什么会倾斜”“Count Distinct为什么会倾斜”,每一种的根因和方案都不一样。接下来我按类型拆开讲,每种都给出定位方法和处理方案。
2. 定位倾斜:先搞清楚是哪种倾斜
遇到任务卡住,第一步不是急着加参数,而是先定位。我一般的排查顺序是:看日志、看执行计划、看Key分布。这三步能覆盖大部分场景。
2.1 从日志和Counter里找线索
任务跑得慢,先看YARN的日志或者Hive的日志。重点看几个地方:Map完成了多少、Reduce完成了多少、有没有Reduce卡在某个百分比不动。如果Map早就100%了,Reduce卡在99%或者某个Reducer一直不结束,基本可以确定是倾斜。
再看Counter。Hive任务结束后会打印一堆Counter,重点看HDFS Bytes Read和Reduce input records。如果某个Reducer的输入记录数远大于其他Reducer,那就是倾斜。不过任务卡住的时候可能拿不到最终Counter,这时候可以看中间日志,或者用EXPLAIN看执行计划。
-- 查看执行计划,重点看Stage的依赖关系和Operator EXPLAIN SELECT a.id, a.name, b.order_amount FROM user_info a JOIN order_detail b ON a.id = b.user_id;执行计划里如果看到Reduce Output Operator的Key分布信息,能大致判断倾斜发生在哪个Stage。不过EXPLAIN只能看逻辑计划,要看更细的得用EXPLAIN EXTENDED。
2.2 用SQL直接验证Key分布
最直接的办法是跑一条SQL看Key的分布。比如怀疑user_id倾斜,就跑:
SELECT user_id, COUNT(*) AS cnt FROM order_detail GROUP BY user_id ORDER BY cnt DESC LIMIT 20;如果第一名和第二十名差了几个数量级,那就是典型的Key分布不均。我遇到过最夸张的一次,某个user_id占了全表40%的数据,这种不管怎么调参数都救不了,必须从业务层面处理。
还有一种情况是Join的Key本身分布均匀,但某个Key对应的数据量特别大。比如user_id分布均匀,但某个用户的订单记录有几百万条,Join的时候这个Key就会倾斜。这种要看Join后的数据量,不能只看单表。
2.3 判断倾斜发生在Map端还是Reduce端
Map端倾斜比较少见,一般是小文件太多或者输入格式有问题。Reduce端倾斜才是大头,主要发生在Join和Group By。判断方法很简单:如果Map阶段就卡住,看是不是小文件太多;如果Map很快Reduce很慢,那就是Reduce端倾斜。
Reduce端倾斜又分几种:Join倾斜、Group By倾斜、Count Distinct倾斜。每种的处理方式不同,下面分别讲。
3. 大表Join小表的倾斜处理
这是最常见的一种。小表可能就几MB,大表几百GB,Join的时候如果小表的Key分布不均,或者大表的Key分布不均,都会导致倾斜。
3.1 Map Join:把小表广播出去
最直接的方案是用Map Join。原理是把小表加载到每个Map任务的内存里,在Map阶段就完成Join,不需要Reduce阶段。这样从根本上避免了Reduce端倾斜。
-- 方式一:通过Hint指定 SELECT /*+ MAPJOIN(b) */ a.id, a.name, b.order_amount FROM user_info a JOIN order_detail b ON a.id = b.user_id; -- 方式二:通过参数控制,小表阈值默认25MB SET hive.auto.convert.join = true; SET hive.mapjoin.smalltable.filesize = 25000000;Map Join的适用条件是小表足够小,能放进内存。默认阈值是25MB,如果小表超过这个大小,可以适当调大,但要注意内存压力。我一般建议不超过200MB,再大就有OOM风险。
注意:Map Join不是万能的。如果小表本身就不小,或者Join的Key在两张表里都分布不均,Map Join解决不了问题。另外,Map Join会把小表复制到每个Map任务,如果Map任务很多,网络传输和内存开销也不小。
3.2 大表Join大表:分而治之
两张都是大表,没法用Map Join,这时候要看倾斜的Key是不是少数。如果是少数几个Key导致的倾斜,可以用“加盐”的方式把热点Key打散。
具体做法是给热点Key加上随机前缀,让小表也做同样的处理,这样Join的时候热点Key就被分散到多个Reducer。处理完再去掉前缀。
-- 第一步:给大表的热点Key加随机前缀 SELECT CONCAT(CAST(FLOOR(RAND() * 10) AS STRING), '_', user_id) AS join_key, user_id, order_amount FROM order_detail WHERE user_id IN ('hot_user_1', 'hot_user_2'); -- 第二步:给小表的相同Key也加前缀,每个Key复制10份 SELECT CONCAT(CAST(n AS STRING), '_', user_id) AS join_key, user_id, user_name FROM user_info LATERAL VIEW EXPLODE(ARRAY(0,1,2,3,4,5,6,7,8,9)) t AS n WHERE user_id IN ('hot_user_1', 'hot_user_2'); -- 第三步:Join后去掉前缀 SELECT SPLIT(join_key, '_')[1] AS user_id, user_name, order_amount FROM ( -- Join逻辑 ) t;这个方案的核心思路是“把热点Key打散到多个Reducer”。加盐的倍数要根据热点Key的数据量来定,一般取热点Key数据量除以平均数据量。比如热点Key有1000万条,平均每个Key有10万条,那倍数取100左右比较合适。
实操心得:加盐方案写起来麻烦,而且只对少数热点Key有效。如果热点Key很多,或者Key分布是长尾分布,加盐的效果有限。我一般先用SQL统计一下Top 100的Key占比,如果Top 10的Key占了50%以上,加盐效果很好;如果Top 1000的Key才占50%,那加盐就没意义了。
3.3 空值Key导致的倾斜
还有一种特殊情况是Join的Key里有大量空值。比如user_id为NULL的记录,Join的时候所有NULL会被分到同一个Reducer,导致倾斜。
处理方式很简单,把空值过滤掉或者给空值加上随机前缀。
-- 方式一:过滤空值(如果业务允许) SELECT a.id, a.name, b.order_amount FROM user_info a JOIN order_detail b ON a.id = b.user_id WHERE b.user_id IS NOT NULL; -- 方式二:给空值加随机前缀 SELECT a.id, a.name, b.order_amount FROM user_info a JOIN ( SELECT CASE WHEN user_id IS NULL THEN CONCAT('null_', CAST(FLOOR(RAND() * 100) AS STRING)) ELSE user_id END AS user_id, order_amount FROM order_detail ) b ON a.id = b.user_id;空值倾斜很容易被忽略,因为很多人觉得NULL不参与Join。实际上Hive的Join会把NULL当成一个正常的Key来处理,所有NULL分到同一个Reducer。我踩过这个坑,一个任务卡了四个小时,最后发现是几百万条NULL记录导致的。
4. Group By和Count Distinct的倾斜处理
Group By倾斜和Join倾斜的根因一样,都是Key分布不均。但处理方式略有不同。
4.1 Group By倾斜的两阶段聚合
Hive提供了一个参数hive.groupby.skewindata,开启后会做两阶段聚合。第一阶段在Map端做局部聚合,给每个Key加上随机前缀,这样同一个Key的数据会被分到多个Reducer做预聚合;第二阶段再去掉前缀做最终聚合。
SET hive.groupby.skewindata = true; SELECT user_id, COUNT(*) AS order_cnt FROM order_detail GROUP BY user_id;这个参数的本质是“用两次MapReduce换一次倾斜”。第一次MR做局部聚合,第二次MR做全局聚合。代价是多了一次Shuffle,但解决了倾斜问题。
注意:这个参数不是万能的。如果倾斜的Key是少数几个,效果很好;如果所有Key都倾斜(比如每个Key的数据量都很大),那两阶段聚合也救不了。另外,这个参数只对Group By有效,对Join无效。
4.2 Count Distinct的倾斜处理
Count Distinct是Group By的一种特殊情况,但更容易倾斜。因为Count Distinct需要把所有数据放到一个Reducer去重,数据量大的时候很容易卡住。
处理方式是把Count Distinct拆成两步:先去重,再Count。
-- 原始写法,容易倾斜 SELECT COUNT(DISTINCT user_id) AS uv FROM order_detail; -- 优化写法:先去重再Count SELECT COUNT(*) AS uv FROM ( SELECT user_id FROM order_detail GROUP BY user_id ) t;如果去重后的数据量还是很大,可以进一步用hive.groupby.skewindata或者加盐。还有一种方式是近似计算,用approx_distinct或者HyperLogLog,但精度会有损失,看业务能不能接受。
-- 近似计算,精度约1% SELECT APPROX_DISTINCT(user_id) AS uv FROM order_detail;实操心得:Count Distinct的优化优先级是:先去重再Count > 两阶段聚合 > 近似计算。近似计算虽然快,但面试的时候如果面试官问“怎么保证精度”,你得能说出HyperLogLog的原理和误差范围。我一般建议在数据量特别大且业务能接受误差的场景下才用近似计算。
5. 参数调优与执行计划解读
前面讲的都是方案层面的处理,这一节讲参数层面的调优。参数调优是“锦上添花”,不是“雪中送炭”。如果倾斜的根因没解决,光调参数没用。
5.1 常用参数速查表
| 参数 | 默认值 | 作用 | 适用场景 |
|---|---|---|---|
hive.auto.convert.join | true | 自动转Map Join | 大表Join小表 |
hive.mapjoin.smalltable.filesize | 25MB | 小表阈值 | 调整Map Join适用范围 |
hive.groupby.skewindata | false | 两阶段聚合 | Group By倾斜 |
hive.optimize.skewjoin | false | 倾斜Join优化 | Join倾斜 |
hive.skewjoin.key | 100000 | 倾斜Key阈值 | 配合skewjoin使用 |
hive.exec.reducers.bytes.per.reducer | 256MB | 每个Reducer处理的数据量 | 调整Reducer数量 |
hive.exec.reducers.max | 1009 | Reducer数量上限 | 控制并发 |
hive.optimize.skewjoin这个参数值得单独说一下。开启后,Hive会把超过阈值的Key单独拿出来处理,其他Key正常Join。这个方案对少数热点Key有效,但会多一次MR。
SET hive.optimize.skewjoin = true; SET hive.skewjoin.key = 100000;5.2 怎么读执行计划
执行计划是排查倾斜的核心工具。EXPLAIN输出的是逻辑计划,EXPLAIN EXTENDED输出的是更详细的计划,EXPLAIN DEPENDENCY输出的是依赖关系。
-- 查看详细执行计划 EXPLAIN EXTENDED SELECT a.id, COUNT(*) AS cnt FROM user_info a JOIN order_detail b ON a.id = b.user_id GROUP BY a.id;执行计划里重点看几个东西:Stage的划分、每个Stage的Operator、Reduce Output Operator的Key。如果看到某个Stage的Reduce Output Operator后面跟着Group By Operator或者Join Operator,那就是倾斜可能发生的地方。
注意:执行计划是逻辑计划,不是物理计划。实际执行的时候Hive会根据参数做优化,所以执行计划和实际执行可能不一样。要结合日志和Counter一起看。
6. 面试里的高频追问与应对
面试官问完基础问题后,通常会追问几个深入的问题。这一节整理几个高频追问和应对思路。
6.1 “加盐方案有什么缺点”
加盐方案的缺点很明显:一是实现复杂,需要改SQL;二是只对少数热点Key有效;三是加盐倍数不好确定,太小没效果,太大增加Shuffle开销;四是加盐后数据分布可能还是不均,因为随机函数不一定均匀。
我一般会补充一句:“加盐是最后的手段,优先考虑Map Join或者业务层面处理。如果必须加盐,建议先用SQL统计Key分布,确定加盐倍数。”
6.2 “Map Join一定比Reduce Join快吗”
不一定。Map Join的优势是避免了Shuffle,但如果小表不够小,或者Map任务很多,Map Join的内存开销和网络传输开销可能比Reduce Join还大。另外,Map Join不支持Full Outer Join,也不支持非等值Join。
我遇到过一个小表200MB,Map任务有5000个,Map Join直接把集群内存打满了。后来改成Reduce Join加两阶段聚合,反而更快。
6.3 “怎么判断倾斜是数据问题还是SQL问题”
数据问题和SQL问题的判断方法:先看Key分布,如果Key分布本身就不均,那是数据问题;如果Key分布均匀但任务还是慢,那可能是SQL写法有问题,比如笛卡尔积、错误的Join条件、重复扫描等。
还有一种情况是数据问题+SQL问题,比如Key分布不均,同时SQL里又用了Count Distinct,两个问题叠加。这种要分开处理,先解决SQL问题,再解决数据问题。
7. 真实场景下的排查记录
这一节分享一个我实际处理过的案例。某天一个核心报表任务跑了八个小时没出结果,平时只要一个小时。任务是一个大表Join大表,然后Group By。
第一步看日志,Reduce卡在99%,某个Reducer处理了80%的数据。第二步看Key分布,发现shop_id这个Key的分布极度不均,Top 1的shop_id占了全表60%的数据。第三步看SQL,发现Join之后又做了Count Distinct。
处理方案分三步:第一步把Count Distinct改成先去重再Count;第二步对热点shop_id加盐,加盐倍数取50;第三步调整Reducer数量,从默认的300调到800。改完之后任务跑了40分钟,比平时还快。
这个案例的关键点是:倾斜往往是多个问题叠加的,不要指望一个参数解决所有问题。先定位根因,再逐个解决。
实操心得:排查倾斜的时候,我一般会先跑一条SQL看Key分布,这一步能省很多时间。如果Key分布均匀,那问题不在数据,在SQL或者参数;如果Key分布不均,那就针对热点Key处理。另外,改完SQL后一定要先跑一个小样本验证,别直接上全量,不然可能白等几个小时。
8. 写在最后的一些经验
Hive数据倾斜这块,面试里考的是思路,工作里考的是效率。我面人的时候,如果候选人能说出“先定位再处理”这个思路,基本就及格了;如果能说出具体的定位方法和不同场景的处理方案,那就是优秀。
有几个坑我踩过,这里提一下。一是不要一上来就加参数,参数解决不了根因;二是不要忽略空值Key,空值Key导致的倾斜很常见;三是不要迷信Map Join,小表不够小的时候Map Join反而更慢;四是加盐方案要慎用,能用其他方案就用其他方案。
最后说一个容易被忽略的点:数据倾斜有时候不是技术问题,是业务问题。比如某个店铺的订单量突然暴涨,导致Key分布不均。这种时候技术方案只能缓解,根本解决要靠业务层面处理,比如把大店铺单独拆出来处理。面试的时候如果能提到这一点,面试官会觉得你有全局视角。
另外,Hive的版本不同,参数和行为可能有差异。我上面讲的基于Hive 2.x和3.x,更早的版本可能不支持某些参数。实际使用的时候要先确认版本,别照搬。