前面 14 篇已经把 Hudi 的概念、写入、索引、Spark、Flink、CDC、调优和底层原理基本串起来了。这一篇作为当前 Hudi 学习笔记主线的收尾,回到一个很现实的问题:Hudi 表已经写在 HDFS 上了,Hive 怎么查?
Hive 集成 Hudi 容易被误解成“在 Hive 里建一张外表就行”。实际没有这么简单。Hive 需要知道表 schema、分区、Location、InputFormat、SerDe,还要知道 MOR 表到底查读优化视图还是实时视图。这些元数据如果手工维护,很容易和 Hudi 表真实状态不一致,所以生产里通常要用 Hive Sync。
1. Hive 集成基础
1.1 背景问题
Hudi 表本质上是文件系统上的一张事务表。它的真实状态在:
- HDFS / S3 上的 Base File 和 Log File。
.hoodie目录里的 Timeline。hoodie.properties里的表配置。- 分区目录和 FileGroup。
Hive 要查询这张表,不能只知道 Parquet 文件路径,还要知道如何构造 Hudi 文件视图。尤其是 MOR 表,Hive 如果只读 Parquet,就会漏掉 Log File 里的更新。
所以 Hive 集成 Hudi 的核心问题不是“创建一张 Hive 外表”,而是:
- 如何把 Hudi 表结构同步到 Hive Metastore。
- COW 表应该使用什么 InputFormat。
- MOR 表应该同步成 ro 表还是 rt 表。
- Hudi 表新增分区后 Hive 如何感知。
- Hive 查询执行引擎和 Hudi Hadoop MR bundle 是否兼容。
1.2 Hive Sync 的作用
Hive Sync 的作用可以理解为:把 Hudi 的文件视图翻译成 Hive 能理解的元数据。

这张图主要说明 Hive Sync 不搬运数据,而是把 Hudi 表的 schema、分区、InputFormat、SerDe 和 ro / rt 视图信息同步到 Hive Metastore。
它同步的不是数据本身,而是这些内容:
| 元数据 | 作用 |
|---|---|
| database | Hive 中的数据库 |
| table | Hive 中的外部表 |
| schema | 字段、类型、Hudi meta 字段 |
| partition | 分区字段和分区路径 |
| InputFormat | Hive 读取 Hudi 表时使用的 reader |
| SerDe | Hive 反序列化 Parquet 数据 |
| table properties | Hudi 表名、ro / rt 视图等属性 |
如果没有 Hive Sync,就要手工维护这些信息。小表测试还可以,生产环境不建议长期手写。
Hive Sync 成功时,通常会在 Hive 中生成这些表视图:
| Hudi 表类型 | Hive 中常见结果 | 说明 |
|---|---|---|
| COW | 一个 Hive 外表 | 读取最新 Base File |
| MOR + RO | table_ro 或按 skip-ro-suffix 保留原表名 |
只读 Base File,查询更轻 |
| MOR + RT | table_rt |
读取 Base File + Log File,能看到实时更新 |
这里的 ro / rt 只是 Hive Metastore 中的查询视图,不会改变 HDFS 上 Hudi 表本身的文件布局。
1.3 Hive 需要的 Hudi 依赖
Hive 侧的 Hudi 依赖要分成两类看,不建议把所有 jar 一股脑丢进 $HIVE_HOME/lib。
| 依赖 | 主要用途 | 放置建议 |
|---|---|---|
hudi-hadoop-mr-bundle |
Hive 查询 Hudi 表,提供 InputFormat / RealtimeInputFormat | 放到 HiveServer2 可加载路径 |
hudi-hive-sync-bundle |
Hive Sync Tool 同步表元数据 | 优先放到同步命令的 classpath |
hudi-utilities-bundle |
包含 DeltaStreamer、部分工具类,当前环境里也能找到 HiveSyncTool |
用于单独执行工具命令 |
Hive 查询 Hudi 表时,最关键的是 Hadoop MR bundle:
/opt/module/hudi-1.0.2/lib/hudi-hadoop-mr-bundle-1.0.2.jar
测试时可以在 Beeline 会话里临时加载:
ADD JAR /opt/module/hudi-1.0.2/lib/hudi-hadoop-mr-bundle-1.0.2.jar;
生产环境更常见的做法是把它放到 HiveServer2 能加载的路径,比如:
mkdir -p $HIVE_HOME/auxlib
cp /opt/module/hudi-1.0.2/lib/hudi-hadoop-mr-bundle-1.0.2.jar \
$HIVE_HOME/auxlib/
然后重启 HiveServer2:
hive --service hiveserver2
这一步解决的是 Hive 运行时找不到 Hudi InputFormat 的问题。如果缺少这个包,常见报错是:
ClassNotFoundException: org.apache.hudi.hadoop.HoodieParquetInputFormat
Hive Sync Tool 的依赖不要和查询依赖混为一谈。比如当前环境里我用 hudi-utilities-bundle 执行 HiveSyncTool,如果你的 Hudi 包里提供了独立的 hudi-hive-sync-bundle,也可以把 HUDI_HIVE_SYNC_JAR 替换成对应 jar。关键是把同步工具依赖放进同步命令的 classpath,而不是直接加入 HiveServer2 的运行时目录:
export HUDI_HIVE_SYNC_JAR="/opt/module/hudi-1.0.2/lib/hudi-utilities-bundle_2.12-1.0.2.jar"
export HUDI_HIVE_SYNC_CLASSPATH="$HUDI_HIVE_SYNC_JAR:$HIVE_HOME/lib/*:$HADOOP_HOME/etc/hadoop:$HADOOP_HOME/share/hadoop/common/*:$HADOOP_HOME/share/hadoop/common/lib/*:$HADOOP_HOME/share/hadoop/hdfs/*:$HADOOP_HOME/share/hadoop/mapreduce/*:$HADOOP_HOME/share/hadoop/yarn/*"
这样做的好处是:HiveServer2 负责查询,Hive Sync Tool 负责同步元数据,两条 classpath 分开,排查 ClassNotFoundException、NoSuchMethodError、Guava / HiveConf 冲突时会更直接。
如果 $HIVE_HOME/auxlib 没有被当前 HiveServer2 自动加载,需要显式设置 HIVE_AUX_JARS_PATH,或者在启动 HiveServer2 时通过 --auxpath 指定 Hudi MR bundle。还要注意,classpath 分开不代表版本兼容问题消失,Hive Sync Tool 仍然需要与当前 HiveConf / HMS API 兼容。
1.4 COW 与 MOR 在 Hive 中的视图差异
COW 表比较简单,最新数据已经在 Base File 里。Hive 外表通常使用:
org.apache.hudi.hadoop.HoodieParquetInputFormat
MOR 表更复杂,因为它有 Base File 和 Log File。Hive 里一般有两类视图:

这张图主要说明 COW 在 Hive 中通常只有一个查询视图,而 MOR 往往要区分 Read Optimized / ro 和 Real Time / rt 两种视图。
| 视图 | 常见后缀 | 读取内容 | 特点 |
|---|---|---|---|
| Read Optimized | _ro |
只读 Base File | 查询快,但可能看不到 Log File 里的最新更新 |
| Real Time | _rt |
Base File + Log File | 能看到最新更新,但读取成本更高 |
本文后面会沿用第 14 篇创建的 MOR 表验证这个差异。order_id=15001 在 Base File 里是 CREATED,后来多次更新写进 Log File,实时视图能看到 FINISHED,读优化视图只能看到旧状态。
1.5 Hive Sync Tool 命令
Hudi 提供了 HiveSyncTool。本文环境里这个类在 hudi-utilities-bundle 中:
jar tf /opt/module/hudi-1.0.2/lib/hudi-utilities-bundle_2.12-1.0.2.jar \
| grep org/apache/hudi/hive/HiveSyncTool
输出:
org/apache/hudi/hive/HiveSyncTool.class
查看参数:
java -cp "/opt/module/hudi-1.0.2/lib/hudi-utilities-bundle_2.12-1.0.2.jar:$HIVE_HOME/lib/*:$HADOOP_HOME/share/hadoop/common/*:$HADOOP_HOME/share/hadoop/common/lib/*:$HADOOP_HOME/share/hadoop/hdfs/*:$HADOOP_HOME/share/hadoop/mapreduce/*:$HADOOP_HOME/share/hadoop/yarn/*" \
org.apache.hudi.hive.HiveSyncTool \
--help
以下参数来自本文环境中 HiveSyncTool --help 的实际输出,升级 Hudi 后需要重新核对参数名和取值:
--base-path
--database
--table
--partitioned-by
--partition-value-extractor
--metastore-uris
--jdbc-url
--sync-mode
--sync-strategy
--skip-ro-suffix
HMS 模式同步命令示例:
java -cp "$HUDI_HIVE_SYNC_CLASSPATH" \
org.apache.hudi.hive.HiveSyncTool \
--sync-mode hms \
--metastore-uris thrift://localhost:9083 \
--database hudi_demo \
--table hive_cow_orders \
--base-path hdfs:///hudi/demo_spark_ds_orders \
--partitioned-by dt \
--partition-value-extractor org.apache.hudi.hive.MultiPartKeysValueExtractor \
--auto-create-database \
--sync-strategy ALL
MOR 表如果希望同步 ro 和 rt 两个视图,可以使用:
java -cp "$HUDI_HIVE_SYNC_CLASSPATH" \
org.apache.hudi.hive.HiveSyncTool \
--sync-mode hms \
--metastore-uris thrift://localhost:9083 \
--database hudi_demo \
--table hive_mor_orders \
--base-path hdfs:///hudi/demo_hudi_internal_orders \
--partitioned-by dt \
--partition-value-extractor org.apache.hudi.hive.MultiPartKeysValueExtractor \
--auto-create-database \
--sync-strategy ALL
--sync-strategy 常见取值:
| 参数值 | 含义 |
|---|---|
RO |
只同步读优化视图 |
RT |
只同步实时视图 |
ALL |
ro / rt 都同步 |
这里的 RO、RT、ALL 只影响 Hive Metastore 中同步出的查询视图,不会改变 Hudi MOR 表的数据写入方式。
--sync-mode 决定 Hive Sync Tool 怎么和 Hive 交互:
| 模式 | 连接方式 | 使用建议 |
|---|---|---|
hms |
直接访问 Hive Metastore Thrift 服务 | 最常用,链路短,适合服务端同步 |
jdbc |
通过 HiveServer2 JDBC 执行同步 | 适合只开放 HiveServer2 的环境,需要配置 JDBC URL、用户名和密码 |
hiveql |
生成或执行 Hive SQL | 更像兜底方案,不作为首选 |
还有一个容易忽略的参数是 --skip-ro-suffix。MOR 表默认可能同步出 表名_ro 和 表名_rt 两个视图;如果历史系统已经约定不带 _ro 后缀,可以用这个参数兼容旧表名。但新环境我更建议保留清晰后缀,否则后面排查“为什么查不到 Log File 里的更新”会很绕。
有些安装包会提供 run_sync_tool.sh 这类封装脚本,本质上还是在拼 HiveSyncTool 的 classpath。遇到依赖冲突时,不要只改脚本参数,要把最终执行的 java -cp 打出来检查,确认 Hadoop、Hive、Hudi 三类依赖都在正确位置。
1.6 当前环境中的 Hive Sync 兼容问题
需要注意的是,上面这些 Hive Sync Tool 命令在本文的 Hudi 1.0.2 + Hive 4.0.1 组合中没有最终跑通。我在当前组合里实际执行 Hive Sync,COW 和 MOR 都触发了同一个错误:
Exception in thread "main" java.lang.NoSuchFieldError: METASTOREURIS
at org.apache.hudi.hive.HiveSyncTool.<init>(HiveSyncTool.java:119)
at org.apache.hudi.hive.HiveSyncTool.<init>(HiveSyncTool.java:108)
at org.apache.hudi.hive.HiveSyncTool.main(HiveSyncTool.java:547)
这个问题不是 Hudi 表坏了,而是 Hudi 1.0.2 的 Hive Sync Tool 在访问 HiveConf 字段时,和 Hive 4.0.1 的 API 不完全兼容。也就是说,Hive 4.0.1 作为“比较新”的版本,并不代表和 Hudi 1.0.2 的所有 Hive 集成路径都稳定。
我的处理方式是:
- 保留 Hive 4.0.1 的真实失败记录。
- 不把文章写成“全部顺利跑通”。
- 用手工 Hive 外表验证 HiveServer2 读取 Hudi 的核心机制。
- 在结论里明确:生产环境如果要重度使用 Hive Sync,更建议优先选择 Hudi 官方文档和自己测试环境中都验证通过的 Hive 3.x 组合,或者先完整验证 Hive 4.x。
如果团队的核心诉求是 Hive 报表稳定查询 Hudi,优先选择已经完整验证 Hive Sync、ro / rt 查询、MR / Tez 查询和 schema evolution 的组合,而不是只看组件版本新旧。
2. 手工表和查询验证
2.1 手工创建 COW Hive 外表
本文用第 7 篇创建的 COW 表:
hdfs:///hudi/demo_spark_ds_orders
表属性确认如下:
hdfs dfs -cat /hudi/demo_spark_ds_orders/.hoodie/hoodie.properties \
| egrep "hoodie.table.name|hoodie.table.type|hoodie.table.partition.fields|hoodie.table.recordkey.fields" \
| sort
输出:
hoodie.table.name=demo_spark_ds_orders
hoodie.table.partition.fields=dt
hoodie.table.recordkey.fields=order_id
hoodie.table.type=COPY_ON_WRITE
因为这张表的分区目录是:
/hudi/demo_spark_ds_orders/2025-08-05
/hudi/demo_spark_ds_orders/2025-08-06
不是 Hive 风格的 dt=2025-08-05,所以手工验证时我没有直接用 MSCK REPAIR TABLE。MSCK REPAIR TABLE 更适合 Hive 风格分区目录,例如 dt=2025-08-05;本文 Hudi 表目录是非 Hive 风格分区路径,直接 repair 不能按预期生成 dt 分区。为了避免 Hive 分区字段和 Parquet 文件里的业务字段 dt 重名,我用 pt 作为 Hive 分区列,保留文件里的 dt 字段。
DDL 如下:
下面的 pt 是手工验证时使用的 Hive 分区列名,用来避开文件内业务字段 dt 与 Hive 分区字段重名导致的投影问题;生产中应优先依赖 Hive Sync 正确生成表结构。
ADD JAR /opt/module/hudi-1.0.2/lib/hudi-hadoop-mr-bundle-1.0.2.jar;
SET hive.vectorized.execution.enabled=false;
SET hive.vectorized.execution.reduce.enabled=false;
SET hive.input.format=org.apache.hadoop.hive.ql.io.HiveInputFormat;
SET hive.fetch.task.conversion=more;
CREATE DATABASE IF NOT EXISTS hudi_demo;
DROP TABLE IF EXISTS hudi_demo.hive_cow_orders_pt3;
CREATE EXTERNAL TABLE hudi_demo.hive_cow_orders_pt3 (
`_hoodie_commit_time` string,
`_hoodie_commit_seqno` string,
`_hoodie_record_key` string,
`_hoodie_partition_path` string,
`_hoodie_file_name` string,
order_id bigint,
user_id string,
amount double,
status string,
ts bigint,
dt string
)
PARTITIONED BY (pt string)
ROW FORMAT SERDE "org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe"
STORED AS INPUTFORMAT "org.apache.hudi.hadoop.HoodieParquetInputFormat"
OUTPUTFORMAT "org.apache.hadoop.hive.ql.io.parquet.MapredParquetOutputFormat"
LOCATION "hdfs:///hudi/demo_spark_ds_orders"
TBLPROPERTIES ("hoodie.table.name"="demo_spark_ds_orders");
ALTER TABLE hudi_demo.hive_cow_orders_pt3
ADD IF NOT EXISTS PARTITION (pt="2025-08-05")
LOCATION "hdfs:///hudi/demo_spark_ds_orders/2025-08-05";
ALTER TABLE hudi_demo.hive_cow_orders_pt3
ADD IF NOT EXISTS PARTITION (pt="2025-08-06")
LOCATION "hdfs:///hudi/demo_spark_ds_orders/2025-08-06";
DESCRIBE FORMATTED 里能看到关键元数据:
Location: hdfs://localhost:9000/hudi/demo_spark_ds_orders
Table Type: EXTERNAL_TABLE
SerDe Library: org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe
InputFormat: org.apache.hudi.hadoop.HoodieParquetInputFormat
OutputFormat: org.apache.hadoop.hive.ql.io.parquet.MapredParquetOutputFormat
查询:
SELECT order_id, status, dt, pt
FROM hudi_demo.hive_cow_orders_pt3
LIMIT 10;
真实输出:
+-----------+----------+-------------+-------------+
| order_id | status | dt | pt |
+-----------+----------+-------------+-------------+
| 7002 | PAID | 2025-08-05 | 2025-08-05 |
| 7001 | CREATED | 2025-08-05 | 2025-08-05 |
| 7003 | CREATED | 2025-08-06 | 2025-08-06 |
| 7004 | CREATED | 2025-08-06 | 2025-08-06 |
+-----------+----------+-------------+-------------+
4 rows selected (1.206 seconds)
这说明 HiveServer2 在 fetch 查询路径上已经可以通过 Hudi InputFormat 读取 COW 表。不过这只说明 fetch 查询路径可用,不代表聚合、排序等 MR 执行路径也已经稳定,后面会单独说明。
2.2 手工创建 MOR 实时视图
接着用第 14 篇创建的 MOR 表:
hdfs:///hudi/demo_hudi_internal_orders
表属性:
hoodie.table.name=demo_hudi_internal_orders
hoodie.table.partition.fields=dt
hoodie.table.recordkey.fields=order_id
hoodie.table.type=MERGE_ON_READ
实时视图使用:
org.apache.hudi.hadoop.realtime.HoodieParquetRealtimeInputFormat
DDL:
以下 SQL 仍然依赖第 7 节中已经执行过的 ADD JAR 和 Hive 会话参数。
DROP TABLE IF EXISTS hudi_demo.hive_mor_orders_rt_pt3;
CREATE EXTERNAL TABLE hudi_demo.hive_mor_orders_rt_pt3 (
`_hoodie_commit_time` string,
`_hoodie_commit_seqno` string,
`_hoodie_record_key` string,
`_hoodie_partition_path` string,
`_hoodie_file_name` string,
order_id bigint,
user_id string,
amount decimal(38,18),
status string,
ts bigint,
dt string
)
PARTITIONED BY (pt string)
ROW FORMAT SERDE "org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe"
STORED AS INPUTFORMAT "org.apache.hudi.hadoop.realtime.HoodieParquetRealtimeInputFormat"
OUTPUTFORMAT "org.apache.hadoop.hive.ql.io.parquet.MapredParquetOutputFormat"
LOCATION "hdfs:///hudi/demo_hudi_internal_orders"
TBLPROPERTIES (
"hoodie.table.name"="demo_hudi_internal_orders",
"hoodie.query.as.ro.table"="false"
);
ALTER TABLE hudi_demo.hive_mor_orders_rt_pt3
ADD IF NOT EXISTS PARTITION (pt="2025-10-19")
LOCATION "hdfs:///hudi/demo_hudi_internal_orders/2025-10-19";
ALTER TABLE hudi_demo.hive_mor_orders_rt_pt3
ADD IF NOT EXISTS PARTITION (pt="2025-10-20")
LOCATION "hdfs:///hudi/demo_hudi_internal_orders/2025-10-20";
MOR 表中的 amount 在当前写入 schema 中被映射为 decimal(38,18),因此 Hive DDL 按实际 Parquet schema 设置;COW 示例表则使用 double。
查询:
SELECT order_id, status, dt, pt
FROM hudi_demo.hive_mor_orders_rt_pt3
LIMIT 10;
输出:
+-----------+-----------+-------------+-------------+
| order_id | status | dt | pt |
+-----------+-----------+-------------+-------------+
| 15001 | FINISHED | 2025-10-19 | 2025-10-19 |
| 15002 | CREATED | 2025-10-19 | 2025-10-19 |
| 15003 | CREATED | 2025-10-20 | 2025-10-20 |
+-----------+-----------+-------------+-------------+
3 rows selected (1.981 seconds)
15001 能看到 FINISHED,说明实时视图确实合并了 Log File 中的更新。这里验证的是 HiveServer2 fetch 查询路径下的实时视图读取能力,不能等同于所有 Hive 执行路径都已经稳定。
2.3 手工创建 MOR 读优化视图
读优化视图使用普通 Parquet Hudi InputFormat:
org.apache.hudi.hadoop.HoodieParquetInputFormat
DDL:
DROP TABLE IF EXISTS hudi_demo.hive_mor_orders_ro_pt3;
CREATE EXTERNAL TABLE hudi_demo.hive_mor_orders_ro_pt3 (
`_hoodie_commit_time` string,
`_hoodie_commit_seqno` string,
`_hoodie_record_key` string,
`_hoodie_partition_path` string,
`_hoodie_file_name` string,
order_id bigint,
user_id string,
amount decimal(38,18),
status string,
ts bigint,
dt string
)
PARTITIONED BY (pt string)
ROW FORMAT SERDE "org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe"
STORED AS INPUTFORMAT "org.apache.hudi.hadoop.HoodieParquetInputFormat"
OUTPUTFORMAT "org.apache.hadoop.hive.ql.io.parquet.MapredParquetOutputFormat"
LOCATION "hdfs:///hudi/demo_hudi_internal_orders"
TBLPROPERTIES (
"hoodie.table.name"="demo_hudi_internal_orders",
"hoodie.query.as.ro.table"="true"
);
补充分区:
ALTER TABLE hudi_demo.hive_mor_orders_ro_pt3
ADD IF NOT EXISTS PARTITION (pt="2025-10-19")
LOCATION "hdfs:///hudi/demo_hudi_internal_orders/2025-10-19";
ALTER TABLE hudi_demo.hive_mor_orders_ro_pt3
ADD IF NOT EXISTS PARTITION (pt="2025-10-20")
LOCATION "hdfs:///hudi/demo_hudi_internal_orders/2025-10-20";
补充分区后查询:
SELECT order_id, status, dt, pt
FROM hudi_demo.hive_mor_orders_ro_pt3
LIMIT 10;
输出:
+-----------+----------+-------------+-------------+
| order_id | status | dt | pt |
+-----------+----------+-------------+-------------+
| 15001 | CREATED | 2025-10-19 | 2025-10-19 |
| 15002 | CREATED | 2025-10-19 | 2025-10-19 |
| 15003 | CREATED | 2025-10-20 | 2025-10-20 |
+-----------+----------+-------------+-------------+
3 rows selected (0.33 seconds)
这里 15001 是 CREATED,不是 FINISHED。原因是读优化视图只读 Base File,而 FINISHED 在 Log File 里。这个结果刚好说明 ro / rt 的差异:
| 表 | 15001 状态 |
原因 |
|---|---|---|
hive_mor_orders_ro_pt3 |
CREATED |
只读 Base File |
hive_mor_orders_rt_pt3 |
FINISHED |
合并 Base File + Log File |
手工建表只适合排查和验证。生产中 Hudi schema 变更、新增分区、ro / rt 视图、InputFormat 和表属性都需要持续同步,长期手工维护很容易出现 Hive 元数据与 Hudi Timeline 不一致的问题。
2.4 Hive 查询 Hudi 的执行链路
Hive 查询 Hudi 表时,大致经过下面几步:

这张图主要说明 Hive 查询 Hudi 表时,Hive Metastore 先提供表结构和分区路径,Hudi InputFormat 再在这些路径范围内根据 Timeline 构造正确的文件视图。
- Beeline 提交 SQL。
- HiveServer2 从 Hive Metastore 读取表结构。
- 表结构里包含 Hudi InputFormat。
- InputFormat 在 Hive 已知的表 Location / 分区路径范围内,根据 Hudi Timeline 和文件视图选择有效 Base File / Log File。
- COW 读取 Base File。
- MOR rt 读取 Base File + Log File。
- MOR ro 只读取 Base File。
这也是为什么 Hive Sync 很重要。Hive 外表里如果 InputFormat、SerDe、分区、Location 任何一个写错,都可能出现读不到数据、读到旧数据、MR 执行失败。
2.5 Hive 4.0.1 下遇到的真实问题
2.5.1 Hive Sync Tool 报 NoSuchFieldError: METASTOREURIS
现象:
Exception in thread "main" java.lang.NoSuchFieldError: METASTOREURIS
at org.apache.hudi.hive.HiveSyncTool.<init>(HiveSyncTool.java:119)
原因分析:
Hudi 1.0.2 的 Hive Sync Tool 访问 HiveConf 中的字段时,和 Hive 4.0.1 的 API 不完全兼容。
处理建议:
- 如果生产强依赖 Hive Sync,优先选择经过验证的 Hive 3.x 组合。
- 如果必须使用 Hive 4.x,要先在测试环境验证 Hudi Hive Sync Tool、Spark Hive Sync、Flink HMS Catalog。
- 不要只验证 Hive Metastore 能启动,还要验证 sync、查询、分区更新、MOR rt 查询。
2.5.2 VectorizedRowBatch cannot be cast to ArrayWritable
第一次跑 Hive MR 查询时,YARN 日志里出现:
java.lang.ClassCastException:
class org.apache.hadoop.hive.ql.exec.vector.VectorizedRowBatch
cannot be cast to class org.apache.hadoop.io.ArrayWritable
修复方向:
SET hive.vectorized.execution.enabled=false;
SET hive.vectorized.execution.reduce.enabled=false;
这个问题说明在本文当前环境中,Hive 4 的向量化执行路径和当前 Hudi MR reader 组合不稳。它属于执行引擎读取路径兼容问题,不是 Hudi 表数据损坏。
2.5.3 手工分区表字段投影不匹配
当我把 dt 同时作为 Hive 分区字段处理时,MR 日志里出现:
java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 10
at org.apache.hudi.hadoop.utils.HoodieArrayWritableAvroUtils.lambda$projectRecord$1(HoodieArrayWritableAvroUtils.java:60)
原因是手工 Hive DDL 的字段投影和 Hudi 文件 schema 没有完全对齐。测试时我用 pt 做 Hive 分区列,保留文件中的业务字段 dt,才让 fetch 查询成功。这也是第 7、8、9 节手工建表时使用 pt 作为 Hive 分区列,而不是直接使用 dt 作为 Hive 分区列的原因。
生产中不要靠这种手工 workaround 长期维护,应该让 Hive Sync 正确处理分区字段和 partition extractor。
2.5.4 ORDER BY 触发 MR 后失败
LIMIT 查询可以通过 fetch task 成功:
SET hive.fetch.task.conversion=more;
SELECT order_id, status, dt, pt
FROM hudi_demo.hive_cow_orders_pt3
LIMIT 10;
但 ORDER BY 会触发 MapReduce,本文当前组合仍失败:
FAILED: Execution Error, return code 2 from org.apache.hadoop.hive.ql.exec.mr.MapRedTask
这里没有继续展开 MR 失败根因,因为本文重点是 Hive 集成路径。生产验证时需要继续查看 YARN container 日志,确认到底是向量化、schema 投影、InputFormat split 还是依赖冲突。这个现象已经足够说明:HiveServer2 本地读取和 Hive MR 执行路径不是一回事。生产验证不能只跑 LIMIT 10,还要跑聚合、排序、分区过滤、JOIN 等真正会进入执行引擎的 SQL。
2.6 当前环境验证结果总表
| 验证项 | 结果 | 说明 |
|---|---|---|
| HiveServer2 启动 | 成功 | 可用 Beeline 连接 |
| COW fetch 查询 | 成功 | LIMIT 查询可返回数据 |
| MOR rt fetch 查询 | 成功 | 15001 可看到 FINISHED |
| MOR ro fetch 查询 | 成功 | 15001 只能看到 CREATED |
| Hive Sync Tool | 失败 | NoSuchFieldError: METASTOREURIS |
Hive MR ORDER BY |
失败 | 外层报 return code 2,需要继续看 YARN 日志 |
| Hive 增量查询 | 未完整验证 | 建议兼容性验证后再使用 |
3. 扩展能力和生产建议
3.1 扩展:Spark 写入时同步 Hive
Spark 写 Hudi 时可以直接开启 Hive Sync,典型参数如下:
df.write.format("hudi")
.option("hoodie.table.name", "orders")
.option("hoodie.datasource.write.table.type", "COPY_ON_WRITE")
.option("hoodie.datasource.write.recordkey.field", "order_id")
.option("hoodie.datasource.write.partitionpath.field", "dt")
.option("hoodie.datasource.write.precombine.field", "ts")
.option("hoodie.datasource.hive_sync.enable", "true")
.option("hoodie.datasource.hive_sync.mode", "hms")
.option("hoodie.datasource.hive_sync.metastore.uris", "thrift://localhost:9083")
.option("hoodie.datasource.hive_sync.database", "hudi_demo")
.option("hoodie.datasource.hive_sync.table", "orders")
.option("hoodie.datasource.hive_sync.partition_fields", "dt")
.option(
"hoodie.datasource.hive_sync.partition_extractor_class",
"org.apache.hudi.hive.MultiPartKeysValueExtractor"
)
.mode("append")
.save("hdfs:///hudi/orders")
这些参数名以本文 Hudi 1.0.2 环境为准,升级 Hudi 后需要重新核对 Hive Sync 参数是否有迁移。需要注意,本文环境中 standalone HiveSyncTool 已经因为 Hive 4.0.1 兼容问题失败;Spark 写入时开启 Hive Sync 也需要单独验证,不能默认一定能绕开该兼容问题。
如果是 MOR 表,还要注意同步策略和表名后缀。很多查询引擎默认更适合读 ro 表,实时查询才读 rt 表。
3.2 扩展:Flink 使用 HiveCatalog / HMS Catalog
Flink 侧有三种常见方式,它们不是必须同时使用:
| 方式 | 目标 | 说明 |
|---|---|---|
| Flink HiveCatalog | 访问 Hive Metastore 里已经存在的表 | 适合查询或管理已同步到 HMS 的表 |
| Hudi Catalog HMS mode | 用 Hudi Catalog 对接 HMS 管理 Hudi 表 | 配置项随 Hudi / Flink 版本变化较多 |
| Flink 写 Hudi 时开启 hive_sync | 写入 Hudi 后同步 Hive 元数据 | 依赖 checkpoint、Hive Sync 参数和 HMS 兼容性 |
第一种是 Flink 直接使用 HiveCatalog。这个方式不是 Hudi Catalog,而是让 Flink SQL 直接访问 Hive Metastore,适合查询或管理普通 Hive 表,也可以看到已经同步到 Hive 的 Hudi 外表。
CREATE CATALOG hive_catalog WITH (
'type' = 'hive',
'default-database' = 'default',
'hive-conf-dir' = '/opt/module/hive/conf',
'hadoop-conf-dir' = '/opt/module/hadoop-3.4.1/etc/hadoop'
);
USE CATALOG hive_catalog;
如果 Flink 启动时报 Guava 或 Hadoop 类冲突,优先检查 $FLINK_HOME/lib 里 Hive connector、Hadoop 依赖和 Hudi bundle 是否重复。Flink 这条链路对 jar 版本很敏感,能进入 SQL Client 不代表真正查询时不会冲突。
第二种是 Hudi Catalog 的 HMS mode:
CREATE CATALOG hudi_hms WITH (
'type' = 'hudi',
'mode' = 'hms',
'catalog.path' = 'hdfs:///hudi/catalog',
'hive.conf.dir' = '/opt/module/hive/conf',
'table.external' = 'true'
);
USE CATALOG hudi_hms;
这段是配置思路示例,本文没有把 Hudi Catalog HMS mode 作为稳定链路完整验证。Flink Hudi Catalog 的参数名随版本变化较多,实际使用前需要以当前 Flink 1.20.1 + Hudi 1.0.2 SQL Client 的 SHOW CATALOGS、SHOW TABLES 和写入结果为准。
第三种是在 Flink Hudi 表参数里开启 Hive 同步。下面是 HMS 模式的写法,参数名以当前 Hudi 1.0.x 生态为准,升级时要重新核对:
CREATE TABLE flink_hive_sync_orders (
order_id BIGINT PRIMARY KEY NOT ENFORCED,
user_id STRING,
amount DECIMAL(10, 2),
status STRING,
ts BIGINT,
dt STRING
)
PARTITIONED BY (dt)
WITH (
'connector' = 'hudi',
'path' = 'hdfs:///hudi/flink_hive_sync_orders',
'table.type' = 'COPY_ON_WRITE',
'precombine.field' = 'ts',
'hive_sync.enable' = 'true',
'hive_sync.mode' = 'hms',
'hive_sync.metastore.uris' = 'thrift://localhost:9083',
'hive_sync.db' = 'hudi_demo',
'hive_sync.table' = 'flink_hive_sync_orders',
'hive_sync.partition_fields' = 'dt',
'hive_sync.partition_extractor_class' = 'org.apache.hudi.hive.MultiPartKeysValueExtractor'
);
如果使用 jdbc 模式,还要补充 HiveServer2 的 JDBC URL、用户名和密码。能用 hms 时我会优先选 hms,因为它少绕一层 HiveServer2,问题定位更清晰。
无论哪种方式,核心都还是:
- Hive Metastore 地址。
- database / table。
- partition fields。
- partition extractor。
- ro / rt 视图策略。
Flink 实时写入时尤其要注意 checkpoint。Hive Metastore 里能看到表,不代表数据已经形成 completed instant;要结合 .hoodie/timeline 判断。
3.3 扩展:Hive 增量查询
这一节主要整理 Hive 增量查询的配置思路,本文当前环境没有把 Hive 增量查询作为稳定生产路径验证通过。Hive 增量查询不是只设置一个 consume.mode 就结束。表类型不同,InputFormat 和查询表也不一样。
COW 表的增量查询一般在 COW 外表上设置表级消费参数,并用 _hoodie_commit_time 做过滤:
SET hive.input.format=org.apache.hadoop.hive.ql.io.HiveInputFormat;
SET hoodie.hive_cow_orders_pt3.consume.mode=INCREMENTAL;
SET hoodie.hive_cow_orders_pt3.consume.max.commits=5;
SET hoodie.hive_cow_orders_pt3.consume.start.timestamp=20250805163300000;
SELECT order_id, status, `_hoodie_commit_time`
FROM hudi_demo.hive_cow_orders_pt3
WHERE `_hoodie_commit_time` > '20250805163300000';
MOR 表如果要做增量查询,应该针对 rt 表,并切到 Hudi 的 Combine InputFormat:
SET hive.input.format=org.apache.hudi.hadoop.hive.HoodieCombineHiveInputFormat;
SET hoodie.hive_mor_orders_rt_pt3.consume.mode=INCREMENTAL;
SET hoodie.hive_mor_orders_rt_pt3.consume.max.commits=5;
SET hoodie.hive_mor_orders_rt_pt3.consume.start.timestamp=20251019182700000;
SELECT order_id, status, `_hoodie_commit_time`
FROM hudi_demo.hive_mor_orders_rt_pt3
WHERE `_hoodie_commit_time` > '20251019182700000';
这里的 consume.start.timestamp 应该使用 Hudi Timeline 中的 completed instant 时间,不是业务字段 ts,也不是文章发布时间。consume.start.timestamp 决定 Hudi InputFormat 从哪个 instant 开始消费,WHERE 条件更多是结果层面的二次过滤和展示,二者不要混淆。
查完以后要把会话参数恢复掉,避免影响后续普通 Hive 查询:
SET hive.input.format=org.apache.hadoop.hive.ql.io.HiveInputFormat;
SET hoodie.hive_mor_orders_rt_pt3.consume.mode=SNAPSHOT;
更稳妥的方式是重新打开 Beeline 会话,或者显式清理 / 覆盖所有相关的 consume.* 参数,包括 COW 和 MOR 表上的 consume.mode、consume.max.commits、consume.start.timestamp。
这里还有一个坑:如果没有设置合适的 hive.input.format,实时视图查询可能会出现类似下面的错误:
HoodieRealtimeReader can only work on RealTimeSplit
这不是数据坏了,而是 Hive 执行计划没有按 Hudi realtime split 组织输入。COW、MOR ro、MOR rt、MOR incremental 的 InputFormat 不要混用。
这类查询适合按 commit 拉取变化,但我更倾向于把 Hive 增量查询放在“兼容性验证通过以后再使用”。原因是 Hive 本身不是流式消费引擎,如果要做稳定的增量 pipeline,Spark Incremental Query 或 Flink Streaming Query 通常更可控。
Hive 更适合作为查询入口、兼容老数仓报表、验证 Hudi 表可见性,而不是作为复杂增量消费的主力。
3.4 生产建议
如果生产环境要接 Hive,我会按下面顺序验证:
- 先确认 Hudi 表本身能被 Spark 正常读取。
- 再确认 Hive Metastore、HiveServer2、Beeline 可用。
- 加载
hudi-hadoop-mr-bundle,确认 HiveServer2 能找到 Hudi InputFormat。 - 单独准备 Hive Sync classpath,确认
hudi-hive-sync-bundle或hudi-utilities-bundle可执行。 - 在兼容版本组合中,用 Hive Sync 同步 COW 表。
- 查询 COW 表的
LIMIT、分区过滤、聚合、排序。 - 在兼容版本组合中,同步 MOR 表的 ro 和 rt 视图。
- 验证 ro / rt 是否符合预期。
- 验证 Hive 增量查询所需的
consume.mode和hive.input.format。 - 跑一次新增分区同步。
- 跑一次 schema 变更同步。
- 不要只用
LIMIT 10验证,必须验证会触发执行引擎的 SQL,例如COUNT、GROUP BY、ORDER BY、分区过滤、JOIN。 - 最后再评估是否让报表或调度任务依赖 Hive 查询 Hudi。
在版本选择上,本文的结论比较明确:在本文当前测试环境中,Hadoop 3.4.1、Hudi 1.0.2、Hive 4.0.1 这个组合可以启动 HiveServer2,也可以通过 fetch 路径读到 Hudi 数据,但 Hive Sync Tool 和 Hive MR 查询路径存在兼容问题。如果当前版本组合 Hive Sync 无法跑通,就不要进入生产依赖。生产里不能因为 Hive 4.0.1 更新,就默认它是 Hudi 1.0.2 的最佳选择。稳定性优先时,Hive 3.x 仍然值得作为备选组合验证。
4. 使用边界与总结
4.1 Hive 集成使用边界
Hive 集成 Hudi 的核心不是“Hive 能不能读 Parquet”,而是“Hive 能不能按 Hudi 的文件视图读对数据”。
COW 表的查询路径比 MOR 简单,最新状态主要落在 Base File 中,但 Hive 仍然应该通过 Hudi InputFormat 按 Timeline 读取,而不是简单当作普通 Parquet 目录处理。MOR 表不一样,Base File 只是某个压缩点的状态,Log File 才保存了后续更新。如果 Hive 只读 ro 表,看到的是读优化结果;如果业务要最新状态,就必须读 rt 表。
Hive Sync 的价值就在这里:它把 Hudi 的表视图翻译成 Hive 能理解的表。但这个翻译层高度依赖版本兼容。Hudi、Hive、Hadoop、Spark/Flink 这些组件任何一个版本变化,都可能影响同步工具或查询路径。
所以我对 Hive 集成的态度是:可以接,但要谨慎。Hive 适合承接老数仓查询和报表生态,不适合作为验证 Hudi 是否正确的唯一入口。Hudi 表的第一真相仍然应该回到 Timeline、HDFS 文件和 Spark/Flink 读写结果上。
4.2 总结
这篇主要完成了 Hudi 系列最后一块内容:
- Hive Sync 的本质是同步 Hudi 表元数据,不是搬运数据。
- COW 表在 Hive 中通常对应一个外表。
- MOR 表在 Hive 中要区分 ro 和 rt 视图。
- rt 视图能看到 Log File 中的最新更新,ro 视图只能看到 Base File。
- Hive 查询 Hudi 依赖
hudi-hadoop-mr-bundle、InputFormat、SerDe、Location 和分区。 - Hive Sync 还要单独关注
hudi-hive-sync-bundle、sync mode、sync strategy 和 classpath。 - Hive 增量查询要区分 COW、MOR rt、InputFormat 和
_hoodie_commit_time过滤。 - 当前 Hudi 1.0.2 + Hive 4.0.1 组合中,HiveServer2 fetch 路径可以读 COW 和 MOR ro / rt 表,但 Hive Sync Tool 和触发 MR 的查询路径仍存在兼容问题。
- 生产环境要优先选择实测稳定的组合,不要盲目追组件版本新旧。
这 15 篇 Hudi 学习笔记从 Hudi 是什么,到核心概念、表类型、索引、写入、Spark、Flink、CDC、调优、底层原理,再到 Hive 集成。后续如果继续扩展,我会更倾向于单独写 Hudi 1.x 新特性、Hudi 与 Trino 查询、Hudi on S3、以及生产故障案例。