目录
基于Tez引擎的 Hive SQL 性能优化
背景
最近生产上有一个Hive SQL脚本运行耗时比较久(短则一分多钟,长则十几分钟),被用户多次吐槽,大数据离线计算一分钟还算正常,但是跑十几分钟而且时间还不稳定就影响用户其他事务的安排和计划了。于是决定分析和优化一下这个 SQL ,在这里做个笔记。
Hive 执行过程分析
1.优化之前首先了解 Hive 是如何执行 SQL 的,生产环境基于 Tez 引擎,这一点在分析问题时也需要考虑。从大的方向上看 Hive 由以下执行过程组成:
-
Compile Query 编译 SQL 为逻辑计划
-
Prepare Plan 生成物理执行计划
-
Get Query Coordinator 向 YARN 申请 ApplicationMaster (AM)
-
Submit Plan 提交 DAG 到 AM
-
Start DAG DAG 初始化启动
-
Run DAG Tez 并行执行 DAG 中每个节点的task
根据这个过程再结合运行日志就可以获取到每个过程运行的耗时:
从截图日志看 6. Run DAG 占用绝大部分的时间,这点是正常的,毕竟这一步就是在真正执行计算任务。为什么要看这里的信息,有时候集群资源紧张向 YARN 申请资源时耗时过长,会让人误以为 SQL 性能问题,看总体的查询信息避免无效的精力投入。
2.接下来看 SQL 构成,这个脚本主要由一张事实表关联多张维表分几部分 union 合并数据后写入另一张表,先从每段独立的查询语句分别看下日志运行情况。
explain 获取 SQL 的执行计划,看到 union 的每段查询分别对应 MAP1 / Reducer16 和 Reducer8,再从运行日志查看这三段的任务数和耗时。
从这两组数据可以明显看出 MAP1 的任务耗时最长(439.748秒),但并发task数量仅仅8个,SQL 脚本的瓶颈显而易见,接下来就是找出 MAP1 的对应代码展开优化分析。
| 节点 | task数量 | 耗时(s) |
|---|---|---|
| MAP1 | 8 | 439.748 |
| Reducer8 | 9 | 4.597 |
| Reducer16 | 13 | 3.577 |
3.MAP1 的task数量只有8个,显然没有充分利用集群资源,那么下一步便是分析表信息:表是否分区和 where 语句有没有使用分区字段、分区数量多少、文件数量多少、数据量多大,带着这些信息尝试增加task数量,提高任务并行度充分利用集群资源。其中分区字段的使用尤为关键,如果 where 条件未命中分区字段,数据扫描量会成倍增加,后续优化措施的效果也将大打折扣。
使用 desc formatted <table> 语句查看主表信息,得到下面关键信息:主表是分区表,分区字段是 source 和 period。89个分区一共有522个文件,总行数21709392。
| # Partition Information | NULL | NULL |
| # col_name | data_type | comment |
| source | string | |
| period | string | |
| | numFiles | 522 |
| | numPartitions | 89 |
| | numRows | 21709392 |
| | rawDataSize | 1736751360 |
| | totalSize | 4589198547 |
从表信息看出主表有很多小文件,平均每个文件大小约为 4589198547 / 522 / 1024 / 1024 = 8MB,虽然CombineHiveInputFormat会自动合并小文件,但是 Tez 会重新打包数据切片动态调节task数量。利用 tez.grouping.min-size 和 tez.grouping.max-size 两个参数可以调节每个切片的数据量从而调节task数。
| 配置项 | 优化值 | 默认值 | 说明 |
|---|---|---|---|
| tez.grouping.min-size | 67108864 (64MB) | 16777216 (16MB) | 控制单个分组切片的最小字节数,Tez 会将小文件合并到这个大小 |
| tez.grouping.max-size | 134217728 (128MB) | 1073741824 (1GB) | 控制单个分组切片的最大字节数,文件超过这个字节数会被拆分 |
4.分析 MAP1 的 DURATION 和 CPU_TIME 可以看出这个节点完整运行耗时439秒,节点内8个task总计CPU计算耗时5402秒,task是并发运行而CPU_TIME是所有任务的CPU时间累加,故CPU_TIME远大于节点完整耗时。
为了降低单个task的CPU耗时进而减少总体运行时间,可以利用 Hive 的向量化计算让task内部批量处理数据,提高运算效率,从而达到缩短CPU_TIME的目的。向量化计算默认每批处理1024行,通常不用调节这个参数,主要配置参数有下面几个:
| 配置项 | 参数值 | 说明 |
|---|---|---|
| hive.vectorized.execution.enabled | true | 总开关,开启MAP端向量化批处理 注:这里不开启的话 *.execution.reduce.enabled 不会生效 |
| hive.vectorized.execution.reduce.enabled | true | 开启REDUCER端向量化批处理 |
| hive.vectorized.adaptor.usage.mode | all | 没有对应原生向量化版本的函数适配模式 |
启用 Hive 向量化有一些前提条件:数据表格式需为列式存储格式(例如 ORC/PARQUET),数据类型需在特定支持范围之内(例如 Int/BigInt/Double/String)。hive.vectorized.adaptor.usage.mode 有 none / chosen / all 三种配置,设置为 all 表示对不支持原生向量化的函数使用适配器包装,使其能在向量化框架内处理,避免因不支持的函数导致整个执行链路回退为行模式。使用 all 可以减少向量模式与行模式切换带来的额外开销。
优化点分析验证
分析再多终究是纸上谈兵,确定好方法后便开始测试验证。根据上面的分析结论需要在脚本中添加配置项:
sql
set hive.vectorized.execution.enabled=true;
set hive.vectorized.execution.reduce.enabled=true;
set hive.vectorized.adaptor.usage.mode=all;
set tez.grouping.min-size=67108864;
set tez.grouping.max-size=134217728;
1.从执行计划观察向量化启用是否生效
before:
after:
对比向量化配置前后的MAP5执行计划,可以看到多了 vectorized 标识,这意味着MAP5这个节点会启用向量化。整个执行链路从之前的"向量模式 -> 行模式 -> 向量模式"反复切换变为全向量模式,避免了模式切换带来的额外开销。至于MAP5为何没有默认开启向量模式,是因为 instr 函数不支持原生向量化。
注:对于 instr 函数不支持向量化的情况,还可以修改 SQL 语句,例如用 col01 like '%something%' 替代原有逻辑,因为 like 有原生对应的向量化版本,这也是一种优化方式。需要注意,仅当 instr 用于判断子串是否存在(> 0)时,才可等价替换为 like。
2.数据切片的大小因环境而异,没有固定的最佳值。这里不用系统默认值,选择 64MB, 128MB 的区间是经过测试的结果。数据切分太小会导致 task 数量过多,占用太多 YARN 容器资源,且每个容器未得到充分利用,造成资源浪费。此外,生产环境通常不止一个任务在运行,过多占用集群资源会挤占其他任务的资源,因此这个区间需根据自身集群情况调整合适的范围。
打印运行日志看到 MAP1 task数从 8个 增加到 94 个,说明数据切片配置已经生效。
3.验证 SQL 执行性能的提升效果
before:
after:
对比优化前 447s 和优化后 39s,性能提升约11倍,说明这个优化是有效可行的。
优化总结
文章开头提到 SQL 主要是一张大表关联多张维表,从执行计划来看维表基本都是 MAPJOIN 关联,个别出现 MERGEJOIN 的维表数据量确实太大需要 Shuffle,便先不对此深入优化。另外数据倾斜也是一个分析优化方向,这点留待后面补充。
通过开启算子向量化处理、增加task数量提高并行度来加速数据处理,效果很明显,SQL 运行耗时从 447s 缩小到 39s。在这个优化过程中也注意到 instr 函数不支持原生向量化处理,以后开发时可以考虑用 like 等语句替代。
- 启用向量化
set hive.vectorized.execution.enabled=true;
set hive.vectorized.execution.reduce.enabled=true;
set hive.vectorized.adaptor.usage.mode=all;- 切分数据增加task数量
set tez.grouping.min-size=67108864;
set tez.grouping.max-size=134217728;- 尽量使用支持原生向量的函数,例如:特殊场景下用
like代替instr
除了下面一些文章中未提及的优化方向也可以做参考:
- 列裁剪 :避免使用
*符号,只 select 需要的列,减少 IO - 分区裁剪:where 条件尽量使用分区字段
- 尽早过滤:子查询/CTE 中提前过滤数据,减少中间数据量
还有一些配置项在本次优化中没有用到,但在其他场景下也能起到一定作用,这里列出一部分抛砖引玉。
| 参数 | 说明 |
|---|---|
hive.tez.auto.reducer.parallelism |
Tez 自动调节 reducer 并行度,默认 true |
hive.tez.max.partition.factor |
控制 reducer 最大并行数(默认 2,即 2 × 分区数) |
hive.merge.tezfiles |
启用 Tez 输出文件合并,减少小文件问题 |
hive.exec.reducers.bytes.per.reducer |
每个 reducer 处理的数据量,间接控制 reducer 数 |
tez.task.resource.memory.mb |
单个 task 容器内存大小 |
hive.tez.container.size |
Tez 容器大小,影响并行度与资源利用率 |