ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Hive on Tez 源码编译与整合调优实战:从环境准备到性能调优

Hive on Tez 源码编译与整合调优实战:从环境准备到性能调优

1. 为什么我要折腾 Tez 编译这件事

Hive 用久了总会碰到那个让人头皮发麻的场景:一个稍微复杂点的多表 JOIN 加 GROUP BY,提交上去之后进度条卡在 Map 阶段半天不动,最后跑完一看,中间落了好几轮盘。默认的 MapReduce 引擎在这种场景下就是老老实实地"写盘—读盘—再写盘",每一步 shuffle 都要落地 HDFS,IO 开销大得离谱。我最早接触 Tez 是在一个离线数仓项目里,当时集群规模不算大,但每天的 ETL 任务窗口越来越紧张,调度队列里堆着的 SQL 越排越长,逼得不得不从执行引擎层面想办法。

Tez 这个东西,说白了就是把多个 MR 任务串成一个有向无环图(DAG),让中间结果尽量在内存和本地磁盘之间流转,减少不必要的 HDFS 往返。它跟 Hive 整合之后,一个原本要拆成好几轮 MR 的查询,可以压缩成一两个 DAG 阶段跑完,实测下来复杂查询的耗时经常能砍掉一半甚至更多。但问题在于,很多发行版自带的 Tez 包要么版本对不上,要么编译时缺了某些依赖,跑起来各种NoClassDefFoundError、NoSuchMethodError,所以"源码编译"这一步绕不开。

这篇内容适合谁看?如果你正在维护一套 Hive 数仓,发现查询性能上不去,想从执行引擎层面做优化;或者你拿到手的 Tez 二进制包跟自己的 Hadoop、Hive 版本总是打架,想自己从源码编一个干净可控的版本,那这篇就是写给你的。我会把整个编译、整合、调优、排错的链路拆开讲清楚,包括我踩过的那些坑。需要说明的是,下面涉及的版本号、路径、参数都是基于常见生产实践给出的参考方案,你实际用的时候要按自己集群的情况替换。

2. 整体方案设计与选型考量

2.1 为什么是 Tez 而不是别的引擎

Hive 的执行引擎选择其实就那么几个:MapReduce、Tez、Spark。MapReduce 是默认的,胜在稳定、兼容性好,但性能是硬伤,尤其是多阶段查询。Spark 性能也好,但引入 Spark 意味着要额外维护一套 Spark 集群或者至少一套 Spark on YARN 的资源池,运维成本上去了,而且 Hive on Spark 在某些版本上兼容性坑也不少。Tez 的定位很讨巧,它是专门为 Hive、Pig 这类框架设计的 DAG 执行引擎,跟 Hadoop 生态贴合得最紧,部署上只需要往 HDFS 放一个 tar 包,配置几个参数就能切换,不需要额外起常驻服务。

从架构上看,Tez 的核心是DAG 调度和容器复用。传统 MR 里,每个 Map 或 Reduce 任务都要单独申请容器、启动 JVM、跑完销毁,任务多了光启动开销就吓人。Tez 的 Container Reuse 机制让一个容器可以连续跑多个 task,省掉了反复申请和启动的成本。另外 Tez 的 Shuffle Handler 做了优化,中间数据可以只写本地磁盘而不落 HDFS,这对 IO 密集型的聚合查询提升非常明显。

我选 Tez 的另一个理由是它的可插拔性。Hive 通过hive.execution.engine=tez这一个开关就能切换,出问题了改回mr就行,回滚成本极低。这种"低风险试错"的特性,对于生产环境来说太重要了。

2.2 为什么坚持源码编译而不是直接用二进制包

很多人会问,Apache 官网不是提供 Tez 的二进制发行包吗,为什么还要自己编?我自己的经历是,二进制包在简单场景下确实能用,但一旦你的 Hadoop 版本比较特殊(比如某些厂商的发行版),或者你需要用到某些特定版本的依赖库,二进制包就会出问题。最典型的是protobuf 版本冲突:Tez 编译时依赖的 protobuf 版本如果跟 Hadoop 自带的不一致,运行时会抛NoSuchMethodError,这种错误排查起来极其痛苦,因为堆栈信息往往指向一个看起来毫不相关的地方。

源码编译的好处是,你可以在编译阶段就锁定所有依赖的版本,用-Dhadoop.version、-Dprotobuf.version这些参数把版本对齐,编出来的包跟你的集群环境是精确匹配的。另外,源码编译还能让你按需裁剪,比如去掉一些用不到的模块,减小分发包体积。代价就是编译过程本身有点折腾,依赖下载、环境变量、Maven 配置,每一步都可能卡住,这也是为什么我要把编译细节单独拎出来讲。

2.3 版本匹配的核心逻辑

版本匹配是整件事的地基,地基没打好后面全是坑。核心原则是:Tez 的主版本要跟 Hadoop 的主版本对齐,Hive 的版本要跟 Tez 支持的版本范围对齐。比如 Hadoop 3.x 系列,一般配 Tez 0.9.x 或 0.10.x;Hadoop 2.7 系列,配 Tez 0.8.x 或 0.9.x 比较稳。Hive 这边,Hive 2.x 和 3.x 对 Tez 的支持都比较好,但 Hive 3.x 对 Tez 的版本要求会更高一些。

下面这张表是我整理的一个常见组合参考,实际选型时以官方兼容性矩阵为准,这里只是给个方向:

组件参考版本说明
Hadoop3.2.x / 3.3.x主流稳定版,YARN 和 HDFS 都比较成熟
Hive3.1.x对 Tez 支持完善,社区资料多
Tez0.10.x适配 Hadoop 3.x,功能较全
JDK8Tez 编译对 JDK 8 兼容最好,JDK 11 有额外坑
Maven3.6+版本太低会导致部分插件下载失败
Protobuf2.5.0跟 Hadoop 3.x 自带版本对齐的关键

提示:JDK 版本这件事千万别大意。Tez 的编译脚本里有些地方对 JDK 9 以上的模块化机制不友好,用 JDK 8 能省掉一大堆--add-opens的麻烦。如果你的集群跑的是 JDK 11,编译时也要用 JDK 8 编,编出来的包在 JDK 11 上跑一般没问题,但反过来就容易出幺蛾子。

3. 编译环境准备与依赖处理

3.1 基础环境搭建

编译 Tez 之前,先把基础环境理清楚。我习惯用一台独立的编译机,不跟集群节点混在一起,避免污染集群环境。操作系统用常见的 Linux 发行版就行,关键是几个环境变量要配对。

首先是 JDK。装好 JDK 8 之后,JAVA_HOME要指向 JDK 根目录,而不是 JRE。验证一下:

echo $JAVA_HOME java -version javac -version

java -version和javac -version输出的版本要一致,如果javac找不到,说明装的是 JRE 不是 JDK,得重装。

然后是 Maven。Maven 的settings.xml建议配一个国内镜像源,不然下载依赖能等到天荒地老。在~/.m2/settings.xml里加上镜像配置:

<mirrors> <mirror> <id>central-mirror</id> <mirrorOf>central</mirrorOf> <url>https://maven.aliyun.com/repository/central</url> </mirror> </mirrors>

Protobuf 是编译 Tez 绕不开的依赖。Tez 的源码里有.proto文件,编译时会调用protoc生成 Java 代码。这里有个关键点:编译用的 protoc 版本要跟 Hadoop 运行时依赖的 protobuf-java 版本匹配。Hadoop 3.x 一般用 protobuf 2.5.0,所以你要装 protoc 2.5.0。装高版本会生成不兼容的代码,运行时直接报错。

# 下载 protobuf 2.5.0 源码编译安装 tar -zxvf protobuf-2.5.0.tar.gz cd protobuf-2.5.0 ./configure --prefix=/usr/local/protobuf make && make install export PATH=/usr/local/protobuf/bin:$PATH protoc --version

其他依赖还包括autoconf、automake、libtool、cmake、openssl-devel这些,用包管理器一次性装上就行。别小看这些,缺一个编译到一半报错,回头补装再重来,时间全浪费在等待上。

3.2 源码获取与目录结构

Tez 的源码从官方仓库拿,选一个跟你目标版本对应的 tag。下载下来解压,目录结构大致是这样:

tez/ ├── tez-api/ # 对外 API ├── tez-common/ # 公共工具类 ├── tez-dag/ # DAG 核心实现 ├── tez-mapreduce/ # 跟 MR 兼容的层 ├── tez-runtime-internals/ ├── tez-runtime-library/ ├── tez-tests/ # 测试用例 ├── tez-ui/ # Web UI └── pom.xml

编译前先看一眼根目录的pom.xml,里面定义了默认的hadoop.version、protobuf.version等属性。你可以直接改这个文件,也可以在编译命令里用-D参数覆盖。我倾向于用命令行参数覆盖,这样源码保持干净,方便以后切换版本重新编。

3.3 编译参数的关键取舍

编译命令的核心是这几个参数:

mvn clean package \ -DskipTests \ -Dhadoop.version=3.3.4 \ -Dprotobuf.version=2.5.0 \ -Dmaven.javadoc.skip=true \ -Dcheckstyle.skip=true \ -Drat.skip=true \ -Dfindbugs.skip=true

逐个解释一下为什么这么配。-DskipTests是必须的,Tez 的测试用例跑起来非常慢,而且很多用例依赖完整的集群环境,在编译机上根本跑不通,跳过能省大量时间。-Dhadoop.version和-Dprotobuf.version是版本对齐的关键,一定要跟你集群的实际版本一致。后面几个skip是跳过 javadoc 生成、代码风格检查、许可证检查、静态分析,这些对最终产物没影响,但会拖慢编译速度,甚至因为一些无关紧要的格式问题导致编译失败。

注意:-DskipTests只是跳过测试执行,测试代码还是会编译。如果测试代码里有依赖缺失导致编译失败,可以再加-Dmaven.test.skip=true,这个连测试代码都不编译,更彻底。

编译过程第一次会比较慢,因为要下载大量依赖。如果中途因为网络问题失败,重新执行命令即可,Maven 会复用已经下载好的依赖。编完之后,产物在tez-dist/target/目录下,主要关注两个文件:tez-0.10.x.tar.gz是完整分发包,tez-0.10.x-minimal.tar.gz是精简包。生产环境一般用完整包,省得缺东西。

4. 整合 Hive 的实操过程

4.1 分发包上传与目录规划

编好的 tar 包要上传到 HDFS,因为 Tez 的任务在 YARN 上跑的时候,各个节点需要能访问到这个包。我习惯在 HDFS 上建一个专门的目录:

hdfs dfs -mkdir -p /apps/tez hdfs dfs -put tez-0.10.2.tar.gz /apps/tez/ hdfs dfs -chmod -R 755 /apps/tez

目录权限要给足,因为 YARN 的各个容器都要能读。有些集群开了 Kerberos,那还得给对应的 principal 授权,这个按集群的安全策略来配。

本地也建议解压一份,方便配置环境变量:

tar -zxvf tez-0.10.2.tar.gz -C /opt/ mv /opt/tez-0.10.2 /opt/tez

4.2 Hive 端配置详解

Hive 整合 Tez 的核心就是改hive-site.xml,加上几个关键配置。下面这几个是必须的:

<property> <name>hive.execution.engine</name> <value>tez</value> </property> <property> <name>tez.lib.uris</name> <value>${fs.defaultFS}/apps/tez/tez-0.10.2.tar.gz</value> </property> <property> <name>tez.use.cluster.hadoop-libs</name> <value>false</value> </property> <property> <name>hive.tez.container.size</name> <value>4096</value> </property> <property> <name>hive.tez.java.opts</name> <value>-Xmx3276m -XX:+UseG1GC</value> </property>

tez.lib.uris指向 HDFS 上的 tar 包路径,这个路径要写全,带上fs.defaultFS的前缀。tez.use.cluster.hadoop-libs设成 false 表示用 Tez 包自带的 Hadoop 依赖,而不是用集群节点上的,这样能避免版本不一致的问题。hive.tez.container.size和hive.tez.java.opts是容器资源相关的,堆内存一般设成容器大小的 80% 左右,留点空间给堆外内存和 JVM 自身开销。

除了hive-site.xml,还要确保tez-site.xml在 classpath 里。可以从 Tez 包里把conf/tez-site.xml拷到 Hive 的配置目录,或者直接在hive-site.xml里用tez.前缀的配置项覆盖。我倾向于单独维护一个tez-site.xml,配置更清晰。

4.3 环境变量与 classpath 处理

Hive 启动的时候要能找到 Tez 的 jar 包。有两种方式:一种是把 Tez 的 jar 包拷到 Hive 的lib目录,另一种是通过HADOOP_CLASSPATH或者 Hive 的 auxlib 机制引入。我推荐用 auxlib,干净且不影响 Hive 本身的 lib 目录。

# 在 hive-site.xml 里配置 auxlib 路径 <property> <name>hive.aux.jars.path</name> <value>file:///opt/tez/tez-api.jar,file:///opt/tez/lib/tez-runtime-library.jar</value> </property>

不过更省事的做法是直接把 Tez 解压目录下的 jar 和 lib 下的 jar 都加到HADOOP_CLASSPATH:

export TEZ_HOME=/opt/tez export HADOOP_CLASSPATH=$TEZ_HOME/*:$TEZ_HOME/lib/*:$HADOOP_CLASSPATH

这个环境变量要在 HiveServer2 和 Hive CLI 的启动脚本里都生效。如果是通过服务方式启动的 HiveServer2,记得改对应的启动脚本或者 systemd 配置。

4.4 验证整合是否成功

配置改完之后,重启 HiveServer2,然后开一个 Hive CLI 或者 beeline 连上去,执行:

set hive.execution.engine;

如果返回tez,说明引擎切换生效了。再跑一个简单的查询:

SELECT count(*) FROM some_table;

跑的时候观察 YARN 的 ResourceManager 页面,如果看到应用类型是TEZ而不是MAPREDUCE,那就说明整合成功了。另外 Tez 有自己的 Web UI,默认端口 8080 左右(具体看配置),能打开看到 DAG 的可视化图,那就更稳了。

提示:第一次跑 Tez 任务时,YARN 需要把 HDFS 上的 tar 包分发到各个节点并解压,这个过程叫 localization,会额外花一些时间。第二次跑同一个包就会走缓存,快很多。所以别拿第一次的运行时间来判断性能提升,多跑几次取稳定值。

5. 性能调优与参数精讲

5.1 容器与并行度调优

Tez 的性能很大程度上取决于容器配置和并行度。容器太小,任务频繁 OOM;容器太大,资源利用率低,还可能导致队列排队。并行度这块,Tez 会根据输入数据量自动计算,但自动算出来的值不一定最优,需要手动干预。

<property> <name>tez.am.resource.memory.mb</name> <value>2048</value> </property> <property> <name>tez.task.resource.memory.mb</name> <value>4096</value> </property> <property> <name>tez.am.container.reuse.enabled</name> <value>true</value> </property> <property> <name>tez.am.container.reuse.rack-fallback.enabled</name> <value>true</value> </property>

tez.am.resource.memory.mb是 ApplicationMaster 的内存,一般 2G 够用,DAG 特别复杂可以加到 4G。tez.task.resource.memory.mb是每个 task 容器的内存,这个要结合你的数据量和查询复杂度来调。容器复用一定要开,这是 Tez 性能优势的重要来源。

并行度相关的参数:

<property> <name>tez.grouping.min-size</name> <value>16777216</value> </property> <property> <name>tez.grouping.max-size</name> <value>1073741824</value> </property>

这两个参数控制输入分片的大小范围,单位是字节。min-size设 16MB,max-size设 1GB,意思是 Tez 会把输入数据切成 16MB 到 1GB 之间的分片。分片太小,task 数量多,调度开销大;分片太大,单个 task 处理时间长,并行度不够。这个值要根据你的平均数据量和集群规模来调,数据量大的集群可以适当调大。

5.2 Shuffle 与内存管理

Shuffle 是 Tez 性能的关键环节。Tez 的 Shuffle Handler 支持把中间数据写到本地磁盘,减少 HDFS 压力,但本地磁盘的 IO 和空间也要关注。

<property> <name>tez.runtime.io.sort.mb</name> <value>512</value> </property> <property> <name>tez.runtime.sorter.class</name> <value>PIPELINED</value> </property> <property> <name>tez.runtime.sort.threads</name> <value>2</value> </property>

tez.runtime.io.sort.mb是排序缓冲区大小,默认 100MB 偏小,调到 512MB 能明显减少溢写次数。tez.runtime.sorter.class设成PIPELINED表示用流水线排序,比默认的LEGACY模式效率高。tez.runtime.sort.threads是排序线程数,一般设成 2 到 4,太多反而会因为上下文切换降低效率。

内存管理还有个容易忽略的点是JVM 堆外内存。Tez 的 Shuffle 会用到直接内存,如果堆外内存不够,会报OutOfMemoryError: Direct buffer memory。可以在hive.tez.java.opts里加上-XX:MaxDirectMemorySize:

-Xmx3276m -XX:MaxDirectMemorySize=1024m -XX:+UseG1GC

5.3 针对不同查询类型的调参策略

不同类型的查询,调参侧重点不一样。我大致分三类来说。

大表 JOIN 大表:这种查询的瓶颈通常在 Shuffle 阶段,数据要按 JOIN key 重新分布。重点是加大 Shuffle 相关缓冲区,提高并行度。可以把tez.grouping.max-size调小一点,让分片更细,并行度更高。同时关注数据倾斜,如果某个 key 的数据量特别大,会导致单个 task 跑得特别慢,这时候要考虑用hive.optimize.skewjoin或者手动拆分。

大表 GROUP BY:聚合操作对内存敏感,因为要维护聚合状态。tez.runtime.io.sort.mb要调大,容器内存也要给足。如果聚合结果集不大,可以开启hive.map.aggr做 map 端预聚合,减少 shuffle 数据量。

多阶段复杂查询:这种查询会被 Tez 拆成多个 DAG 阶段,重点是容器复用和阶段间的数据传递。确保tez.am.container.reuse.enabled开启,并且tez.am.container.reuse.max-per-container设一个合理的值(比如 10 到 20),让一个容器能连续跑多个 task。

下面这张表总结了常见参数和调整方向:

参数默认值调整建议适用场景
tez.task.resource.memory.mb10242048~8192内存密集型查询
tez.runtime.io.sort.mb100256~1024Shuffle 数据量大
tez.grouping.max-size1GB256MB~1GB控制并行度
tez.am.container.reuse.enabledtrue保持 true所有场景
tez.runtime.sort.threads12~4排序密集

6. 常见问题与排查实录

6.1 编译阶段的高频报错

编译 Tez 最容易卡在依赖下载和 protobuf 上。我整理了几个典型问题和处理方式。

问题一:protoc 版本不匹配导致生成代码编译失败。报错信息通常是com.google.protobuf.GeneratedMessageV3找不到,或者生成的 Java 文件里有语法错误。根因是 protoc 版本跟 protobuf-java 依赖版本不一致。解决方法是确认protoc --version输出 2.5.0,并且pom.xml里的protobuf.version也是 2.5.0。

问题二:Maven 依赖下载超时。这个纯粹是网络问题,配好镜像源,多试几次。如果某个依赖死活下不下来,可以手动下载 jar 包放到本地仓库对应目录,Maven 会识别。

问题三:OutOfMemoryError编译时内存不足。Tez 编译比较吃内存,Maven 默认堆可能不够。设置MAVEN_OPTS:

export MAVEN_OPTS="-Xmx2g -XX:MaxPermSize=512m"

JDK 8 之后MaxPermSize已经废弃,改成-XX:MaxMetaspaceSize=512m。

6.2 运行阶段的典型故障

故障一:NoClassDefFoundError: org/apache/tez/...。这是 classpath 没配好,Tez 的 jar 没被 Hive 加载到。检查HADOOP_CLASSPATH是否包含 Tez 的 jar,或者 auxlib 配置是否正确。用hive --service version之类的命令确认 classpath。

故障二:NoSuchMethodError。这个几乎都是版本冲突,最常见的是 protobuf 或者 Hadoop 的某个类版本不一致。排查方法是看报错涉及的类来自哪个 jar,用jar tf或者mvn dependency:tree分析依赖树,找出冲突的版本,然后在编译时用-D参数强制对齐。

故障三:任务卡在ACCEPTED状态不动。这是 YARN 资源不足,队列满了或者集群没有可用资源。检查 ResourceManager 的队列使用情况,调整队列容量或者错峰调度。

故障四:Container killed on request. Exit code is 143。容器被 YARN 杀掉了,通常是内存超限。YARN 的yarn.nodemanager.pmem-check-enabled和vmem-check-enabled会监控容器内存,超过限制就杀。要么调大容器内存,要么关掉虚拟内存检查(不推荐,治标不治本)。

6.3 性能不升反降的排查思路

有时候配了 Tez 之后,某些查询反而比 MR 慢。这种情况别慌,先分析原因。

第一种可能是数据量太小。Tez 的 DAG 调度和容器启动有固定开销,数据量小的时候,这个开销占比高,反而不如 MR 直接跑。这种查询就让它走 MR 好了,Hive 支持按查询指定引擎。

第二种可能是参数没调好。比如容器内存给太小,任务频繁 GC;或者并行度设置不合理,task 数量太少跑不满资源。回头检查第 5 节的参数。

第三种可能是数据倾斜。Tez 对数据倾斜的容忍度不一定比 MR 好,某个 key 数据量过大时,单个 task 会成为瓶颈。用hive.optimize.skewjoin或者手动加盐处理。

下面这张速查表可以帮你快速定位问题:

现象可能原因排查方向
编译报 protobuf 相关错误protoc 版本不匹配确认 protoc 和 pom 版本一致
运行时 NoClassDefFoundErrorclasspath 缺 jar检查 HADOOP_CLASSPATH
运行时 NoSuchMethodError依赖版本冲突分析依赖树,对齐版本
任务卡 ACCEPTEDYARN 资源不足检查队列和集群资源
容器被杀 exit 143内存超限调大容器内存或检查内存泄漏
性能不如 MR数据量小或参数不当分析查询特征,调整参数

提示:排查 Tez 问题时,YARN 的容器日志是金矿。每个容器的 stderr 和 stdout 里都有详细的错误堆栈,比 Hive 客户端看到的报错信息全得多。学会用yarn logs -applicationId <app_id>拉日志,能省很多时间。

7. 我踩过的坑和几条实在经验

编译整合 Tez 这件事,我前后在不同环境里做过好几轮,有些坑是重复踩的,这里挑几个最有代表性的说说。

第一个坑是 JDK 版本。有一次集群节点是 JDK 11,我图省事直接用 JDK 11 编译,结果编译到一半各种模块访问错误,加了一堆--add-opens才勉强编过,但编出来的包跑起来又出问题。后来老老实实用 JDK 8 编译,一次过。所以编译环境用 JDK 8,运行环境可以 JDK 8 或 11,这个组合最稳。

第二个坑是 protobuf 的隐式依赖。Tez 编译时会从 Hadoop 的依赖里间接引入 protobuf,如果你只设置了-Dprotobuf.version但没装对应版本的 protoc,编译会用系统里默认的 protoc,版本对不上就出问题。一定要显式确认protoc --version。

第三个坑是 HDFS 上的 tar 包权限。有一次配好了所有参数,任务提交上去一直报找不到 tar 包,查了半天发现是 HDFS 目录权限不对,YARN 的容器用户读不了。把目录和文件权限改成 755 就好了。Kerberos 环境下还要注意 principal 的授权。

第四个坑是容器复用的副作用。容器复用虽然能提升性能,但如果某个 task 有内存泄漏,复用的容器会累积问题,最后 OOM。所以开启复用的同时,要监控容器的内存使用趋势,发现异常及时排查。

最后分享一个实用技巧:用 Tez 的 Web UI 分析 DAG。Tez 的 UI 能把整个查询的 DAG 画出来,每个顶点的输入输出数据量、耗时、并行度都标得清清楚楚。哪个阶段慢、哪个顶点数据倾斜,一眼就能看出来。调优的时候对着这个图看,比盲猜参数高效得多。这个 UI 默认可能没开,需要在tez-site.xml里配置tez.history.logging.service.class和相关的 UI 参数,具体参考官方文档配一下就行。

另外,如果你的集群同时跑 MR 和 Tez 任务,建议给 Tez 单独划一个 YARN 队列,避免跟 MR 任务抢资源。队列的容量调度策略按实际负载配,这个属于 YARN 层面的调优,跟 Tez 本身关系不大,但对整体稳定性影响很大。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进