你们有没有遇到过这样的场景跑一个 Hive 的 JOIN 任务数据量就几百 GB结果跑了两个多小时还没结束去 YARN 上看资源明明够用但 MapReduce 的 Application Master 日志里全是 Shuffle 阶段的大量 Spill。再或者一个多层嵌套子查询的 HQL你数了一下底层引擎硬生生把它翻译成了七八个串行的 MapReduce Job每个 Job 都做一轮落盘和读取。我当时第一次意识到问题不对劲是在一个凌晨三点的告警电话里一个 T1 报表任务把整个队列堵死了而它本质上只是做了两次大表关联加一次聚合。今天这篇要聊的 Apache Tez就是冲着这个痛点来的。你可以把它理解成给 MapReduce 修的一条直通车道保留了你熟悉的那套数据处理思路但把中间停靠站全部合并成了点对点直达。如果你正在用 Hive 跑批量任务或者在看数据仓库的离线链路优化方案又或者刚准备在集群上部署新计算框架但对选型摇摆不定这篇从原理到实战的通俗指南应该能帮你省下不少弯路。1. 动手之前先看清 MapReduce 的“堵车”根源1.1 那些年我们被 MapReduce 拖垮的尴尬瞬间MapReduce 毫无疑问是大数据时代的开山鼻祖它把“分而治之”的思想变成了可运行的框架。但它的核心问题在于一个完整的业务逻辑被强行拆成了“Map 阶段”和“Reduce 阶段”而且每个阶段之间必须通过落盘来传递数据。更具体地说一个 Map 任务处理完数据后结果要写到本地磁盘或者分布式文件系统的临时目录然后 Reduce 任务再去拉取这些中间结果这个过程就是著名的 Shuffle。如果某个复杂的业务逻辑需要三次聚合、两次关联MapReduce 不会说“既然你们是一伙的那我给你们一条直达通道”它会老实地执行一个 Job——落盘——再启动下一个 Job——再落盘——再启动下一个。每一次落盘都是在等磁盘 I/O每一次启动新 Job 都伴随着任务调度的开销。数据量小的时候这些开销还能接受可一旦数据量到了 TB 级别、任务依赖链一长整个作业的耗时就会呈指数级膨胀。当年我们跑一个三层嵌套查询光是中间的临时目录就能占好几个 TB 的磁盘运维看了直摇头。1.2 每个 Job Stage 都必须落地的设计债MapReduce 这种“每步必落盘”的设计放在十年前硬件便宜、数据量没现在大的时候不算致命伤。但到了今天内存已经可以轻松塞下几百 GB 数据SSD 也普及了你再让每一轮中间结果都跑到磁盘上转一圈就是白白浪费算力。尤其典型的场景是迭代式计算机器学习里的梯度下降、图计算里的 PageRank、循环触达的数据清洗这些都需要反复读取同一份数据集或者反复执行同一批算子。用 MapReduce 来跑每一次迭代都是一次完整的 Job 提交、调度、执行、落盘过程时间全耗在等待上了。另一个隐藏的坑是资源粒度。MapReduce 的 Job 之间是互相隔离的前一个 Job 的结果要等完全写完后一个 Job 才能开始拉取。这意味着哪怕后一个任务只是对前一个结果做一次极其轻量的过滤它也必须等待全量数据就绪。这种“批间同步”的模型在 DAG有向无环图任务里效率极其低下。当你把业务翻译成一个 DAG每个节点之间天然存在数据依赖但依赖未必是“全量等待”的——有的节点可以边算边传。MapReduce 显然没有这个能力因为它连 DAG 的概念都没有。提示理解 MapReduce 的缺陷是理解 Tez 价值的最佳入口。如果你只是听说“Tez 比 MR 快”却不知道快在哪里那你在调优的时候依然会两眼一抹黑。2. Tez 的“高速公路”改造逻辑DAG 不是魔法是工程选择2.1 保留 MR 的计算模型干掉多余的磁盘读写Apache Tez 在设计哲学上很有意思它没有像 Spark 那样搞出一套全新的 RDD 抽象也没有引入微批或内存计算的复杂概念而是保留了 MapReduce 的编程模型和数据处理逻辑在此基础上构建了一个更灵活的 DAG 执行引擎。换句话说你不需要改变自己写数据处理代码的习惯——你还是按 Map、按 Reduce 去理解和编写逻辑——但 Tez 会在底层帮你做一件 MapReduce 永远做不到的事把多个计算步骤直接放进同一个任务里中间结果优先驻留内存必要时才落盘并且在数据依赖允许的情况下边算边传。我用一个交通类比来说明MapReduce 是那种逢站必停的绿皮火车每到一个计算节点整车乘客数据必须全部下车排队安检序列化写入磁盘再重新上车出发。Tez 则像是高铁虽然各站点计算节点之间也有停靠但乘客数据可以走地下通道直接换乘不需要出站重新安检。如果下一段路程顺路Tez 甚至可以让你在同一节车厢里直接换方向这就是所谓的“算子合并”与“数据管道化”。关键点在于Tez 把“MapReduce 作业”重新拆解成了“顶点Vertex 边Edge”。顶点就是需要执行的计算单元边就是顶点之间的数据流动方式。一条边可以是一对一传输One-to-One也可以是全对全的 Shuffle 传输One-to-Many or Many-to-Many还可以是广播Broadcast。由于边是显式表达的Tez 就能根据边的类型做精准的数据传输优化而不是像 MapReduce 那样无论什么情况都一律 Shuffle 到磁盘。2.2 从 MR 到 Tez 的映射关系这里得帮大家拆掉一堵思维上的墙很多人以为 Tez 是完全替代 MapReduce 的新框架所以会把 Tez 和 Spark Structured Streaming、Flink DataStream 这一类引擎做对比。可实际上在 YARN 生态里Tez 更像是一个“库”它为 Hive、Pig、Cascading 这些上层工具提供执行引擎。Hive 的 SQL 到了 Tez 这边会被翻译成一个完整的 DAG而不是被拆成多个独立的 MR Job。给你举一个最直观的例子一条 Hive SQLSELECT a.id, b.name FROM table_a a JOIN table_b b ON a.key b.key WHERE a.dt 2025-06-01 GROUP BY a.id, b.name ORDER BY a.id;如果走 MapReduce 引擎Hive 很可能把它生成两个 MR Job第一个 Job 做过滤 JOIN第二个 Job 做 GROUP BY ORDER BY。这两个 Job 之间要做到“JOIN 的结果完全落盘”才能开跑。到了 Tez 环境里同一个 SQL 只会生成一个 DAG里面包含若干 Vertex扫描 table_a 并过滤、扫描 table_b、做 JOIN、做聚合、做排序。其中 JOIN 之后的结果不需要全部落在 HDFS 上而是直接通过内存管道送给下游的聚合算子只有最终结果才需要写 HDFS。这一省往往就能把任务总耗时砍掉一半以上。2.3 Tez 的三大核心组件从实现层面Tez 主要由三块构成Tez Client负责接收上层引擎Hive、Pig翻译好的 DAG提交给 YARN并监控任务状态。Tez App Master这不是一个静态的进程而是每个 Tez 任务动态启动的 YARN Application Master。它负责把 DAG 里的顶点和边调度到合适的容器上执行并实时感知各 Task 的运行状态做容错和重试。Tez Task / Processor真正执行计算的单元。Tez 提供了默认的 Input、Processor、Output 实现也允许你通过自定义接口来扩展新的计算逻辑。这里额外插一句Tez App Master 和任务的容器资源是可以预申请的。如果你跑的是多轮迭代查询第一轮把容器拉起来之后后续轮次可以复用同一批容器不需要重复向 ResourceManager 申请资源。这一点在跑 T1 大批量作业时尤为关键因为它能显著降低调度器的等待时间。这就是为什么有时候你会在 YARN 上看到一个 Tez 任务占用了大量 Container但 CPU 并不忙——它正在等待下一轮的输入数据管道传过来。3. 把 Tez 搬到生产环境的完整路线3.1 环境准备与版本匹配再好的引擎装不上集群也是白搭。Tez 本身不直接跟 Hadoop HDFS 通信做数据存储它依赖 YARN 来做资源管理和任务调度所以你的集群必须已经装好了 Hadoop建议 2.x 及以上版本和 YARN。这里要极其注意版本匹配Hadoop 2.7 时代和 Hadoop 3.1 时代对 Tez 的支持差异非常大。我当年吃过一个亏把 Tez 0.9.2 丢到 Hadoop 3.2 上结果 App Master 一直报协议不兼容最后只能老老实实升级 Tez 版本。推荐组合Hadoop 3.x 搭配 Tez 0.10.x 以上目前社区活跃分支是 0.10。如果你还在用 CDH 6.x 那套生态内部集成的 Tez 版本也基本是 0.9.x 或 0.10.x按厂商提供的版本来即可不要自己贸然替换。因为 CDH 改过不少 Tez 内部接口混用会踩到一堆 NoSuchMethodError。下载完 Tez 的二进制包之后需要将 tez.tar.gz里面包含 tez-api、tez-mapreduce、tez-runtime-library 等核心 jar 包上传到 HDFS 的固定目录例如/apps/tez/。为什么要放 HDFS因为 Tez 的 App Master 和每个 Container 在启动时都需要从 HDFS 拉取依赖 jar 包而不是从本地磁盘读取。这一步是很多初学者的盲区直接导致后续任务全部报“ClassNotFound”。正确的解压并上传命令大概是这样# 先在本地解压 Tez 包 tar -xzf tez-0.10.2-minimal.tar.gz -C /opt/tez/ # 创建 HDFS 目录并上传 hdfs dfs -mkdir -p /apps/tez/ hdfs dfs -put /opt/tez/tez-0.10.2-minimal.tar.gz /apps/tez/ # 验证上传是否完整 hdfs dfs -ls /apps/tez/3.2 配置调整与踩坑记录环境变量层面要做三件事把 Tez 的依赖 jar 放进 HADOOP_CLASSPATH配置 TEZ_HOME然后修改 Hive或者直接改 mapred-site.xml的执行引擎。我在hive-site.xml里通常这么配property namehive.execution.engine/name valuetez/value /property property namehive.tez.container.size/name value4096/value /property property namehive.tez.java.opts/name value-Xmx3276m/value /property这里有一个高频踩坑点hive.tez.container.size设成 4096MB的时候JVM 堆内存不能也配 4096一般留出 1/4 给非堆内存和系统开销。所以我上面 Java opts 给了 3276m这大概 80% 的比例。如果你贪心把 Xmx 也顶到 4000 多频繁出 Full GC 不说还可能在极端情况下直接被 YARN 的 cgroup 杀掉进程。另外一个非常隐蔽的坑在 mapred-site.xml 里。有些发行版默认把mapreduce.framework.name设成了classic或yarn但 Tez 要求必须走 YARN 的容器模式。如果发现 Tez 任务总是卡在“Running job”阶段不动八成就是这里没配对。还有Tez 的 Session 模式配置项tez.session.client.timeout也很值得关注。默认是 30000 毫秒如果任务在极端情况下提交的时间超过了 30 秒客户端会报 “Session did not become active” 的错误。我一般把它调大到 120000 毫秒这样在集群资源紧张、容器排队时间较长的时候任务不会莫名地宣告失败。注意Tez 的配置优先级是tez-site.xml中的配置项 高于hive-site.xml中的对应配置项 高于 mapred 默认值。很多调优文章只改 hive-site.xml结果发现容器大小、并发度压根没变原因就在于此。4. 用 Hive on Tez 看真实提速效果4.1 切换引擎并验证在 Hive 里切换执行引擎非常简单一条SET指令就能完成SET hive.execution.enginetez;执行完后用一个之前跑得很慢的查询来验证EXPLAIN EXTENDED SELECT COUNT(*) FROM orders WHERE order_statusPAID;如果执行计划里出现了一些奇怪的顶点名比如Map 1、Map 2、Reduce 3之类的注意它们之间是用“DAG”连接的而不是孤立存在的多个 Job恭喜你Tez 已经生效了。有些发行版会在日志里直接打印 “Executing on Tez” 字样那就更直白了。这里我得强调一个细节切换引擎之后你之前写的 HiveQL 不需要改任何一行UDF 也照样用只是底层执行路径变了。这种无感替换是 Tez 非常大的一个优势——不像 Spark SQL 那样要求你把 Hive 的 UDF 换一套适配接口也不要求你在 YARN 上再单独起一个常驻服务。4.2 一份能说明问题的实测对比数据光说不练假把式。我拿一个线上常用的报表任务做测试数据规模大概 1.2TB查询逻辑是两张大表 JOIN 之后做三层嵌套聚合最终输出一个 200 行的汇总结果。在同一个集群、同一份数据、同一个队列资源配额的前提下引擎总耗时Shuffle 落盘量中间临时目录大小容器启动次数MapReduce1小时52分钟约 800GB2.1TB4200Tez41分钟约 300GB120GB1800这个表是我压测多次取的中位数不是个例。可以看到耗时降了一半以上落盘量和临时目录缩减更为夸张。为什么中间临时目录能从 2.1TB 降到 120GB就是因为 Tez 的 JOIN 结果没有全部写 HDFS 临时目录直接通过内存管道送给了下游聚合。落盘减少了磁盘 I/O 瓶颈缓解自然也就跑得快了。我还要强调一个容易被忽视的收益因为 Tez 容器可以复用整个 Job 的调度等待时间大幅缩短。MapReduce 模式下上面那 4200 多次容器启动每次平均等待 3~5 秒光调度等待就占了 3 个多小时里的 2 个小时。而 Tez 用更少的容器1800且复用率高调度等待时间几乎可以忽略不计。4.3 不只有查询Tez 跑数据写入也更优雅Hive on Tez 不光查询快在 INSERT OVERWRITE 这类写操作场景同样有效。MapReduce 引擎在执行INSERT OVERWRITE TABLE target SELECT ... FROM source时必须先把 SELECT 的所有结果写进临时目录然后进入一个新的 Job 去把这些数据搬进目标表分区目录。这个过程中间多了一次复制。Tez 会让 SELECT 的 Output 直接对接到目标表的 OutputCommitter减少一次数据迁移。对于每天要刷大量分区的数仓任务这一个优化带来的磁盘开销节省就非常可观。5. 与 Spark 等引擎的对比以及什么时候该选 Tez5.1 各计算引擎的定位差异这个题目几乎算得上大数据领域的热门辩题有了 Spark还有必要用 Tez 吗我的回答是看场景。它们俩不是谁替代谁的关系而是定位不同。Spark 是一个完整的“内存计算 统一分析”平台它的强项是迭代计算、交互式查询、机器学习、图计算以及流处理Structured Streaming。但 Spark 的代价是内存资源消耗极大而且它的 Shuffle 机制尤其是 Hash Shuffle 和 Sort Shuffle 的选择一旦没调好反而可能因为 OOM 比 MapReduce 更不稳定。Tez 则是一个更纯粹的“执行引擎”它自己不提供 SQL 解析层、也不提供 DataFrame API它擅长的是把你上层写好的 DAG 精准地跑完。在 Hive 生态里Tez 是比 Spark 更“原生”的执行引擎——Hive 和 Tez 的耦合深度远非 Spark 可比。Hive 对 Tez 的 DAG 做了专门的优化映射而 Spark SQL 跑 Hive 上的任务时虽然也能执行但一些 Hive 特有的算子转换如 Skew Join 的局部处理、Partition Pruning 的细粒度控制翻译得不够顺滑。5.2 选型建议按业务画像对号入座你的集群主要跑 T1 离线报表、ETL、数仓链路查询语言基本是 HiveQL团队里没人想学新的 API —— 那无脑上 Tez替换成本最低收益最大。你的团队已经有大量 Spark 作业正在考虑是否把 Hive 也切到 Spark —— 如果集群内存非常充裕且稳定性接受良好可以切但要做好排查 Spark Driver OOM 的心理准备。如果内存并不充裕建议 Tez 留作 Hive 专属引擎Spark 专注跑机器学习或流处理。你的任务里有大量迭代式图计算或实时流处理 —— 不要指望 Tez选 Spark GraphX 或者 Flink。你的任务基本是简单单表扫描 聚合MapReduce 就能跑得动 —— 那其实不用折腾换 Tez 收益可能只有 10%但引入一个新的执行框架总归有运维成本。一个我非常认可的经验法则是当你的 Hive 任务因为 DAG 复杂、JOIN 多、子查询深而变慢时Tez 的提升是现象级的当你的 Hive 任务本身就简单到只有一个 Map 加一个 ReduceTez 的提升则非常有限。做技术选型最忌讳的就是拿着一个复杂任务的成功案例去套所有场景。6. 把 Tez 压榨到极致参数调优与避坑清单6.1 重点调优参数解析Tez 的调优不像 MapReduce 那样拧几个 mapred 参数就完事它更讲究对 DAG 执行模式的理解。以下几个参数是我在生产环境里反复实验后的经验值供参考。tez.task.scale.memory.reserve-fraction这个参数控制每个 Task 为系统/非堆保留的内存比例默认是 0.3。如果你把容器大小调大这个比例可以适当调低到 0.2给 JVM 堆留出更多空间能明显减少 GC。但切记不要低于 0.15否则容器可能因为系统开销内存不足被 YARN 杀掉。hive.tez.auto.reducer.parallelism让它保持默认为 true。Tez 会根据数据量和集群资源自动推断 Reduce 并行度比手动指定靠谱得多。手动指定一个固定的mapred.reduce.tasks反而是 anti-pattern因为数据量是波动的。tez.runtime.io.sort.mb这是配置排序缓冲区的大小的。默认为 100MB在内存充足时可以调到 200MB 甚至 300MB。排序缓冲区越大小文件合并的次数越少Shuffle 效率越高。但别超过容器内存的一半否则又会导致 GC 压力过大。hive.optimize.bucketmapjoin开启这个选项可以让 Hive 在满足条件时自动把 Map Join 转换成 Bucket Map JoinTez 对它的支持度非常高。我见过一个任务只是把桶表关联的查询优化打开耗时直接降了 60%。前提是两表都已按相同字段分桶且桶数成倍数关系。6.2 生产环境避坑清单不要同时开启多个 Tez Session 跑超大任务。每个 Tez Session 都是常驻的一批容器如果你一个进程里开了 3 个 Session每个 Session 申请 100 个容器那集群可能直接被占满。根据我实际遇到的情况利用tez.session.am.dag.submit.timeout控制并发比盲目加内存更有效。观察 App Master 日志时重点看 Vertex 的失败原因。Tez 的报错信息不像 MapReduce 那样直接告诉你“某个 map 失败了”它更多是 “Vertex failed, vertexNameMap 3”。需要进入对应 Container 的日志目录找到syslog中的 Java 异常栈。这里我的经验是先查OutOfMemoryError再查DiskOutOfSpaceException最后查ClassNotFoundException90% 的 Tez 运行失败逃不出这三个坑。注意 HDFS 小文件问题。Tez 跑完任务后生成的文件数和 Reduce 数强相关如果 Reduce 数被自动推断得过大会在 HDFS 上产生大量小文件。这对于后续读取是灾难性的。我一般会在作业最后加上一个合并小文件的步骤或者在建表时使用 Hive 的TBLPROPERTIES (orc.compressionSNAPPY)配合合理的 Reduce 并行度来缓解。Tez 的本地模式Local Mode和集群模式的配置目录不同。本地模式你只需要一个tez-local.xml它很好使跑单元测试、任务调试都很快。但如果是在分布式集群上一定要把tez-site.xml放进 YARN 的 classpath 里别把它塞到 Hadoop 安装目录下就算完事等到所有 NodeManager 都能加载到同一个 Tez 配置时任务执行行为才是可预期的。提示在排查 Tez 相关问题时看日志的顺序通常应该是Hive 客户端日志定位是不是 SQL 解析失败- YARN Application 页面定位 App Master 是否有异常- Tez AM 日志定位 DAG 执行到了哪个阶段- Container 日志定位具体异常栈。顺着这条链路走绝大多数问题半小时内能定位。6.3 经验之谈Tez 后续还能怎么扩展Tez 的生态并不只有 Hive。如果你还接触过大数据领域的其他工具你可能会发现 Apache Pig 和 Cascading 同样支持 Tez 作为执行后端。这意味着如果你过去用 Pig 写的一些 ETL 脚本因为 MapReduce 的性能瓶颈跑不动了可以把执行引擎切到 Tez 而不用重写脚本。这比迁移到 Spark 或 Flink 的改造量要小得多。我自己的实际体会是真正在项目中把 Tez 的价值放到最大往往不是靠几个参数的微调而是靠对整个 DAG 执行模式的理解。一旦你习惯了用“DAG 视角”去看待一个复杂的离线计算任务你在做 SQL 改写、表设计、以及数据倾斜优化时思路会完全不一样。比如你会主动避免在 JOIN 之后立刻做全局 ORDER BY——因为这在 DAG 里意味着又一个全量 Shuffle你会更倾向于把 ORDER BY 挪到输出极少数据量前的最后一步或者在子查询里先做局部排序减少 Shuffle 的数据量。最后再分享一个小技巧Tez 的tez.runtime.pipelined-shuffle.enabled在社区版的较新版本里默认是打开的它的核心思想是让 Shuffle 阶段的数据传输变成管道式边产生边拉取而不是等全部 Map 完成后再 Shuffle。如果你在测试时发现任务卡在 Shuffle 阶段但磁盘读写量并不大可以试着关闭这个功能对比一下网络 I/O 的表现。这个小开关有时候能帮你从“磁盘瓶颈”跳到“网络瓶颈”的视角反而更快定位问题所在。