1日批从 35 分钟到 4 小时:倾斜往往不是单一根因
大促结束后,离线数仓日批任务 dws_order_summary 在 Hive on Tez 上从平时约 35 分钟飙到 4 小时仍未结束。
YARN 上该 Application 只剩 1 个 Reduce Task 在跑,CPU 长期打满,其余 Reduce 早已结束;
Shuffle 阶段某几个 Tez Vertex 的输入数据量是均值的 200 倍以上。
值班同学先怀疑集群资源不足,扩容队列后问题依旧;再查 SQL,发现是对 dim_user 与
fact_order 做宽表 Join 后接 GROUP BY user_id,而 user_id
里存在大量「未登录下单」默认值 0 与若干头部 KOL 账号——典型的复合倾斜:
Join 键倾斜叠加聚合倾斜,再叠加分区裁剪失效带来的扫描放大。
现场时间线:T+0 告警日批 SLA 超时;T+15min 查 YARN 发现 Reduce 1/1;T+40min 误判资源不足调整队列;
T+1h 打开 Tez UI 见 Shuffle Vertex 某 Task 输入异常;T+1.5h 跑 key 分布 SQL 确认
user_id=0 与头部 ID;T+2h 发现 dt 谓词缺失;T+3h 完成 SQL 改写与重跑验证。
若团队有本篇 SOP,前两步可压缩到约 20 分钟,避免无效扩容。
文中业务名、耗时、数据量为便于讲解而合成,但机制与排障步骤与生产一致。与 T01 Hadoop 架构篇衔接: YARN 队列不足会放大慢任务,但本篇案例中扩容无效,说明瓶颈在 Shuffle 与单 Task 数据分布,而非 RM 调度。 后续 T03 分层篇会讲 DWS 表职责,本篇聚焦「单表 DDL + 单条 SQL 如何写对、跑稳」。
风格比例大致为:框架训练 50%(建表/SQL/倾斜治理的不变量与决策顺序)、 问题现场 35%(复合场景现象与踩坑)、机制推导 15%(分桶 Join 与加盐为何有效)。 结论处会区分「机制共识」与「作者经验总结」,避免把参数阈值说成绝对真理。
值班现场还有一类「看起来像倾斜、其实是扫描」的假阳性:Tez UI 上最慢 Vertex 名字带 Reduce, 但 Counter 显示该 Vertex 输入本身来自未裁剪的全表 Map 输出。若先加盐而不补分区谓词, 你会看到加盐后 Reduce 数变多、总耗时却几乎不动——因为瓶颈仍在读盘。本篇反复强调 「先定阶段、再选手段」,就是为了切断这种无效循环。
读完本篇,你应能独立回答四个问题:这张表该内部还是外部、分区键能否支撑九成以上 SQL、 当前 plan 为何没走 MapJoin、以及倾斜 key 是默认值还是头部业务账号。若四个问题答不全, 说明还停在「会写 SQL」而未进入「会读计划与分布」的排障层。把这四个问题写进晋升答辩或值班交接清单,比再背十个参数名更有用。本篇余下章节按这四个问题的求解顺序组织:建表与分区回答前两问,Explain 与倾斜治理回答后两问。
2Hive 3.x + Tez:从 SQL 到 DAG 的执行链路
Hive 负责 SQL 语义、元数据与优化;Tez 负责把逻辑计划落成 YARN 上的 DAG。
生产环境普遍设置 hive.execution.engine=tez,相对 MapReduce 减少 Job 间落盘、支持 Vertex 级并行。
本文参数与 UI 路径均按 Tez 描述;若集群仍用 MR,Join 与倾斜参数名大多相同,但观测界面与 Stage 切分不同,
需单独留存 MR 版 Explain 样例供值班对照。理解这条链路的意义在于:优化手段必须落在正确的层——
改 SQL 解决的是计划与分布问题,改队列解决的是资源争用问题,二者不可互相替代。
机制依据:Apache Hive 官方站点 · Apache Tez 文档 · Hive 3.x Language Manual(Joins / EXPLAIN)
后续章节按值班真实决策顺序展开:先把表建对(第 3 章),再谈分区分桶(第 4 章),然后用 Explain 验证计划(第 5 章), 最后才进入倾斜的参数与 SQL 改写(第 6~8 章)。跳过 DDL 与分区直接加盐,是最常见的无效加班路径。
HiveServer2、Metastore、Tez AM 三者职责不要混:HS2 负责会话与编译提交;Metastore 负责表/分区/统计; Tez AM 负责向 YARN 要 Container 并调度 Vertex。值班时「连不上」多半是 HS2 或 Kerberos; 「表空但 HDFS 有数」多半是 LOCATION/分区未同步;「Application 一直 ACCEPTED」多半是队列资源或 AM 起不来—— 这些都不属于本篇倾斜 SOP,但若阶段判断错了,会把排障带进死胡同。
2.1 观测面:Tez UI 与 YARN 分工
YARN ResourceManager 回答「Application 是否拿到资源、队列是否被占满」; Tez UI 回答「DAG 里哪个 Vertex、哪个 Task 在拖后腿」。两者缺一不可。 常见误判是只看 YARN 进度百分比:Reduce 显示 99% 可能意味着只剩一个倾斜 Task, 也可能意味着 Shuffle 仍在拉取数据。打开 Tez UI 看 Task 输入字节分布,比盯进度条更接近根因。 建议值班手册写明:SLA 告警工单模板必须粘贴 ApplicationId,以及 Tez 最慢 Vertex 名称与 Top Task 输入量。
相对 MapReduce,Tez 的价值在于:同一 Application 内多 Vertex 可流水线化,中间结果少落 HDFS; 观测上应看 DAG 而不是单一 Job 进度条。切换引擎后同一 SQL 的 Join 策略可能变化, 性能对比必须声明引擎前提,否则「升级 Tez 变慢了」这类结论往往不可复现。
3建表规范:内部/外部表、存储格式与元数据
建表是后续所有优化的地基。团队里常见两类返工:把该外部化的 ODS 建成内部表,删表误删 HDFS 数据; 或全库 TextFile 导致 Tez 读盘与 Shuffle 体积过大。下面几条不变量可覆盖多数生产决策, 也是 Code Review 时最值得卡住的检查点:生命周期、格式、分区、统计、注释五件事一次定准, 比事后在 SQL 里打补丁便宜一个数量级。
3.1 内部表 vs 外部表
内部表(Managed Table):元数据与 HDFS 目录生命周期绑定,DROP TABLE 会删除数据。
适合可重建的中间层、严格受控的 DWD/DWS。默认路径在 hive.metastore.warehouse.dir 下。
外部表(External Table):DROP TABLE 仅删元数据,HDFS 文件保留。
适合 ODS 贴源层、由 DataX/Sqoop 写入的路径、多引擎共享的原始数据。
LOCATION 变更需配合 ALTER TABLE ... SET LOCATION,禁止手工挪目录却不改 metastore。
选型决策树:数据是否由外部系统写入且需跨 Hive/Spark 共享?是 → 外部表。 表是否可整库重建且不含唯一原始证据?是 → 内部表。不确定 → 默认外部表,更安全。 外部表 LOCATION 变更后务必同步 metastore,否则会出现「表空但 HDFS 有数」的幽灵数据, 值班同学按 SQL 查为空、运维按路径查有文件,双方对账半天才能发现元数据漂移。
-- Hive 3.1.x · 外部表 ODS 示例
CREATE EXTERNAL TABLE IF NOT EXISTS ods_order (
order_id BIGINT,
user_id BIGINT,
order_amount DECIMAL(18,2),
create_time TIMESTAMP
)
PARTITIONED BY (dt STRING)
STORED AS ORC
LOCATION 'hdfs://ns/warehouse/ods/ods_order/'
TBLPROPERTIES ('orc.compress'='ZSTD');
3.2 存储格式与压缩
Tez 下 ORC 与 Parquet 是默认推荐:谓词下推、列裁剪、内置索引统计,配合
ANALYZE TABLE ... COMPUTE STATISTICS 可让 CBO 选对 Join 策略。
TextFile 仅保留在调试或特殊分隔场景。
| 格式 | Tez 读取 | 压缩建议 | 适用层 |
|---|---|---|---|
| ORC | 原生优化最好 | ZSTD / SNAPPY | Hive 主力仓 ODS~ADS |
| Parquet | 良好,跨 Spark 友好 | Snappy / GZIP | 混合栈或 Spark 主导层 |
| TextFile | 无列裁剪,IO 大 | 不推荐生产 | 临时调试 |
格式能力说明参见 Hive LanguageManual ORC 与 Parquet SerDe 章节;压缩不会解决倾斜,只会降低传输量。
3.3 字段、文件大小与统计信息
- 分区字段与业务时间字段分离:业务时间进列,
dt仅作分区键,避免WHERE create_time无法裁剪分区。 - 单个 ORC 文件建议约 128MB~256MB(机制共识:过小则 NameNode 与 Task 启动开销大,过大则并行度不足)。
- 定期
ANALYZE TABLE ... COMPUTE STATISTICS FOR COLUMNS;缺失统计时优化器常把两表都判为大表而错过 MapJoin。 - 主键逻辑靠业务约定:Join 键与粒度字段(如
order_id)要在 DDL 注释写清。 - 敏感字段在 DDL 层写清注释,配合 Ranger 做列级权限;类型一次定准,避免下游反复
CAST。 - 对高基数字段(device_id、openid)谨慎做桶键,除非有稳定的大表对大表 Join;否则桶维护成本高于收益。
写入端可通过控制 Reduce 数与 hive.exec.orc.default.stripe.size 影响文件与 Stripe 粒度:
Stripe 越小谓词过滤越细,但元数据开销上升——需在「扫描效率」与「小文件」之间折中。
Metastore 3.x 可独立部署,生产建议开启路径校验与定期备份。表结构变更走评审:加列优先可空;
改类型视为 breaking change,需通知全下游。元数据不清楚时,Explain 里的表别名与路径会对不上,浪费值班时间。
命名建议 层级_业务域_表意,如 dwd_trade_order_detail。临时表统一 tmp_ 前缀并设 TTL,
禁止与生产表同名。LOCATION 路径与库名一致,避免 metastore 指 A、运维清 B。
规范本身不提升单次 SQL 性能,但能缩短事故定位时间——排障效率也是性能的一部分。
压缩选型权衡:ZSTD 压缩率高、CPU 适中,适合冷数据与宽表;SNAPPY 解压快,适合热分区与高并发读取。 机制共识:压缩不会解决倾斜,但会降低 Shuffle 网络传输量——在倾斜未治理前,压缩只能「稍微不那么慢」, 不能替代 key 分布修复。纯 Hive 批处理链路仍优先 ORC 以获取更完整的 Tez 向量化读取; 若下游 Spark 占比高,ODS 可统一 Parquet 以减少格式转换。
把数仓核心事实表建成内部表,清理脚本误 DROP 同名生产表后数据难恢复——ODS 与共享层一律外部表并隔离路径权限。
另一常见坑:从 Hive 1.x 迁来的全库 TextFile 未改格式,Tez 仍按行读,列裁剪失效,IO 可高一个数量级(作者经验总结,具体倍数视列数与压缩率而定)。
4分区与分桶:裁剪范围、文件数与 Join 加速
分区解决「扫多少数据」,分桶解决「Join 与采样如何均衡」。二者混用不当,会分别引发分区爆炸与小文件灾难。
实践中常见误把高基数字段当分区键(如 order_id),或只对一张表分桶、关联表未分桶导致无法 Bucket Join,
排障时却误以为「分桶没用」。分桶与分区可叠加:先按 dt 分区再按 user_id 分桶,
适合「按天任务内大表 Join」的稳定 pipeline;临时探数与一次性回刷不必上桶。
4.1 分区键选择与爆炸治理
分区键应满足:查询几乎必带等值或范围过滤;单分区数据量可控;分区总数可控,避免按秒级时间或高基数字段分区。 用「分区设计三问」评审新表:(1) 90% 以上 SQL 是否带该分区谓词?(2) 单分区是否会在 6 个月内超过容量上限? (3) 是否存在「只查少数分区却要扫全表」的报表?任一为否,就需要二级分区、分桶或拆表。
二级分区 (dt, region) 适合区域公司架构:华东/华南报表各扫各的分区;全国汇总必须显式列出 region
或使用动态分区写入,不能假设「扫 dt 就够」。动态分区插入若上游 key 失控,一次 job 可产出上万分区目录,
NameNode 压力与小文件同步爆发。治理:预聚合分区键、限制动态分区上限、定期合并小文件。
分区字段类型优先 STRING 的 yyyy-MM-dd,与调度传参一致;避免
INT yyyyMMdd 与字符串混用导致隐式转换、裁剪失效。
复合场景中,若 fact_order 只有 dt 分区而 Join 未带 dt,Tez 会对多分区全表扫描;
若再按高基数 user_id 做 Join,倾斜在 Shuffle 前就已注定。调度层应对无分区谓词的任务打标预警,
这是治理倾斜最便宜的闸口之一。分区列类型不一致是另一类隐蔽坑:调度传入 '20260807',
表分区却是 dt='2026-08-07',谓词永远匹配不到——要么扫全表要么空结果,两种都极难排障。
SET hive.exec.dynamic.partition = true;
SET hive.exec.dynamic.partition.mode = nonstrict;
SET hive.exec.max.dynamic.partitions = 500;
SET hive.exec.max.dynamic.partitions.pernode = 100;
4.2 分桶 Join:Bucket Map Join 与 SMB
Hive 分桶使用 hash(key) mod numBuckets。若表 A 与表 B 对同一 Join 键用相同桶数 N 分桶,
则 A 的桶 i 与 B 的桶 i 包含的 key 集合一致,Map Task 可只读 A_i 与 B_i 做本地 Hash Join,无需全量 Shuffle。
SMB(Sort Merge Bucket Join)进一步要求桶内按 Join 键排序,适合超大桶内仍放不下的场景。
分桶与 Join 优化参见 Hive LanguageManual DDL(CLUSTERED BY)及 Join 优化章节。
CREATE TABLE dim_user_bucketed (
user_id BIGINT, user_level STRING
)
CLUSTERED BY (user_id) INTO 32 BUCKETS
STORED AS ORC
TBLPROPERTIES ('transactional'='false');
SET hive.optimize.bucketmapjoin = true;
SET hive.optimize.bucketmapjoin.sortedmerge = true;
SET hive.enforce.bucketing = true;
| 反例 | 现象 | 正例 | 验收 |
|---|---|---|---|
| 按 user_id 分区 | 分区数失控 | dt 分区 + user_id 列/分桶 | SHOW PARTITIONS 可控 |
| 桶键与 Join 键不一致 | 回退 Reduce Join | CLUSTERED BY Join 键且两表对齐 | Explain 见 Bucket Map Join |
| 普通 INSERT 写分桶表 | 桶文件错乱 | enforce.bucketing 后规范写入 | 每桶文件符合约定 |
MapJoin(Broadcast)把小表读入内存构建 HashTable,在每个 Map Task 处理大表分片时本地探测,
从而消除 Join 阶段 Shuffle。Tez 上小表经 broadcast edge 分发给下游 Map Vertex。
触发门槛由 hive.mapjoin.smalltable.filesize(文档默认约 25MB,生产常调到数百 MB)与
hive.auto.convert.join 共同决定。代价是 Container 堆内存:维表过大可能 OOM。
Bucket Map Join没有整表广播,而是按桶切片并行:每个桶号一对文件做本地 Hash Join, 适合两表均较大的稳定链路。前置条件严格:桶键等于 Join 键、桶数一致或整数倍、分桶写入规范、ORC。 SMB Join在此基础上要求桶内按 Join 键有序,做 merge-join,内存占用低于 Hash Join。 写入时需保证桶内有序,否则优化器可能回退。
桶数 N 通常取 2 的幂(8/16/32/64)。N 过小则桶内仍可能倾斜;N 过大则文件数膨胀。 经验起点(作者经验):维表百万级取 32,亿级事实表同键 Join 取 64 或 128,再用抽样验证 Task 耗时比。 分桶的维护成本在于上游改桶键或改桶数需全量重写——临时探数 SQL 不必上桶。
-- 反例:高基数字段做分区
CREATE TABLE bad_fact (order_id BIGINT, user_id BIGINT, amt DECIMAL(18,2))
PARTITIONED BY (user_id BIGINT);
-- 正例:按天分区 + Join 键分桶
CREATE TABLE good_fact (order_id BIGINT, user_id BIGINT, amt DECIMAL(18,2))
PARTITIONED BY (dt STRING)
CLUSTERED BY (user_id) INTO 32 BUCKETS
STORED AS ORC;
4.3 分区与分桶联合验收
设计评审通过不等于线上生效。分区分桶上线后建议固定三道验收:
其一,用 SHOW PARTITIONS 与调度日历核对分区个数是否符合预期,动态分区是否触达上限;
其二,用 DESCRIBE FORMATTED 核对两表 Num Buckets 与桶键是否一致或成整数倍;
其三,对代表 SQL 跑 EXPLAIN,确认出现目标 Join 类型而非默默回退 Reduce Join。
三道都过,才把「DDL 已改」写成「优化已落地」。若只有文件数变多而 plan 不变,说明写入未 enforce bucketing,
需要全量重写而不是继续调 Session 参数。
小文件合并也是分区治理的一部分:Hive 3 可对 ORC 表执行数据重写类语句合并小文件(以集群版本支持为准)。 合并窗口应避开日批高峰,并在合并后复查分区级统计信息是否需要重新 ANALYZE。 否则 CBO 仍按旧行数选 Join,值班同学会误以为「合并没用」。
桶数通常取 2 的幂;大表桶数宜为小表桶数的整数倍,否则 SMB 前置条件不满足。
分桶表改桶键/桶数需全量重写,因此仅对「每日稳定执行、Join 键不变」的核心链路上桶。
决策顺序:先问查询是否总能带分区谓词 → 再问 Join 是否固定大表关联小维表或两表同键长期关联 →
最后才考虑 SQL 层加盐。复合场景若在第 1 步就补 WHERE f.dt = '${bizdate}',
扫描量可先降一个数量级,后续倾斜治理压力会小很多。
5SQL 优化:谓词下推、Join 策略与 Explain 解读
Tez 将 Hive SQL 编译为 DAG。优化目标是减少扫描量、消除不必要的 Shuffle、让 CBO 选对小表广播或大表桶 Join。 与 MR 相比,Tez 的 Vertex 可在同一 Application 内共享 Shuffle 中间结果,减少 HDFS 落盘次数; 因此对比优化效果时,应使用同一引擎前后对照,避免 Tez 与 MR 混比得出错误结论。 遇到多表 Join,先画「表大小与 Join 键」矩阵——小表一律尝试 MapJoin;两表同键且长期关联考虑分桶; 其余才走 Reduce Join 并准备 skew 治理。统计信息错误时,CBO 也会「自信地选错」。
Session 级参数建议:排障时可 SET hive.exec.dynamic.partition.mode=strict 防止误写全分区;
调大 tez.grouping.min-size / max-size 控制 Map 并行度,但 Map 过多时小文件合并压力上升——
需与 HDFS 块大小、ORC Stripe 一并考虑。以上属于作者经验总结,具体数值应结合集群规模验证。
Hive 3 默认 CBO 开启;新分区首日无统计时,建议调度里在主力 SQL 前挂 ANALYZE 子任务,
或开启从 ORC Footer 读取轻量统计的相关配置。核心事实表按天 ANALYZE 的成本通常远小于一次错误 Join 策略的 Shuffle 浪费。
5.1 谓词下推与分区裁剪
不变量:分区列裸列比较,谓词尽量下推到最内层扫描。
对分区列做 substr(dt,...)、或把 dt 写在子查询外层,裁剪常失效。
宽表避免 SELECT *,列裁剪发生在 Reader 层,与是否倾斜无关,但扫描过大可掩盖倾斜症状——
慢是因为读太多,还是因为 Shuffle 不均,要先靠 Explain 与 Counter 区分。
-- 反例:分区裁剪失效
SELECT * FROM (
SELECT order_id, user_id, order_amount, dt FROM fact_order
) x WHERE substr(x.dt, 1, 7) = '2026-08';
-- 正例:谓词下推 + 列裁剪
SELECT user_id, SUM(order_amount) AS amt
FROM fact_order
WHERE dt BETWEEN '2026-08-01' AND '2026-08-07'
GROUP BY user_id;
5.2 Join 策略:MapJoin / Bucket / Reduce / Skew
| 策略 | 触发条件(Tez) | 适用场景 | 风险 |
|---|---|---|---|
| MapJoin(Broadcast) | 小表低于 hive.mapjoin.smalltable.filesize | 维表关联事实表 | 广播过大撑爆 Container |
| Bucket Map Join / SMB | 两表分桶对齐(SMB 还需有序) | 稳定大表对大表同键 Join | DDL 维护成本 |
| Reduce Side Join | 默认兜底 | 无优化条件时 | Shuffle 大、易倾斜 |
| Skew Join(参数) | hive.optimize.skewjoin | 已知倾斜 key 分布 | 需统计或经验阈值 |
选型口诀(作者经验):维表低于阈值且 Join 键无极端倾斜 → MapJoin;两表均大、分布均匀、DDL 已对齐 → SMB; 其余 → Reduce Join + 倾斜治理。切忌强行 MapJoin hint 广播过大维表——OOM 反复失败往往比 Reduce Join 更糟。
Hive 可通过 hive.auto.convert.join 与小表体积阈值自动转为 MapJoin;
CBO(hive.cbo.enable)依赖表/列统计信息选择 Join 顺序与策略。
统计缺失时 plan 常出现 Plan lacks statistics 类提示——应先 ANALYZE 再谈调参。
细节以当前集群的 Hive 3.x 配置与 Join Optimization 文档为准。
读 Explain 时还应核对:Join 节点是否标注 Map Join / Bucket Map Join / Reduce Join;
Reducer 个数是否异常为 1;Partition Filters 是否包含业务日期。这三项构成值班最小检查集。
5.3 Explain 完整案例:dim_shop JOIN fact_order
以下案例独立演示「分区裁剪失效 + 错误 Join 策略」的读法(合成数值便于讲解)。
-- 问题版本:谓词写在 create_time,未写分区列 dt
SELECT s.region, SUM(o.order_amount) AS total
FROM fact_order o
JOIN dim_shop s ON o.shop_id = s.shop_id
WHERE o.create_time >= '2026-08-07 00:00:00'
AND o.create_time < '2026-08-08 00:00:00'
GROUP BY s.region;
Partition Filters: None— 全分区扫描,应补o.dt = '2026-08-07'。- Join 标注 Reduce Join,且
Plan lacks statistics for dim_shop— 先 ANALYZE 再尝试 MapJoin。 - Tez UI:Map 扫描体积异常大;Shuffle 某 Task 输入远高于均值(默认
shop_id=0)。
ANALYZE TABLE dim_shop COMPUTE STATISTICS;
SELECT /*+ MAPJOIN(s) */ s.region, SUM(o.order_amount) AS total
FROM fact_order o
JOIN dim_shop s ON o.shop_id = s.shop_id
WHERE o.dt = '2026-08-07' AND o.shop_id <> 0
GROUP BY s.region
UNION ALL
SELECT s.region, SUM(o.order_amount)
FROM fact_order o JOIN dim_shop s ON o.shop_id = s.shop_id
WHERE o.dt = '2026-08-07' AND o.shop_id = 0
GROUP BY s.region;
改写后应看到 Partition Filters 生效、Join 变为 Map Join、最慢 Task 耗时比下降。
Explain 不是「看一遍就完」,而是优化前后各存一份、标注差异点的可回归资产。
此案例与文首 user_id 场景同构:分区列与过滤列分离时必须显式写分区谓词;
维表虽小也需 ANALYZE;默认值门店/用户 ID 应纳入倾斜 key 探查清单。
5.4 常见 SQL 反模式
- 子查询外层才写
dt条件,内层全表扫描。 - 对事实表
SELECT DISTINCT user_id再 Join,多余去重引发大 Shuffle。 LEFT JOIN后WHERE dim.col IS NOT NULL把外连接写成内连接却保留大 Shuffle。- 滥用
ORDER BY触发全局单 Reduce 排序,与业务需要的「汇总」无关。 - MapJoin hint 广播大表,Container OOM 后任务反复失败,比 Reduce Join 更糟。
# 查看逻辑与物理计划 · Hive 3.1 + Tez
hive -e "EXPLAIN EXTENDED SELECT ..." > plan.txt
yarn application -list -appStates RUNNING
# 经 RM 跳转 Tez UI,查看 Slowest Vertex 与 Task Counter
5.5 优化前后对照纪律
任何一次倾斜治理都应留下三份资产:优化前 Explain、优化后 Explain、Tez 最慢 Vertex 的 Counter 截图。 工单关闭条件建议包含:结果对账通过、SLA 回归、Task 耗时比达标、上述三份资产已归档。 没有资产的「感觉变快了」无法沉淀为团队能力,下一次大促仍会从零排查。 若改写涉及加盐,额外保留一组小样本手工验算,证明去盐后与原语义一致。
复合场景优化前常见:Reducer: 1 of 1,Shuffle 字节 TB 级,Reduce Vertex 单 Task 跑数小时;
改写后补分区谓词 + MapJoin + 倾斜分支 Union + 两阶段聚合,Reduce 数恢复、最慢/最快 Task 耗时比约 2:1 量级、总耗时回到 SLA。
务必保留 plan、YARN Counter、Tez Vertex 截图;仅口头说「快多了」无法在下次大促复用。
6数据倾斜:识别、参数层与 SQL 改写
数据倾斜的本质是:partition key 或 shuffle key 分布严重不均,导致个别 Task 承担绝大部分数据。 Tez 表现为某 Vertex 内 Task 耗时差异极大;MR 则常见 reduce 进度长期停在 99% 附近。 倾斜参数属于「框架内兜底」,对 key 分布有隐含假设:倾斜 key 占比可识别且未超过阈值时,优化器才会拆分子任务。 一旦超级 key 占绝对多数,必须进入 SQL 改写层。参数层、SQL 层、DDL 层三者叠加,才构成完整治理面。
6.1 识别方法
- 运行时:Tez UI / YARN Timeline 看最慢 Vertex;Hive Log 中长尾分区处理;MR 看 Map 100% 后单一 Reduce 长尾。
- 事后:对比各 Task 的 RECORDS_OUT、Shuffle Bytes;最大/最小耗时比超过 10:1 应触发排查。
- 事前:对 Join/GROUP BY 键做 Top 20 分布探查;关注 NULL、空串、默认值 0/-1 与超级账号。
探查 SQL 模板应纳入 Code Review:新建事实表上线前,对主 Join 键跑一次 Top 20 分布,结果归档到表 Wiki。 作者经验总结:约七成倾斜可在上线前被这条规则拦截,余下三成来自大促数据分布突变,需靠运行时 SOP 兜底。
6.2 参数层治理
SET hive.optimize.skewjoin = true;
SET hive.skewjoin.key = 100000;
SET hive.skewjoin.mapjoin.size.threshold = 10000000;
SET hive.skewjoin.mapjoin.min.split = 33554432;
SET hive.groupby.skewindata = true;
SET hive.exec.reducers.bytes.per.reducer = 256000000;
参数是「不改 SQL 语义」的加速器:hive.skewjoin.key 表示某 key 行数超过该值才视为倾斜;
hive.skewjoin.mapjoin.size.threshold 控制倾斜侧 MapJoin 子任务大小。
阈值过小会产生过多小 Task,过大则倾斜 key 仍挤在一个 Reducer。
开启 hive.groupby.skewindata=true 时,优化器在 Map 端对 GROUP BY 键加随机前缀做局部聚合,
Reduce 端再去前缀合并——与手动加盐同源,但可控性弱于手写 SQL;对已知固定倾斜 key,Union 分支往往更可读、可测。
超级 key 占绝对多数时,参数 alone 往往不够,必须 SQL 改写。建议维护日批默认 / 大促临时 / 回滚三套 profile,
避免值班现场临时改全局配置却忘记恢复。
| 场景 | Tez(本文默认) | MapReduce |
|---|---|---|
| Join 倾斜 | hive.optimize.skewjoin | 同左,另关注 mapjoin hashtable 相关项 |
| Group 倾斜 | hive.groupby.skewindata | 同左,两阶段聚合逻辑一致 |
| 进度观测 | Tez Vertex / Task UI | Map 100% 后 Reduce 长尾 |
| 并行度 | DAG 多 Vertex 可并行 | Stage 多 Job 串行 |
6.3 SQL 改写:加盐、Union 分支、MapJoin
改写优先级:补分区谓词 → MapJoin 消除 Join Shuffle → 倾斜 key Union 分支 → 加盐两阶段聚合 → 最后再调 skew 参数。
加盐把「一个 key 一个大 Reduce」拆成多个 (key, salt) 局部聚合,再去盐汇总;在聚合函数可分解时结果不变。
形式化地说:原聚合 SUM(v) GROUP BY k 变为 SUM(SUM(v) GROUP BY k,s) GROUP BY k,
在结合律成立时结果不变。代价是多一轮 Shuffle 与实现复杂度,需用 Explain 确认净收益,并用小样本对账。
倾斜 key 列表来自事前探查的 Top N;分支不宜过多,通常个位数即可;过多 Union 会增加解析与调度开销。
-- 两阶段聚合 · 应对 user_id 倾斜
WITH salted AS (
SELECT user_id, CAST(RAND() * 100 AS INT) AS salt, order_amount
FROM fact_order WHERE dt = '2026-08-07'
),
partial AS (
SELECT user_id, salt, SUM(order_amount) AS partial_sum
FROM salted GROUP BY user_id, salt
)
SELECT user_id, SUM(partial_sum) AS total_amount
FROM partial GROUP BY user_id;
-- 倾斜 key 拆分支(可枚举时更可读)
SELECT user_id, SUM(amt) AS total FROM (
SELECT user_id, order_amount AS amt FROM fact_order
WHERE dt = '2026-08-07' AND user_id NOT IN (0, 10001, 10002)
UNION ALL
SELECT user_id, order_amount FROM fact_order
WHERE dt = '2026-08-07' AND user_id = 0
UNION ALL
SELECT user_id, order_amount FROM fact_order
WHERE dt = '2026-08-07' AND user_id IN (10001, 10002)
) t GROUP BY user_id;
6.4 复合场景根因链复盘
根因链:调度传参丢失导致未带 dt → 全分区扫描;Reduce Join 时 user_id=0 占约 40% 行,
单 Task Shuffle 巨大;GROUP BY user_id 仅 1 个 Reducer。处置:先修调度传参;ANALYZE 后 MapJoin;
对 0 与 TOP KOL 走 Union;开启 groupby.skewindata 作兜底。
验证侧:Tez 最慢 Vertex 从数小时降至十几分钟量级、Shuffle 字节显著下降(复合场景估算值,非基准测试)。
预防:调度强制分区校验;核心 Join 键 weekly 分布报表;倾斜 key 写入团队黑名单与 SQL 改写模板库。
治理效果验收建议固定四项:总耗时回到 SLA;最慢/最快 Task 耗时比小于 3:1;Shuffle 总字节较基线下降可解释; 结果表与改写前抽样对账误差为零。任一项不达标,说明只缓解了部分瓶颈或引入了新倾斜,需回到 SOP 重新定位。 为何加盐能缓解 GROUP BY 倾斜?单 key 的全部行默认进同一 Reducer;加入 salt 后同一 user_id 被散列到多个组合, 局部聚合在多个 Reducer 并行完成,最终一轮去盐时数据量已大幅缩小——这是机制推导,不是「玄学加速」。
7什么时候继续用 Hive SQL,什么时候该换思路
适合继续用 Hive on Tez
- 大批量离线 ETL / 数仓分层(ODS~ADS)日批、周批
- 已有成熟 Metastore、Ranger 权限与调度血缘
- SQL 团队熟练、可接受分钟~小时级 SLA
- 存储以 ORC/Parquet 大文件为主,分区设计健康
不适合硬扛在 Hive
- 亚秒~秒级交互查询、强随机点查
- 极端倾斜且业务要求毫秒迭代试错的探索分析(更适合 Spark AQE / 专用引擎)
- 小文件海量、元数据爆炸且无治理窗口时,应先治存储再谈 SQL
- 需要复杂迭代算法 / 图计算时,不要硬写成多层 Hive SQL
「继续 / 迁移」不是情绪选择:若瓶颈是 DDL 与分区谓词,留在 Hive 修最便宜; 若引擎侧已有 Spark 仓且团队主力在 Dataset API,可把倾斜重的链路迁走,但元数据与权限迁移成本必须单独评估。 Spark 有 AQE 与部分内置 salting 策略,但 Hive 仓仍大量存在——本篇 SOP 与引擎无关的 SQL 层手段(补分区、拆 key、两阶段聚合)可迁移。
也不要把「适合」误解为「永远不该换」:当组织已经统一湖仓表格式、查询入口全部切到 Spark SQL/Trino, 仍单独维护一套 Hive on Tez 日批,会带来双引擎计划差异与值班知识分裂。此时迁移的驱动因素是组织与治理, 而不一定是单条 SQL 跑不过。反过来,若 Metastore + Ranger + 调度血缘已跑多年,为了追「新技术」整仓搬迁, 往往低估权限与口径对齐成本——这与 T01 架构评审里「下线 HDFS 不等于下线治理」是同一类判断。
7.1 选型时的组织因素
技术适配之外,还要看组织能不能养得起双引擎:两套调优手册、两套值班路径、两套对账口径。 若只有少数几条链路在 Hive 上出现慢性倾斜,而团队 Spark 能力成熟,局部迁移可能更划算; 若绝大多数离线任务仍在 Hive,且 Metastore 治理完善,则应优先把本篇 SOP 与分区规范做实, 而不是用迁移掩盖 DDL 与 SQL 基本功问题。选型会议建议强制出示:近三个月 SLA 超时工单分类统计, 以及其中「分区裁剪失效 / 倾斜 key / 资源争用」各自占比——没有分类统计的迁移提案,证据不足,不宜进入资源评审。
8倾斜治理 SOP:从现象到预防的值班闭环
下表可直接贴进值班 Wiki。严格按阶段顺序执行,勿在未完成「倾斜识别」前直接改 SQL—— 未确认 skew key 的改写可能把问题从 Join 转移到聚合,或引入错误结果。 每一阶段产出物应粘贴到工单评论,便于次日复盘与知识库沉淀。大促后首个日批建议提前冻结 SQL 并跑 key 分布探查。 文档中保留 ApplicationId 样例截图,能让新人在第一次值班时少走半小时弯路。
| 阶段 | 动作 | 工具 / 命令 | 判据 / 产出 | 责任人 |
|---|---|---|---|---|
| 1. 现象确认 | 确认卡在 Map / Shuffle / Reduce;记录 ApplicationId | YARN RM、Tez AM UI、yarn logs -applicationId |
最慢 Vertex、耗时占比 >50% 的 Task 截图 | 【占位】 |
| 2. 倾斜识别 | 对比 Task 输入输出;探查 Join/GROUP BY 键 Top N | Tez Counter、SELECT key, COUNT(*) ... LIMIT 20 |
确认 skew key(默认值 0、NULL、头部 ID) | 【占位】 |
| 3. 根因定位 | 检查分区裁剪、Join 类型、统计是否过期 | EXPLAIN EXTENDED、DESCRIBE FORMATTED |
列出:裁剪失效 / 错误 Join / 缺 ANALYZE | 【占位】 |
| 4. 分场景改写 | Join:MapJoin/skewjoin/拆 key;Group:两阶段/加盐;扫描:补 dt | SQL 改写、SET hive.optimize.skewjoin |
优化前后 Explain 与耗时对比表 | 【占位】 |
| 5. 验证上线 | 同数据量重跑;对比 Shuffle 与 Task 耗时比 | Tez UI Counter、调度历史耗时 | 耗时回归 SLA;Task 耗时比 < 3:1 | 【占位】 |
| 6. 预防机制 | 定期 ANALYZE;DDL Review;Join 键分布探查进 CI;黑名单与模板库 | 元数据巡检、Code Review Checklist | 倾斜 key 入库;大促 frozen SQL 清单 | 【占位】 |
| 7. 复盘归档 | plan 对比、Counter、根因一句话、改写 diff | 工单、Git SQL 仓库、Wiki | 同类 SQL 可检索;告警可推荐历史案例 | 【占位】 |
8.1 验收标准与使用说明
总耗时回到 SLA;最慢/最快 Task 耗时比小于 3:1;Shuffle 字节较基线下降可解释; 结果与改写前抽样对账误差为零。任一项不达标,回到 SOP 第 3 阶段重新定位。 使用说明:严格按阶段顺序执行;对大促后首个日批,建议提前冻结 SQL 版本并跑一遍 key 分布探查, 将「事后救火」前移到「事前巡检」。责任人列留空供团队填写;建议与调度告警联动: 日批耗时超过基线 2 倍自动附带 Tez ApplicationId,减少手工翻日志时间。
8.2 值班话术与协作边界
平台组与业务数仓组常见扯皮点是「到底是资源不够还是 SQL 有问题」。建议用客观证据切断争论: 若队列有空闲 Container 而单 Reduce 打满,优先 SQL/分布;若 Application 长期 ACCEPTED,优先队列与配额; 若 Map 扫描体积异常而 Join 键分布正常,优先分区裁剪。把证据贴进工单比口头定性更重要。 改写上线后的 24~48 小时内,关注下游依赖任务是否因结果粒度变化或空分区而产生空跑—— Union 分支改写偶发忘记某个倾斜 key,会对账时才暴露。
8.3 与调度、权限的交叉面
倾斜治理经常踩到调度与权限边界:调度系统传参丢失会导致分区裁剪失效;权限变更导致维表读不全, 会让 MapJoin 侧数据残缺从而「结果变快但变错」。因此 SOP 第 5 阶段验证必须包含对账,不能只看耗时。 若使用 Ranger 列级权限,Explain 与实际可读列不一致时,应先排除权限过滤再判断 Join 策略。 平台侧可将「无分区谓词」「缺 ANALYZE」「倾斜 key 命中黑名单」做成调度前置检查,把第 6 章的事前识别前移到发版门禁。
大促临时 profile 的启用与回滚要有明确窗口:开启 skewjoin 与调高 mapjoin 阈值可以救急, 但长期抬高阈值会掩盖维表膨胀问题,导致 Container 内存水位整体上移。建议在复盘归档阶段强制写明 「临时参数是否已回滚」,并把回滚动作做成工单检查项,而不是依赖个人记忆。
调度模板强制分区谓词静态检查;核心事实表按天 ANALYZE;维护倾斜参数 profile; 日批超时自动附带 Tez ApplicationId。与 T04 衔接:本 SOP 可作为「Hive 倾斜」子章节的上游文档。 Live Demo 建议:选脱敏 SQL,现场跑 Explain 与 key 分布探查,再演示 Union 分支改写前后 Tez UI 对比。
9决策矩阵与相邻知识地图
收尾给出一张可打印的决策矩阵:左侧是值班现场最常看到的信号,中间是优先动作,右侧是「继续 / 迁移」权衡。 矩阵不替代第 8 章 SOP 的阶段顺序,而是帮助在根因已清晰时快速选路径,避免会议上反复争论「要不要换 Spark」。 真正该争论的是:证据是否充分、改写是否可对账、预防是否进了调度与 Review——这三件事比引擎品牌更重要。
9.1 决策矩阵
| 信号 | 优先动作 | 继续用 Hive | 考虑迁移 / 换引擎 | 选型备注 |
|---|---|---|---|---|
| Partition Filters = None | 补分区谓词 / 修调度传参 | 继续 | 不必因扫描失败迁引擎 | 最便宜优化 |
| 维表小、走 Reduce Join | ANALYZE + MapJoin | 继续 | — | 注意 Container 内存 |
| 两表同键长期大 Join | 对齐分桶 / SMB | 继续 | 维护成本过高时可评估 Spark | 改桶需全量重写 |
| 可枚举超级 key | Union 拆分支 | 继续 | — | 可读性优于盲目加盐 |
| GROUP BY 单 Reducer 长尾 | 加盐 / groupby.skewindata | 继续 | 若需交互式重跑可迁 Spark AQE | 对账验证语义 |
| 小文件 + 元数据爆炸 | 合并文件 / 限动态分区 | 先治存储再谈 SQL | 对象存储 / 湖仓格式评估 | 见 T01 小文件讨论 |
9.2 要点回顾
- 建表:ODS 外部表 + ORC/ZSTD;分区列裸比较;统计信息是 CBO 前提;元数据注释与 Owner 写清。
- 分区分桶:分区管扫描,分桶管 Join 均衡;桶对齐才可 SMB / Bucket Map Join;超级 key 分桶无效。
- SQL:谓词下推、MapJoin 优先、Explain 看 Vertex 与 Reducer 数量;Tez DAG 比 MR 更宜细粒度定位慢点。
- 倾斜:先探查 key,再参数兜底 + 加盐/拆分支;复合场景常是裁剪失效与 Join/聚合倾斜叠加。
- 沉淀:SOP 七步闭环,优化前后必须留 plan 与 Counter 证据;只会抢资源而不看 Tez Vertex 等于反复试错。
本篇围绕 Hive 3.1 + Tez,用复合场景把「跑不完的大批」拆成可操作的工程步骤。 若团队分享时只能带走一张表,请带走第 8 章 SOP;若只能带走一句话,请记住: 先定慢在哪个阶段,再选对应改写手段,避免一上来全量加盐。
9.3 相邻知识地图
上游 T01:HDFS 小文件与 YARN 队列影响扫描与 Container 分配,但「队列有空仍慢」必须下钻 Shuffle key。 下游 T03:分层规范决定表职责与命名,避免跳层引用放大排障面;本篇的建表与分区规范应落到分层命名与 LOCATION 约定中。 下游 T04:并列 YARN 抢占、Hive 阶段卡顿与更多倾斜案例;本篇 SOP 是其 Hive 倾斜子章节的上游。 若团队已上实时链路,可对照 Kafka(T19)规划批流一体下的 key 分布监控:日批倾斜 key 往往在实时写入阶段已有征兆, 例如某促销标签导致用户维度集中度上升,提前同步到维表刷新策略可减少大促后首个日批踩雷概率。
讨论环节常见疑问简答:加盐是否影响正确性——两阶段去盐后应与原语义一致,需对小 key 做回归对账;
MapJoin 表多大算大——看 Container 内存与 hive.mapjoin.smalltable.filesize,不能凭感觉;
Tez 与 Spark SQL 谁更适合倾斜治理——Spark 有 AQE,但 SQL 层手段可迁移,选型看团队栈与治理沉没成本。
Hive 3.x + Tez 在离线数仓仍高度可用,前提是把「慢」拆成可验证的计划问题与分布问题。
扩容 YARN 不能替代补 dt、ANALYZE、拆 skew key。未知项(具体迁移成本、团队栈偏好)决定决策矩阵里「继续 / 迁移」的最终落点,没有放之四海而皆准的答案。
核心思想:倾斜不是单一参数就能彻底消除的问题,而是 DDL 设计、SQL 写法、执行计划与运行时观测四层叠加的结果。