前言说实话,只要在大数据岗位干过一年以上,应该都遇到过那种离谱的 Hive 查询:昨天 3 分钟能跑完的任务,今天突然 40 分钟还卡在 map 阶段;同一个 SQL 在测试环境飞快,到了生产连日志都刷不动;有时候 Tez 跑得稀碎,一切换回 MR 又灵了…,瞬间迷茫了。
1. “为什么我的 Hive 明明没动代码,但跑着跑着越来越慢了?”我见过最典型的三种“性能突然下降”的现场,几乎每家公司都出现过:
① map 阶段启动慢、特别慢某次排查生产任务,我看到 map 阶段卡了 7 分钟才开始处理数据。看日志一看,全是:
代码语言:txt复制INFO mapreduce.JobSubmitter: number of splits: 1342013420 个 splits?好家伙,这是在用 Hive 群发邮件吗?
结果一看 HDFS 文件,整整 1.8 亿个小文件。NameNode 的 CPU 直接干到 280%,GC 开始上天,整个任务调度都慢得要死。
性能下降不是 SQL 老化,而是小文件越来越多。
② reduce 阶段的“无限等待”另一次,有个指标表从 3 分钟跑成 25 分钟。我盯着执行计划看了半天,发现是一个字段加了 GROUP BY,结果 reduce 全堆在一个节点上,数据倾斜严重得离谱:
最大一个 reduce 拿了 82% 的数据量其他 reduce 全是休闲玩法,一秒就跑完
这就是典型的低频值 / 空值倾斜。③ join 阶段 shuffle 暴涨还有就是某些任务因为业务增长,数据量几个月翻几倍,但 SQL 完全没改,突然就开始狂 shuffle,网络带宽被榨干,任务能从晚上跑到天亮。
所以 Hive 性能下降并不是“程序变慢了”,而是“你看不见的东西改变了”。往往问题是:文件变多了、数据分布变了、表结构变旧了、底层引擎换了。
2. Hive on MR、Tez、Spark 到底哪个更快我以前也天真以为 Tez 比 MR 快,Spark 比 Tez 快,这是自然规律。直到我遇到一次线上事故,让我改观。
有一次我们把 Hive on Tez 切到 Spark结果某些任务跑得更慢了,而且慢得很均衡,不像 Tez 会卡在 DAG 的某个结点。后来才知道:
Spark 架构中所有 shuffle 必须落地磁盘Tez 可以利用 yarn container 长驻,跨任务复用资源,减少启动开销MR 虽然慢,但对超大数据、超宽表、超大 shuffle 更稳,不容易 OOM不同引擎不是谁更牛,而是“谁更适合你的数据特征”。
真正的对比结论(实战感受):场景
MR
Tez
Spark
小并发、大表批处理
稳
快
可能更快
多 SQL 组合查询
很慢
快
更快
表结构复杂、宽表
稳得可怕
中等
OOM 风险大
shuffle 超大(>TB)
最稳
可能失败
很可能失败
资源紧张的集群
能跑就行
吃资源
最吃资源
我最后得到一条朴素规律:
Hive on Spark 不是性能升级,而是资源升级。
你资源够多自然快,资源不够也只能干着急。
3. 为什么大表 JOIN 大表会这么危险?我第一次被“大表 join 大表”坑,是一个 400GB 的用户行为表和一个 700GB 的日志表。开发同学写了个 SQL:
代码语言:sql复制SELECT a.uid, a.action, b.ip
FROM big_a a
JOIN big_b b
ON a.uid = b.uid;看上去很正常对吧?但这会导致:
双方都要 shuffle 全量数据数据压缩后仍然几十 GB 的文件横飞reduce 数量不够时直接把某个 reduce 塞爆结果:
shuffle 写盘写了 700Greduce 最慢的一个跑了 1 小时 40 分钟任务直接被杀掉后来改成:
对大表预聚合用广播小表的方式解决或者先写成 bucket join一般的原则是:
如果两边都大,就不要直接 join,要么缩一边,要么分桶,要么提前按 key 聚合到更小再 join。
4.数据倾斜数据倾斜不常常体现在 code,而是体现在业务数据的不均匀分布。
WHERE 造成倾斜的经典例子业务为了标记某些特殊用户,加了下面的代码:
代码语言:sql复制WHERE user_type = '0'问题是 user_type = '0' 的数据量是另外几个类型的 30 倍。结果 map 阶段看起来正常,但 reduce(比如 join 或 group by)就一下子被拉爆。我之前遇到一个任务,map 处理 6 分钟,reduce 处理了 53 分钟,其中一个 reduce 占了全部数据的 88%。
GROUP BY倾斜尤其是分母为零、空值、NULL 等。解决方式一般是:
加盐拆分两次 group过滤掉异常空值用 map-side 聚合减少 reduce 压力但真正的解决方式是:
去问业务数仓的人,为什么表里 80% 的用户都是 user_type='0'?
技术优化有时候不如改业务字段来得有效。
5. 小表广播(map join)我特别喜欢 /*+ MAPJOIN(b) */ 这个提示,一键让小表不参与 shuffle,直接广播出去给所有 map 任务。
但很多人不知道,Hive 本身有 auto join 优化,它有阈值,比如:
代码语言:txt复制hive.auto.convert.join=true
hive.mapjoin.smalltable.filesize=25000000默认 25MB 以下的小表会自动被广播。但问题来了:
如果表分区设计垃圾,小表也会变成大表我们有个看着很小的 dimension 表(500MB),但因为每个分区几十 MB,自动 join 无法命中,导致它一直 shuffle。后来我把它变成一张 unpartitioned 表,直接变成 一个 500MB 的 ORC,反而速度快多了。
所以广播小表有效,但前提是:
小表基础设计合理或者你明确手动标注 map join真实有效的 SQL:
代码语言:sql复制SELECT /*+ MAPJOIN(dim) */ a.uid, dim.city
FROM dwd_user_log a
LEFT JOIN dim_user_city dim
ON a.uid = dim.uid;6. ORC、Parquet和textfile有一次我遇到一个非常魔幻的表:30TB 的数据,所有分区都是 textfile,每个表格式都是这种:
代码语言:txt复制字段1\t字段2\t字段3\t...Hive 执行 plan 里根本没有谓词下推、列裁剪,map 完全要扫全表。我们把它改成 ORC 后:
相同数据量执行时间从 82 分钟 → 9 分钟IO 从数百 GB → 十几 GB为什么 ORC 这么重要?
支持列存支持压缩支持索引支持 predicate pushdown支持 vectorization(向量化)通俗点说:
7. 分区设计一次线上排查,一个任务突然从 5 分钟跑成 40 分钟。查日志发现它在 full scan,partition pushdown 根本没生效。后面查原因是开发写成了:
代码语言:sql复制WHERE dt = cast('2025-02-11' as string)dt 是 string,你为什么要 cast?Hive 看到 cast,直接放弃分区裁剪。简单一句改成:
代码语言:sql复制WHERE dt = '2025-02-11'任务瞬间回到 4 分钟。有些开发写 WHERE like '%2025%',更离谱。分区条件必须:
明确精确匹配避免函数包裹(TO_DATE、CAST、SUBSTR 都不行)8. 列裁剪和谓词下推打个比方,如果你要在仓库里找一把扳手,合理的方式不是“把整个仓库搬到办公室”,而是“只把工具箱拿来”。列裁剪就是只读必要列。谓词下推就是尽可能提前过滤。
一个任务从:
代码语言:sql复制SELECT * FROM big_table WHERE event_type = 'login'改成:
代码语言:sql复制SELECT uid, event_time FROM big_table WHERE event_type = 'login'IO 从 280GB → 12GB,map 阶段从 18 分钟 → 1 分钟
9. Hive 参数调优我以前也会随便写:
代码语言:txt复制set mapreduce.job.reduces=300;结果把集群资源打爆,其他任务都排队。真实经验是:
reduce 数量过多会:创建大量文件(小文件地狱)占用大量 containershuffle 时间爆炸reduce 数量过少会:单个 reduce 被塞爆出现数据倾斜OOM所以我总结了以下使用规律:
10GB 数据量 → 1~3 个 reduce100GB → 5~20 个 reduce1TB → 20~50 个 reduce但这取决于你的 key 分布。map 数量不用太操心,HDFS splits 决定的,你要调的是“不要有太多小文件”。另外我常用的几个参数:
代码语言:txt复制set hive.exec.reducers.bytes.per.reducer=512000000; -- 每个 reduce 处理 512MB
set hive.optimize.skewjoin=true; -- 自动倾斜处理
set hive.groupby.skewindata=true; -- group by 倾斜优化
set hive.exec.dynamic.partition.mode=nonstrict;10. 压缩格式我曾经遇到一个 400GB 的 gzip 文件,解压用了 17 分钟,map 阶段就 17 分钟。换成 snappy 后,同样条件 3~4 分钟搞定。
格式
压缩比
解压速度
适用场景
snappy
中
快到离谱
Hive 主力格式
gzip
高
慢
离线冷数据、归档数据
lzo
中
中
历史遗留项目
11. 小文件我们公司有个任务,QPS 正常,Hive 任务也正常,但 NameNode 每天凌晨 CPU 都打满。最后定位到一个日志任务,每天生成 2000 万个小文件。NameNode 的问题是:
元数据放内存文件数太多会让 fsimage 超大editlog 也膨胀GC 频繁(我见过一秒三次)RPC 响应延迟变大Hive 任务明显变慢我见过 NameNode 因为小文件太多,重启耗时 47 分钟才恢复。
12. 真实优化案例有一次,一个实时 T+1 的指标任务,从稳定 3 分钟跑完 → 突然 30 分钟。当时对于这个问题的的排查步骤如下:
第 1 步:看执行计划结果看到 join 阶段 shuffle 量从几百 MB → 20GB。
第 2 步:查明是谁变大了发现 dim_city 表从 200MB → 6GB。开发加了几个 from_app、from_channel 字段,变胖了。
第 3 步:看是否命中广播 join没有命中,因为太大了。
第 4 步:手动用 map join 强制广播改 SQL:
代码语言:sql复制SELECT /*+ MAPJOIN(dim) */ a.uid, dim.city
FROM dwd_user_log a
LEFT JOIN dim_city dim
ON a.city_id = dim.city_id;变成 3 分钟。
第 5 步:优化表结构让 dim_city 只保留需要的字段,删掉无关字段后变回 250MB。
第 6 步:调小 reduce从默认的 20 个 reduce → 8 个,避免小文件爆炸。最终任务从 30 分钟回到 3 分钟。