外观
Hive 数据倾斜怎么识别与处理
难度与前置
难度:进阶
前置:会写 Hive SQL;知道 Map / Reduce 与 Join 基本概念;能看 Job UI。
读完能做什么:区分 Map 端与 Reduce 端倾斜,用 SQL 摸热点,按场景改写 Join/聚合,并用任务时长与结果对齐验收。
适用场景
- 某个 Reduce 长时间 99%,其余已结束
- 同样 SQL,某天因头部渠道/商家暴量而超时
DISTRIBUTE BY/GROUP BY后明显长尾
不适用:
- 全阶段均匀慢(小文件、全表扫、集群队列问题)
- 引擎已是 Spark SQL:思路可借鉴,参数与 UI 以 Spark 为准(见 Spark Join 倾斜)
真实场景(一条)
Hive 日报:GROUP BY channel_id 汇总消耗。Tez 上多数 Reducer 1 分钟结束,1 个 Reducer 跑 40 分钟。摸底发现 Top1 渠道占当日 35% 行数。
改法:对该渠道走单独分支(先过滤热点再 UNION ALL),并对普通渠道保留原聚合;或对 Key 加盐两阶段聚合。墙钟从 45 分钟降到 12 分钟,金额与改前一致。
怎么做(步骤清单)
1. 判断倾斜落在哪一端
| 类型 | 现象 | 常见原因 |
|---|---|---|
| Map 端倾斜 | 少数 Map 读极大分片 | 输入文件大小不均、压缩块、分区内倾斜文件 |
| Reduce / Join 倾斜 | 少数 Reducer 狂吃 Shuffle | GROUP BY / JOIN Key 热点、空值扎堆 |
| 动态分区写倾斜 | 写某分区极慢 | 单分区数据过大或小文件爆炸 |
先看 Job UI 的 Map/Reduce 进度条与字节计数,再决定改输入还是改 Key。
2. 摸底热点 Key
sql
SELECT channel_id, cnt,
cnt * 1.0 / SUM(cnt) OVER () AS ratio
FROM (
SELECT channel_id, COUNT(1) AS cnt
FROM dwd_ad_spend_detail_di
WHERE dt = '${biz_date}'
GROUP BY channel_id
) t
ORDER BY cnt DESC
LIMIT 30;同时查空值:
sql
SELECT
SUM(CASE WHEN channel_id IS NULL OR channel_id = '' THEN 1 ELSE 0 END) AS nullish_cnt,
COUNT(1) AS total_cnt
FROM dwd_ad_spend_detail_di
WHERE dt = '${biz_date}';3. 策略选择
| 场景 | 做法 |
|---|---|
| 小表 Join 大表 | Map Join(/*+ MAPJOIN(small) */ 或自动) |
| 少量热点 Key | 热点拆分:热点单独算 + 非热点原逻辑,再 UNION ALL |
| GROUP BY 倾斜 | 两阶段聚合:先按 key, salt 局部聚合,再按 key 最终聚合 |
| 空值扎堆 | 空键打散或单独支路 |
| 输入文件不均 | 合并小文件 / 调整分片,或先 DISTRIBUTE BY 重分发 |
4. Map Join(优先尝试)
sql
SET hive.auto.convert.join=true;
SET hive.mapjoin.smalltable.filesize=67108864; -- 64MB,按集群调
SELECT /*+ MAPJOIN(d) */
f.bill_id, f.channel_id, f.cost_amount, d.channel_name
FROM dwd_ad_spend_detail_di f
LEFT JOIN dim_ad_channel_df d
ON f.channel_id = d.channel_id
AND d.dt = '${biz_date}'
WHERE f.dt = '${biz_date}';维表必须去重,否则 Map Join 一样会放大行数。
5. 两阶段聚合(GROUP BY 倾斜)
sql
-- 阶段 1:加盐局部聚合
WITH partial AS (
SELECT
channel_id,
CAST(FLOOR(RAND() * 16) AS INT) AS salt,
SUM(cost_amount) AS cost_amount,
COUNT(1) AS bill_cnt
FROM dwd_ad_spend_detail_di
WHERE dt = '${biz_date}'
GROUP BY channel_id, CAST(FLOOR(RAND() * 16) AS INT)
)
-- 阶段 2:去盐最终聚合
INSERT OVERWRITE TABLE dws_ad_channel_1d PARTITION (dt='${biz_date}')
SELECT channel_id,
SUM(cost_amount) AS cost_amount,
SUM(bill_cnt) AS bill_cnt
FROM partial
GROUP BY channel_id;6. 热点拆分模板
sql
-- 非热点
SELECT channel_id, SUM(cost_amount) AS cost_amount
FROM dwd_ad_spend_detail_di
WHERE dt = '${biz_date}'
AND channel_id NOT IN ('hot_channel_1', 'hot_channel_2')
GROUP BY channel_id
UNION ALL
-- 热点:可加盐或提高并行后单独聚合
SELECT channel_id, SUM(cost_amount) AS cost_amount
FROM dwd_ad_spend_detail_di
WHERE dt = '${biz_date}'
AND channel_id IN ('hot_channel_1', 'hot_channel_2')
GROUP BY channel_id;热点名单可落成维表,由调度每日刷新,避免 SQL 写死。
7. 参数辅助(不能替代改 SQL)
sql
SET hive.optimize.skewjoin=true;
SET hive.skewjoin.key=100000; -- 超过阈值的 Key 走倾斜优化
SET hive.exec.reducers.bytes.per.reducer=67108864;倾斜优化依赖引擎实现与统计信息;统计过期时可能误判。生产仍以主动改写为主。
常见坑(至少牢记这些)
只加 Reducer 个数
同一 Key 仍进同一 Reduce,长尾 Key 依旧。Map Join 维表有重复
速度上去了,金额翻倍;先验channel_id唯一。RAND 盐值在两阶段之间不一致用法
最终聚合忘记去掉 salt,结果按盐值拆行,下游重复计算。把「倾斜优化」参数当银弹
未收集统计 / 版本行为差异,导致计划不变或更慢。忽略动态分区与小文件
写环节倾斜被当成计算倾斜,越加 Reduce 小文件越多。
如何验收(可量化)
- [ ] Job UI:最慢 Reduce 与中位数耗时比下降到约定阈值(如 < 3)
- [ ] 同日
SUM(cost_amount)/ 行数与改前基准一致 - [ ] 热点 Key 名单或加盐策略有注释/文档
- [ ] Map Join 场景下维表唯一性检查通过
- [ ] 连续多日头部渠道暴量时任务仍在 SLA 内
sql
-- 结果对齐
SELECT SUM(cost_amount) AS cost_sum, COUNT(1) AS channel_cnt
FROM dws_ad_channel_1d
WHERE dt = '${biz_date}';bash
# Tez / MR UI 记录:max_reduce_time, median_reduce_time相关面试题
占位:与题库小程序对齐后补链接(建议关联:Hive 倾斜表现、MapJoin、两阶段聚合、skewjoin 参数)。
改写说明:结构与判断标准参考 Wood 原创数仓/面试文,已改写成可执行清单;未包含未公开公司数据与内部表名。