上一篇先把 Hudi 放在数据湖架构里看了一遍,知道它不是一个单纯的文件格式,而是一套能在 HDFS、S3 这类存储上管理表、事务、更新和增量消费的系统。
这篇开始搭环境。Hudi 本身并不孤立,它要和 Hadoop、Hive、Spark、Flink、Kafka、MySQL 这些组件一起工作。环境如果搭得太旧,后面很多新参数会对不上;如果只追最新版,又很容易遇到 bundle 不匹配、Hive Metastore 协议不兼容、Flink 依赖冲突之类的问题。
1. 环境基线与基础工具
1.1 环境规划
这次使用一台单机服务器,把所有服务都部署在同一台机器上。单机环境不适合生产,但非常适合学习 Hudi 的读写流程、Timeline、Hive Sync、Flink 写入、Kafka 入湖和 CDC 测试。
| 项目 | 规划 |
|---|---|
| 操作系统 | CentOS Linux 8 |
| 主机名 | hadoop1 |
| 测试 IP | 192.168.56.101 |
| 用户 | root |
| 软件包目录 | /opt/software |
| 安装目录 | /opt/module |
| 数据目录 | /data |
| 日志目录 | /logs |
| HDFS NameNode | 9870 |
| YARN ResourceManager | 8088 |
| Hive Metastore | 9083 |
| HiveServer2 | 10000 |
| Spark Web UI | 4040 |
| Flink Web UI | 8081 |
| Kafka Broker | 9092 |
| MySQL | 3306 |
先创建统一目录:
mkdir -p /opt/software /opt/module /data /logs
安装前先确认机器信息:
cat /etc/os-release
hostname
date
free -h
df -h
我这台机器是 4 核 16G、100G 系统盘。跑单机 Hadoop、Hive、Spark、Flink、Kafka、MySQL 没问题,但不要把它当成高并发压测环境。
这里有两点先说明清楚:
- CentOS Linux 8 仅用于复现实验。如果是长期维护环境,建议改用 Rocky Linux / AlmaLinux / Ubuntu LTS 等仍在维护的发行版。本文为了保持测试记录一致,继续使用 CentOS Linux 8。
- 为了减少单机测试环境权限问题,本文使用 root 用户演示;生产环境应拆分 hadoop、hive、spark、flink、kafka 等专用用户。
这台单机测试环境里的组件关系如下:

1.2 版本选择
| 组件 | 版本 | 选择原因 |
|---|---|---|
| JDK | OpenJDK 11 | Hadoop、Spark、Flink、Hive 混合部署时兼容变量更少;JDK 17 后续可以单独验证 |
| Maven | 3.9.10 | Maven 3.9.x 足够新,适合后续编译 Hudi 或调试依赖 |
| Hadoop | 3.4.1 | Hadoop 3.4.x 是较新的 Hadoop 3 分支,HDFS 和 YARN 作为测试底座 |
| Hive | 4.0.1 | Hive 4 分支,适合验证新版本 Hive Metastore 和 HiveServer2;但 Spark 3.5 直接连接 Hive 4 Metastore 可能存在 Thrift / Hive Client 兼容问题,本文先把 Hive Sync 放到后续单独处理 |
| Spark | 3.5.6 | Hudi 1.0.x 提供 Spark 3.5 对应 bundle,Spark 3.5.x 适合作为本文的稳定测试组合 |
| Flink | 1.20.1 | Hudi 1.0.x 提供 hudi-flink1.20-bundle,Flink 1.20.1 是 1.20 系列修复版 |
| Kafka | 3.9.1 | Kafka 3.9.1 是 3.9 分支的 bugfix 版本,适合做 Kafka 入湖测试 |
| MySQL | 8.0.x | 用作 Hive Metastore 和后续 CDC 测试库;仓库实际安装的小版本以 mysql --version 为准 |
| Hudi | 1.0.2 | Hudi 1.0.x 主线,提供 Spark 3.5 和 Flink 1.20 对应 bundle |
这里没有直接上 Spark 4 或 Flink 2。原因很简单:Hudi 文章后面要写的是稳定可复现实验,不是单纯追版本号。Hudi 1.0.x 对 Spark 3.5、Flink 1.20 的 bundle 支持更明确,排错成本也更低。
1.3 固定测试机时间
这一步只用于博客复现实验,不建议在真实开发环境或生产环境关闭 NTP。真实环境应保持正确系统时间、开启 NTP,并保留证书校验。
timedatectl set-ntp false
timedatectl set-timezone Asia/Shanghai
timedatectl set-time "2025-06-15 21:18:00"
有一个很实际的问题:系统时间调到过去以后,部分 HTTPS 证书可能出现 not yet valid。CentOS 8 使用 dnf 或 curl 下载时,如果遇到证书校验问题,可以在测试环境临时这样处理:
dnf -y --setopt=sslverify=false install mysql-server mysql
curl -k -O https://mirrors.huaweicloud.com/apache/maven/maven-3/3.9.10/binaries/apache-maven-3.9.10-bin.tar.gz
这类命令只允许在隔离测试机中临时使用。真实环境应恢复正确系统时间,开启 NTP,并保留证书校验。
1.4 JDK 安装
CentOS 8 可以直接安装 OpenJDK 11:
dnf -y --setopt=sslverify=false install java-11-openjdk java-11-openjdk-devel
验证:
java -version
输出类似:
openjdk version "11.0.13" 2021-10-19 LTS
OpenJDK Runtime Environment 18.9 (build 11.0.13+8-LTS)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.13+8-LTS, mixed mode, sharing)
1.5 Maven 安装
下载并解压 Maven:
cd /opt/software
curl -k -O https://mirrors.huaweicloud.com/apache/maven/maven-3/3.9.10/binaries/apache-maven-3.9.10-bin.tar.gz
tar -zxvf apache-maven-3.9.10-bin.tar.gz -C /opt/module
ln -sfn /opt/module/apache-maven-3.9.10 /opt/module/maven
配置镜像:
将下面内容写入 /opt/module/maven/conf/settings.xml,也可以合并到 ~/.m2/settings.xml。如果是覆盖 Maven 自带的 settings.xml,生产环境要保留原文件中必要的 XML 头、命名空间和已有配置;学习环境简单配置可以直接使用下面这份。
<settings>
<mirrors>
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
</mirrors>
</settings>
验证:
/opt/module/maven/bin/mvn -v
1.6 统一环境变量
后面的组件命令会反复用到 Hadoop、Hive、Spark、Flink、Kafka、Maven 和 Hudi 路径,所以先统一写到 /etc/profile.d/bigdata_env.sh:
cat > /etc/profile.d/bigdata_env.sh <<'EOF'
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk
export HADOOP_HOME=/opt/module/hadoop
export HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop
export HIVE_HOME=/opt/module/hive
export SPARK_HOME=/opt/module/spark
export FLINK_HOME=/opt/module/flink
export KAFKA_HOME=/opt/module/kafka
export MAVEN_HOME=/opt/module/maven
export HUDI_HOME=/opt/module/hudi
export PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin
export PATH=$PATH:$HIVE_HOME/bin:$SPARK_HOME/bin:$FLINK_HOME/bin
export PATH=$PATH:$KAFKA_HOME/bin:$MAVEN_HOME/bin
export HADOOP_CLASSPATH=$($HADOOP_HOME/bin/hadoop classpath 2>/dev/null)
EOF
source /etc/profile.d/bigdata_env.sh
这里先写入还不存在的组件路径没有问题。对应软链接会在后面的安装步骤里陆续创建。
1.7 Hudi bundle 准备
这次没有重新编译 Hudi,而是直接下载官方 Maven Central 上的 bundle。学习阶段优先把运行链路跑通,后面如果要改源码或排查依赖冲突,再单独写编译过程。
mkdir -p /opt/module/hudi-1.0.2/lib
ln -sfn /opt/module/hudi-1.0.2 /opt/module/hudi
curl -k -L -o /opt/module/hudi/lib/hudi-spark3.5-bundle_2.12-1.0.2.jar \
https://repo1.maven.org/maven2/org/apache/hudi/hudi-spark3.5-bundle_2.12/1.0.2/hudi-spark3.5-bundle_2.12-1.0.2.jar
curl -k -L -o /opt/module/hudi/lib/hudi-flink1.20-bundle-1.0.2.jar \
https://repo1.maven.org/maven2/org/apache/hudi/hudi-flink1.20-bundle/1.0.2/hudi-flink1.20-bundle-1.0.2.jar
curl -k -L -o /opt/module/hudi/lib/hudi-hadoop-mr-bundle-1.0.2.jar \
https://repo1.maven.org/maven2/org/apache/hudi/hudi-hadoop-mr-bundle/1.0.2/hudi-hadoop-mr-bundle-1.0.2.jar
curl -k -L -o /opt/module/hudi/lib/hudi-utilities-bundle_2.12-1.0.2.jar \
https://repo1.maven.org/maven2/org/apache/hudi/hudi-utilities-bundle_2.12/1.0.2/hudi-utilities-bundle_2.12-1.0.2.jar
curl -k -L -o /opt/module/hudi/lib/hudi-cli-bundle_2.12-1.0.2.jar \
https://repo1.maven.org/maven2/org/apache/hudi/hudi-cli-bundle_2.12/1.0.2/hudi-cli-bundle_2.12-1.0.2.jar
检查 Hudi CLI bundle 元信息:
unzip -p /opt/module/hudi/lib/hudi-cli-bundle_2.12-1.0.2.jar META-INF/MANIFEST.MF | \
egrep 'Main-Class|Implementation-Version'
输出:
Implementation-Version: 1.0.2
Main-Class: org.apache.hudi.cli.Main
如果要进一步检查 CLI 是否能启动,可以执行:
java -jar /opt/module/hudi/lib/hudi-cli-bundle_2.12-1.0.2.jar --help
1.7.1 为什么这里不重新编译 Hudi
早期搭 Hudi 环境时,经常会走源码编译路线:改 Maven 镜像、改 Hadoop / Hive 版本、处理 Confluent 依赖、排 Jetty 冲突,然后再编译 Spark、Flink、Utilities、Hive 相关 bundle。这个过程能帮助理解 Hudi 的依赖结构,但对本文这套 Hudi 1.0.2 学习环境来说,并不是第一选择。
我这里直接使用官方 bundle,主要有三个原因:
| 选择 | 适合场景 | 风险 |
|---|---|---|
| 使用官方 bundle | Spark、Flink、Hive 版本和 Hudi 已发布包匹配,只是为了跑通学习环境 | 需要严格选对 Spark / Flink / Scala 对应包 |
| 源码编译 Hudi | 需要修改源码、定制依赖、官方没有对应 bundle | Maven、Scala、Hadoop、Hive、Jetty、Guava 冲突会集中出现 |
| 手工拼 jar | 临时排查某个类缺失 | 最容易引入重复依赖,不建议作为长期方案 |
这也是我没有沿用旧版编译命令的原因。不同 Hudi 大版本的 Maven profile、模块名和依赖树都会变化,旧版本里常见的 spark3.2、flink1.13 这类编译参数,不能直接套到 Hudi 1.0.2 上。本文优先把运行链路跑通,后面如果确实需要源码编译,再单独围绕 Hudi 1.0.x 的源码结构展开。
2. 核心组件安装
2.1 Hadoop 安装
下载 Hadoop 3.4.1:
cd /opt/software
curl -k -O https://mirrors.huaweicloud.com/apache/hadoop/common/hadoop-3.4.1/hadoop-3.4.1.tar.gz
tar -zxvf hadoop-3.4.1.tar.gz -C /opt/module
ln -sfn /opt/module/hadoop-3.4.1 /opt/module/hadoop
source /etc/profile.d/bigdata_env.sh
先配置 Hadoop 的 JAVA_HOME,否则 start-dfs.sh / start-yarn.sh 可能找不到 Java:
cat >> /opt/module/hadoop/etc/hadoop/hadoop-env.sh <<'EOF'
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk
EOF
配置 core-site.xml:
<configuration>
<property>
<name>fs.defaultFS</name>
<value>hdfs://localhost:9000</value>
</property>
<property>
<name>hadoop.tmp.dir</name>
<value>/data/hadoop/tmp</value>
</property>
</configuration>
配置 hdfs-site.xml:
<configuration>
<property>
<name>dfs.replication</name>
<value>1</value>
</property>
<property>
<name>dfs.namenode.name.dir</name>
<value>file:///data/hadoop/namenode</value>
</property>
<property>
<name>dfs.datanode.data.dir</name>
<value>file:///data/hadoop/datanode</value>
</property>
<property>
<name>dfs.permissions.enabled</name>
<value>false</value>
</property>
<property>
<name>dfs.namenode.http-address</name>
<value>0.0.0.0:9870</value>
</property>
</configuration>
配置 yarn-site.xml:
<configuration>
<property>
<name>yarn.resourcemanager.hostname</name>
<value>localhost</value>
</property>
<property>
<name>yarn.resourcemanager.webapp.address</name>
<value>0.0.0.0:8088</value>
</property>
<property>
<name>yarn.nodemanager.aux-services</name>
<value>mapreduce_shuffle</value>
</property>
<property>
<name>yarn.nodemanager.resource.memory-mb</name>
<value>12288</value>
</property>
<property>
<name>yarn.nodemanager.resource.cpu-vcores</name>
<value>4</value>
</property>
</configuration>
配置 mapred-site.xml:
<configuration>
<property>
<name>mapreduce.framework.name</name>
<value>yarn</value>
</property>
<property>
<name>mapreduce.jobhistory.address</name>
<value>localhost:10020</value>
</property>
<property>
<name>mapreduce.jobhistory.webapp.address</name>
<value>0.0.0.0:19888</value>
</property>
</configuration>
格式化并启动:
hdfs namenode -format
start-dfs.sh
start-yarn.sh
mapred --daemon start historyserver
如果使用 start-dfs.sh / start-yarn.sh,需要确认 ssh localhost 可用。没有配置免密时,可以改成下面这种方式逐个启动单机守护进程:
hdfs --daemon start namenode
hdfs --daemon start datanode
yarn --daemon start resourcemanager
yarn --daemon start nodemanager
mapred --daemon start historyserver
验证:
jps
hdfs dfs -ls /
yarn node -list
HDFS Web UI 截图如下:

YARN Web UI 截图如下:

2.2 MySQL 安装
MySQL 用两处:一是 Hive Metastore,二是后续 CDC 入湖测试。本文所有密码仅用于本地单机测试环境,生产环境必须使用强密码和最小权限账户。
dnf -y --setopt=sslverify=false install mysql-server mysql
配置 /etc/my.cnf.d/hudi.cnf:
[mysqld]
server-id=1
log-bin=mysql-bin
binlog_format=ROW
binlog_row_image=FULL
default_authentication_plugin=mysql_native_password
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
[client]
default-character-set=utf8mb4
启动:
systemctl enable --now mysqld
如果 mysqld 因 default_authentication_plugin 参数无法启动,可以先删除该参数,或改为在 CREATE USER 时显式指定认证插件。
创建 Hive Metastore 库和 CDC 测试库:
CREATE DATABASE IF NOT EXISTS metastore DEFAULT CHARACTER SET utf8mb4;
CREATE USER IF NOT EXISTS 'hive'@'localhost' IDENTIFIED WITH mysql_native_password BY 'test_password';
CREATE USER IF NOT EXISTS 'hive'@'%' IDENTIFIED WITH mysql_native_password BY 'test_password';
GRANT ALL PRIVILEGES ON metastore.* TO 'hive'@'localhost';
GRANT ALL PRIVILEGES ON metastore.* TO 'hive'@'%';
CREATE DATABASE IF NOT EXISTS cdc_test DEFAULT CHARACTER SET utf8mb4;
CREATE USER IF NOT EXISTS 'cdc_user'@'%' IDENTIFIED WITH mysql_native_password BY 'test_password';
GRANT SELECT, RELOAD, SHOW DATABASES, REPLICATION SLAVE, REPLICATION CLIENT, LOCK TABLES
ON *.* TO 'cdc_user'@'%';
USE cdc_test;
CREATE TABLE IF NOT EXISTS order_info (
id BIGINT PRIMARY KEY,
user_id BIGINT,
status VARCHAR(32),
amount DECIMAL(10,2),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
INSERT INTO order_info(id, user_id, status, amount)
VALUES (1001, 10, 'CREATED', 99.80);
FLUSH PRIVILEGES;
检查 binlog:
SHOW VARIABLES LIKE 'log_bin';
SHOW VARIABLES LIKE 'binlog_format';
实际安装的小版本以本机输出为准:
mysql --version
2.3 Hive 安装
下载 Hive 4.0.1:
cd /opt/software
curl -k -O https://mirrors.huaweicloud.com/apache/hive/hive-4.0.1/apache-hive-4.0.1-bin.tar.gz
tar -zxvf apache-hive-4.0.1-bin.tar.gz -C /opt/module
ln -sfn /opt/module/apache-hive-4.0.1-bin /opt/module/hive
准备 MySQL Connector,并从前面准备好的 Hudi 目录复制 Hive 读取 bundle:
curl -k -L -o /opt/module/hudi/lib/mysql-connector-j-8.0.33.jar \
https://repo1.maven.org/maven2/com/mysql/mysql-connector-j/8.0.33/mysql-connector-j-8.0.33.jar
cp /opt/module/hudi/lib/mysql-connector-j-8.0.33.jar /opt/module/hive/lib/
cp /opt/module/hudi/lib/hudi-hadoop-mr-bundle-1.0.2.jar /opt/module/hive/lib/
Hive lib 目录也不要混入多个 Hudi 版本的 bundle;如果后续 HiveServer2 启动异常,优先检查 Hive lib 下是否有重复 Hudi、Guava、Jetty、Parquet 相关 jar。
配置 hive-site.xml:
<configuration>
<property>
<name>javax.jdo.option.ConnectionURL</name>
<value>jdbc:mysql://localhost:3306/metastore?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai</value>
</property>
<property>
<name>javax.jdo.option.ConnectionDriverName</name>
<value>com.mysql.cj.jdbc.Driver</value>
</property>
<property>
<name>javax.jdo.option.ConnectionUserName</name>
<value>hive</value>
</property>
<property>
<name>javax.jdo.option.ConnectionPassword</name>
<value>test_password</value>
</property>
<property>
<name>hive.metastore.uris</name>
<value>thrift://localhost:9083</value>
</property>
<property>
<name>hive.metastore.warehouse.dir</name>
<value>/user/hive/warehouse</value>
</property>
<property>
<name>hive.server2.thrift.bind.host</name>
<value>0.0.0.0</value>
</property>
<property>
<name>hive.server2.thrift.port</name>
<value>10000</value>
</property>
<property>
<name>hive.server2.enable.doAs</name>
<value>false</value>
</property>
<property>
<name>hive.notification.event.poll.interval</name>
<value>0s</value>
</property>
</configuration>
确认 NameNode 和 DataNode 已经启动后,再创建 HDFS 目录:
hdfs dfs -mkdir -p /user/hive/warehouse /tmp/hive /hudi /flink-checkpoints
hdfs dfs -chmod -R 777 /user/hive/warehouse /tmp/hive /hudi /flink-checkpoints
初始化 Metastore:
ls -lh /opt/module/hive/lib/mysql-connector-j-8.0.33.jar
ls -lh /opt/module/hive/lib/hudi-hadoop-mr-bundle-1.0.2.jar
schematool -dbType mysql -initSchema --verbose
启动服务:
mkdir -p /logs/hive
nohup hive --service metastore > /logs/hive/metastore.log 2>&1 &
nohup hive --service hiveserver2 > /logs/hive/hiveserver2.log 2>&1 &
验证:
beeline -u jdbc:hive2://localhost:10000 -n root -e 'show databases;'
截图如下:

这里有两个实际遇到的问题。
第一个是 HiveServer2 启动时报:
Error initializing notification event poll
Internal error processing get_current_notificationEventId
处理方式是在单机测试环境关闭事件轮询:
<property>
<name>hive.notification.event.poll.interval</name>
<value>0s</value>
</property>
第二个是 Beeline 连接时报:
User: root is not allowed to impersonate root
处理方式是关闭 HiveServer2 的 doAs:
<property>
<name>hive.server2.enable.doAs</name>
<value>false</value>
</property>
2.4 Spark 安装
Spark 验证里会用 PySpark,先安装 Python:
dnf -y --setopt=sslverify=false install python3
python3 --version
下载 Spark 3.5.6:
cd /opt/software
curl -k -O https://mirrors.huaweicloud.com/apache/spark/spark-3.5.6/spark-3.5.6-bin-hadoop3.tgz
tar -zxvf spark-3.5.6-bin-hadoop3.tgz -C /opt/module
ln -sfn /opt/module/spark-3.5.6-bin-hadoop3 /opt/module/spark
下载 Hudi Spark bundle:
cp /opt/module/hudi/lib/hudi-spark3.5-bundle_2.12-1.0.2.jar /opt/module/spark/jars/
本文下载的是 spark-3.5.6-bin-hadoop3,该发行包对应 Scala 2.12,因此 Hudi Spark bundle 也选择 _2.12 后缀。
Spark 默认只有 .template 配置文件,先复制再修改:
cd /opt/module/spark/conf
cp spark-env.sh.template spark-env.sh
cp spark-defaults.conf.template spark-defaults.conf
配置 /opt/module/spark/conf/spark-env.sh:
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk
export HADOOP_CONF_DIR=/opt/module/hadoop/etc/hadoop
export YARN_CONF_DIR=/opt/module/hadoop/etc/hadoop
export SPARK_LOG_DIR=/logs/spark
配置 /opt/module/spark/conf/spark-defaults.conf:
spark.serializer org.apache.spark.serializer.KryoSerializer
spark.sql.extensions org.apache.spark.sql.hudi.HoodieSparkSessionExtension
spark.sql.catalog.spark_catalog org.apache.spark.sql.hudi.catalog.HoodieCatalog
启动 Spark Shell 验证:
printf "spark.range(3).show(false)\n:q\n" | \
spark-shell \
--master local[1] \
--conf spark.sql.catalogImplementation=in-memory
关键输出如下:
=== spark shell smoke ===
Spark session available as 'spark'.
....
scala> spark.range(3).show(false)
25/06/15 22:03:34 WARN DFSPropertiesConfiguration: Properties file file:/etc/hudi/conf/hudi-defaults.conf not found. Ignoring to load props file
25/06/15 22:03:34 WARN DFSPropertiesConfiguration: Cannot find HUDI_CONF_DIR, please set it as the dir of hudi-defaults.conf
+---+
|id |
+---+
|0 |
|1 |
|2 |
+---+
用 Spark DataSource 写入一张 Hudi COW 表:
cat > /tmp/hudi_pyspark_test.py <<'PY'
from pyspark.sql import SparkSession
spark = (SparkSession.builder
.appName("hudi-pyspark-test")
.master("local[2]")
.config("spark.sql.catalogImplementation", "in-memory")
.getOrCreate())
base_path = "hdfs://localhost:9000/hudi/demo_hudi_orders"
rows = [
(1, "book", 39.9, 20250615210800),
(2, "pen", 3.5, 20250615210810)
]
df = spark.createDataFrame(rows, ["id", "name", "price", "ts"])
(df.write.format("hudi")
.option("hoodie.table.name", "demo_hudi_orders")
.option("hoodie.datasource.write.table.type", "COPY_ON_WRITE")
.option("hoodie.datasource.write.recordkey.field", "id")
.option("hoodie.datasource.write.precombine.field", "ts")
.option("hoodie.datasource.write.operation", "upsert")
.mode("overwrite")
.save(base_path))
(spark.read.format("hudi")
.load(base_path)
.select("id", "name", "price")
.orderBy("id")
.show(truncate=False))
spark.stop()
PY
执行:
spark-submit \
--master local[2] \
--conf spark.sql.catalogImplementation=in-memory \
/tmp/hudi_pyspark_test.py
检查 HDFS:
hdfs dfs -ls /hudi/demo_hudi_orders
hdfs dfs -ls /hudi/demo_hudi_orders/.hoodie
关键输出如下:
=== hudi hdfs table ===
Found 3 items
drwxr-xr-x - root supergroup 0 2025-06-15 21:57 /hudi/demo_hudi_orders/.hoodie
-rw-r--r-- 1 root supergroup 96 2025-06-15 21:57 /hudi/demo_hudi_orders/.hoodie_partition_metadata
-rw-r--r-- 1 root supergroup 435066 2025-06-15 21:57 /hudi/demo_hudi_orders/50540e89-d60a-4fa4-9186-3cf93e348b0c-0_0-26-29_20250615215747801.parquet
Found 7 items
drwxr-xr-x - root supergroup 0 2025-06-15 21:57 /hudi/demo_hudi_orders/.hoodie/.aux
drwxr-xr-x - root supergroup 0 2025-06-15 21:57 /hudi/demo_hudi_orders/.hoodie/.index_defs
drwxr-xr-x - root supergroup 0 2025-06-15 21:57 /hudi/demo_hudi_orders/.hoodie/.schema
drwxr-xr-x - root supergroup 0 2025-06-15 21:57 /hudi/demo_hudi_orders/.hoodie/.temp
-rw-r--r-- 1 root supergroup 1257 2025-06-15 21:57 /hudi/demo_hudi_orders/.hoodie/hoodie.properties
drwxr-xr-x - root supergroup 0 2025-06-15 21:57 /hudi/demo_hudi_orders/.hoodie/metadata
drwxr-xr-x - root supergroup 0 2025-06-15 21:57 /hudi/demo_hudi_orders/.hoodie/timeline
这里还踩到一个重要兼容问题:Spark 3.5 的内置 Hive Client 和 Hive 4.0.1 Metastore 直接配合时,Spark SQL 建表会报:
Unable to fetch table hudi_orders. Invalid method name: 'get_table'
原因是 Spark 3.5 默认的 Hive Client 版本和 Hive 4 Metastore Thrift 接口不完全匹配。本文先用 Spark DataSource 直写 HDFS 验证 Hudi 表,后面写 Hive Sync 时再单独处理 Spark 和 Hive Metastore 的同步策略。
2.5 Flink 安装
这里安装 Flink 1.20.1,过程如下:
cd /opt/software
curl -k -O https://mirrors.huaweicloud.com/apache/flink/flink-1.20.1/flink-1.20.1-bin-scala_2.12.tgz
tar -zxvf flink-1.20.1-bin-scala_2.12.tgz -C /opt/module
ln -sfn /opt/module/flink-1.20.1 /opt/module/flink
下载 Hudi Flink bundle:
cp /opt/module/hudi/lib/hudi-flink1.20-bundle-1.0.2.jar /opt/module/flink/lib/
Flink 1.15 之后 Flink 本身逐步弱化 Scala 二进制依赖,Hudi Flink 1.20 bundle 名称中不再带 Scala 后缀,按 hudi-flink1.20-bundle 选择即可。
配置 flink-conf.yaml:
jobmanager.rpc.address: localhost
jobmanager.memory.process.size: 1600m
taskmanager.memory.process.size: 4096m
taskmanager.numberOfTaskSlots: 4
state.backend: rocksdb
state.checkpoints.dir: hdfs://localhost:9000/flink-checkpoints
execution.checkpointing.interval: 60000
env.hadoop.conf.dir: /opt/module/hadoop/etc/hadoop
如果启动时报 state.backend 配置相关错误,以 Flink 1.20.1 官方配置项为准调整;本文截图对应的是当前配置已验证通过的结果。
启动:
start-cluster.sh
验证 Web UI:
curl http://localhost:8081/overview
截图如下:

Flink SQL Client 简单验证:
cat > /tmp/flink-smoke.sql <<'SQL'
SET 'sql-client.execution.result-mode' = 'tableau';
SELECT 1 AS check_value;
SQL
执行:
sql-client.sh -f /tmp/flink-smoke.sql
如果当前发行包的 SQL Client 仍要求 embedded 模式,再改用 sql-client.sh embedded -f /tmp/flink-smoke.sql,以本机实测可执行命令为准。
关键输出如下:
=== flink sql client ===
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/opt/module/flink-1.20.1/lib/log4j-slf4j-impl-2.17.1.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/opt/hadoop-3.4.1/share/hadoop/common/lib/slf4j-reload4j-1.7.36.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.apache.logging.slf4j.Log4jLoggerFactory]
Jun 15, 2025 10:04:23 PM org.jline.utils.Log logr
WARNING: Unable to create a system terminal, creating a dumb terminal (enable debug logging for more information)
[INFO] Executing SQL from file.
Command history file path: /root/.flink-sql-history
Flink SQL> [INFO] Execute statement succeeded.
Flink SQL> +----+-------------+
| op | check_value |
+----+-------------+
| +I | 1 |
+----+-------------+
Received a total of 1 row (2.84 seconds)
Flink SQL>
Shutting down the session...
done.
Flink 这一步遇到过两个小问题。
第一个是没有显式配置 JobManager 和 TaskManager 内存时,Flink 1.20 会拒绝启动。解决方式是在 flink-conf.yaml 中明确写:
jobmanager.memory.process.size: 1600m
taskmanager.memory.process.size: 4096m
第二个是 TaskManager 找不到 JobManager 地址。单机模式下补上:
jobmanager.rpc.address: localhost
2.6 Kafka 安装
下载 Kafka 3.9.1:
cd /opt/software
curl -k -O https://mirrors.huaweicloud.com/apache/kafka/3.9.1/kafka_2.13-3.9.1.tgz
tar -zxvf kafka_2.13-3.9.1.tgz -C /opt/module
ln -sfn /opt/module/kafka_2.13-3.9.1 /opt/module/kafka
Kafka 3.9 可以直接使用 KRaft 模式,不再依赖 Zookeeper。配置 config/kraft/server.properties:
process.roles=broker,controller
node.id=1
controller.quorum.voters=1@localhost:9093
listeners=PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
advertised.listeners=PLAINTEXT://localhost:9092
controller.listener.names=CONTROLLER
listener.security.protocol.map=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
log.dirs=/data/kafka/kraft-combined-logs
num.partitions=3
offsets.topic.replication.factor=1
transaction.state.log.replication.factor=1
transaction.state.log.min.isr=1
格式化存储目录:
KAFKA_CLUSTER_ID="$(/opt/module/kafka/bin/kafka-storage.sh random-uuid)"
/opt/module/kafka/bin/kafka-storage.sh format \
-t "$KAFKA_CLUSTER_ID" \
-c /opt/module/kafka/config/kraft/server.properties
启动:
mkdir -p /logs/kafka
nohup /opt/module/kafka/bin/kafka-server-start.sh /opt/module/kafka/config/kraft/server.properties \
> /logs/kafka/server.log 2>&1 &
创建 Topic 并验证生产消费:
/opt/module/kafka/bin/kafka-topics.sh \
--bootstrap-server localhost:9092 \
--create \
--if-not-exists \
--topic hudi_order_events \
--partitions 1 \
--replication-factor 1
printf '{"order_id":1001,"user_id":10,"status":"CREATED","amount":99.80,"ts":"2025-06-15 21:08:00"}\n' | \
/opt/module/kafka/bin/kafka-console-producer.sh \
--bootstrap-server localhost:9092 \
--topic hudi_order_events
/opt/module/kafka/bin/kafka-console-consumer.sh \
--bootstrap-server localhost:9092 \
--topic hudi_order_events \
--from-beginning \
--max-messages 1
关键输出如下:
=== kafka ===
Topic: hudi_order_events TopicId: s2EKHEtLQUiyT0nhC_EPFg PartitionCount: 1 ReplicationFactor: 1 Configs:
Topic: hudi_order_events Partition: 0 Leader: 1 Replicas: 1 Isr: 1 Elr: LastKnownElr:
{"order_id":1001,"user_id":10,"status":"CREATED","amount":99.80,"ts":"2025-06-15 21:08:00"}
TopicId 每次创建可能不同,重点看 Topic 已创建、Partition 正常、Consumer 能读到刚才写入的消息。
3. 验证和问题处理
3.1 一次性验证清单
服务进程:
jps | sort
我这次最终看到的关键进程包括:
NameNode
DataNode
SecondaryNameNode
ResourceManager
NodeManager
JobHistoryServer
RunJar(通常对应 Hive Metastore / HiveServer2,需要结合端口 9083、10000 判断)
Kafka
StandaloneSessionClusterEntrypoint
TaskManagerRunner
端口检查:
ss -lntp | egrep '(:9870|:8088|:9083|:10000|:8081|:9092)'
HDFS:
hdfs dfs -ls /
hdfs dfs -ls /hudi
YARN:
yarn node -list
Hive:
beeline -u jdbc:hive2://localhost:10000 -n root -e 'show databases;'
MySQL:
mysql -uroot -e "SHOW DATABASES;"
mysql -uroot -e "SHOW VARIABLES LIKE 'log_bin'; SHOW VARIABLES LIKE 'binlog_format';"
Spark:
spark-submit \
--master local[2] \
--conf spark.sql.catalogImplementation=in-memory \
/tmp/hudi_pyspark_test.py
Flink:
curl http://localhost:8081/overview
Kafka:
/opt/module/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --list
Hudi bundle:
ls -lh /opt/module/hudi/lib/hudi-*.jar
ls -lh /opt/module/spark/jars/hudi-spark3.5-bundle_2.12-1.0.2.jar
ls -lh /opt/module/flink/lib/hudi-flink1.20-bundle-1.0.2.jar
ls -lh /opt/module/hive/lib/hudi-hadoop-mr-bundle-1.0.2.jar
3.2 常见问题
3.2.1 HTTPS 证书提示 not yet valid
现象:
SSL certificate problem: certificate is not yet valid
原因通常是测试机系统时间早于证书生效时间,例如系统没有同步 NTP,或者时钟曾被手工调整。
解决:
curl -k -O https://mirrors.huaweicloud.com/apache/maven/maven-3/3.9.10/binaries/apache-maven-3.9.10-bin.tar.gz
dnf -y --setopt=sslverify=false install mysql-server
这类命令只允许在隔离测试机中临时使用。真实环境应恢复正确系统时间,开启 NTP,并保留证书校验。
3.2.2 HiveServer2 notification event poll 失败
现象:
Error initializing notification event poll
Internal error processing get_current_notificationEventId
解决:
<property>
<name>hive.notification.event.poll.interval</name>
<value>0s</value>
</property>
3.2.3 HiveServer2 impersonation 失败
现象:
User: root is not allowed to impersonate root
解决:
<property>
<name>hive.server2.enable.doAs</name>
<value>false</value>
</property>
3.2.4 Spark SQL 连接 Hive 4 Metastore 报 get_table
现象:
Unable to fetch table hudi_orders. Invalid method name: 'get_table'
原因是 Spark 3.5 默认 Hive Client 和 Hive 4 Metastore 协议不匹配。
解决思路:
# 学习阶段先用 Spark DataSource 直写 HDFS
spark-submit \
--master local[2] \
--conf spark.sql.catalogImplementation=in-memory \
/tmp/hudi_pyspark_test.py
Hive Sync 放到后面单独处理。
3.2.5 Flink TaskManager 启动失败
现象通常是 TaskManager 起不来,或者 Web UI 中没有 Task Slot。
检查:
tail -100 /opt/module/flink/log/flink-*-taskexecutor-*.log
解决:
jobmanager.rpc.address: localhost
taskmanager.memory.process.size: 4096m
taskmanager.numberOfTaskSlots: 4
3.2.6 Spark 找不到 python3
现象:
Cannot run program "python3": error=2, No such file or directory
解决:
dnf -y --setopt=sslverify=false install python3
python3 --version
3.2.7 Spark 找不到 Hudi DataSource
现象:
Failed to find data source: hudi
java.lang.ClassNotFoundException: org.apache.hudi.DefaultSource
原因通常是 Spark 没有加载 Hudi Spark bundle,或者 bundle 和 Spark / Scala 版本不匹配。本文使用的是 Spark 3.5.6,对应的包是:
hudi-spark3.5-bundle_2.12-1.0.2.jar
先检查 jar 是否真的在 Spark classpath 里:
ls -lh /opt/module/spark/jars/hudi-spark3.5-bundle_2.12-1.0.2.jar
如果不想把 jar 放到 Spark 的 jars 目录,也可以在提交任务时显式指定:
spark-submit \
--master local[2] \
--jars /opt/module/hudi/lib/hudi-spark3.5-bundle_2.12-1.0.2.jar \
--conf spark.sql.catalogImplementation=in-memory \
/tmp/hudi_pyspark_test.py
这里不要随便换成 Spark 3.4、Spark 3.3 或 Scala 2.13 的包。Hudi 的 bundle 名字里已经带了 Spark 版本线和 Scala 后缀,选错以后报错不一定直接指向版本不匹配,很多时候只表现为类找不到或方法不存在。
3.2.8 Jetty / Guava 这类依赖冲突
现象可能是:
java.lang.NoSuchMethodError: org.apache.hudi.org.apache.jetty.server.session.SessionHandler.setHttpOnly(Z)V
也可能是 Guava、Jetty、Hive Client、Thrift 相关的 NoSuchMethodError 或 ClassCastException。
这类问题的根因通常不是“少一个 jar”,而是同一个进程里出现了多套版本不一致的依赖。最常见的触发方式有三种:
- Spark 的
jars目录里混入了多个 Hudi bundle。 - 为了解决 Hive 问题,把 Hive 的大量 jar 复制进 Spark。
- 使用源码编译包时,Spark bundle、Utilities bundle 里的 Jetty / Guava 没有被正确 shade 或排除。
排查时先看当前 Spark 目录里到底有哪些相关 jar:
find /opt/module/spark/jars \
\( -name '*hudi*' -o -name '*guava*' -o -name '*jetty*' \) \
-print | sort
学习环境里,我更建议保持边界清晰:
- Spark 只放
hudi-spark3.5-bundle_2.12-1.0.2.jar。 - Flink 只放
hudi-flink1.20-bundle-1.0.2.jar。 - Hive 只放 Hive 查询 Hudi 需要的
hudi-hadoop-mr-bundle-1.0.2.jar。 - DeltaStreamer 使用
hudi-utilities-bundle_2.12-1.0.2.jar,不要把 Utilities 依赖散拆到 Spark 目录。
Spark DataSource 测试只需要 hudi-spark3.5-bundle_2.12-1.0.2.jar,不要同时把 hudi-utilities-bundle、hudi-cli-bundle、hudi-flink-bundle 放进 Spark jars 目录。
如果后续必须源码编译,再去看 Maven dependency:tree 和 bundle 模块里的 exclusions。不要一看到 NoSuchMethodError 就继续往 jars 目录里堆 jar,这样通常只会把冲突变得更隐蔽。
3.2.9 扩展:源码编译 Hudi 时 Confluent 依赖缺失
本文没有源码编译 Hudi,所以正常不会遇到这个问题。但如果后面为了定制 DeltaStreamer 或 Kafka Schema Registry 相关能力去编译 Utilities bundle,可能会看到类似依赖解析失败:
Could not find artifact io.confluent:kafka-avro-serializer
解决方向有两个:
<repository>
<id>confluent</id>
<url>https://packages.confluent.io/maven/</url>
</repository>
或者把对应版本的 Confluent jar 手动安装到本地 Maven 仓库。这个问题本质上是 Maven 仓库可见性问题,不是 Hudi 表读写逻辑的问题。学习环境优先用官方 hudi-utilities-bundle,可以先避开这类编译依赖成本。
4. 环境组合判断与总结
4.1 环境组合的判断
Hudi 环境最难的地方不在 Hudi 自己,而在大数据生态版本之间的边界。
Spark、Flink、Hive、Hadoop、Kafka 都有自己的依赖树。只要把它们放到一台机器上,Guava、Jetty、Hive Client、Thrift、Scala 版本这些问题就会冒出来。Hudi 的 bundle 能降低接入成本,但不能消除所有生态冲突。
所以我更倾向于先确定一套稳定组合:Hadoop 3.4.1 做存储和资源底座,Spark 3.5.6 跑批和 DataSource,Flink 1.20.1 跑实时写入,Hive 4.0.1 做 Metastore 和查询验证,Kafka 3.9.1 做入湖数据源,Hudi 1.0.2 做表管理层。
这套环境不是生产最佳实践,但它足够新,也足够稳定,后面讲 Timeline、索引、COW/MOR、Spark 写入、Flink 写入、DeltaStreamer 和 Hive Sync 都有真实命令可以跑。
4.2 总结
这篇主要完成了 Hudi 学习环境的单机部署:
- Hadoop HDFS 和 YARN 已启动,并通过 Web UI 验证。
- MySQL 已开启 binlog,后续可以做 CDC 测试。
- Hive Metastore 和 HiveServer2 已跑通,Beeline 可以查询。
- Spark 3.5.6 已加载 Hudi Spark bundle,并成功写入第一张 Hudi COW 表。
- Flink 1.20.1 已加载 Hudi Flink bundle,Web UI 和 SQL Client 都验证通过。
- Kafka 3.9.1 使用 KRaft 模式启动,Topic 生产消费正常。
- Hudi 1.0.2 的 Spark、Flink、Hive、Utilities、CLI bundle 都已准备好。
下一篇开始进入 Hudi 的核心概念:Timeline、Instant、FileGroup 和 FileSlice。环境搭完以后再看这些概念,会更容易把原理和 HDFS 上的真实目录结构对应起来。