Apache Hudi 学习笔记 02:Hudi 测试环境安装


上一篇先把 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 没问题,但不要把它当成高并发压测环境。

这里有两点先说明清楚:

  1. CentOS Linux 8 仅用于复现实验。如果是长期维护环境,建议改用 Rocky Linux / AlmaLinux / Ubuntu LTS 等仍在维护的发行版。本文为了保持测试记录一致,继续使用 CentOS Linux 8。
  2. 为了减少单机测试环境权限问题,本文使用 root 用户演示;生产环境应拆分 hadoop、hive、spark、flink、kafka 等专用用户。

这台单机测试环境里的组件关系如下:

Hudi 单机测试环境组件关系

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 使用 dnfcurl 下载时,如果遇到证书校验问题,可以在测试环境临时这样处理:

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.2flink1.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 截图如下:

HDFS Web UI

YARN 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

如果 mysqlddefault_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&amp;allowPublicKeyRetrieval=true&amp;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;'

截图如下:

Hive Beeline 验证

这里有两个实际遇到的问题。

第一个是 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 的同步策略。

这里安装 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 Web UI

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 放到后面单独处理。

现象通常是 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 相关的 NoSuchMethodErrorClassCastException

这类问题的根因通常不是“少一个 jar”,而是同一个进程里出现了多套版本不一致的依赖。最常见的触发方式有三种:

  1. Spark 的 jars 目录里混入了多个 Hudi bundle。
  2. 为了解决 Hive 问题,把 Hive 的大量 jar 复制进 Spark。
  3. 使用源码编译包时,Spark bundle、Utilities bundle 里的 Jetty / Guava 没有被正确 shade 或排除。

排查时先看当前 Spark 目录里到底有哪些相关 jar:

find /opt/module/spark/jars \
  \( -name '*hudi*' -o -name '*guava*' -o -name '*jetty*' \) \
  -print | sort

学习环境里,我更建议保持边界清晰:

  1. Spark 只放 hudi-spark3.5-bundle_2.12-1.0.2.jar
  2. Flink 只放 hudi-flink1.20-bundle-1.0.2.jar
  3. Hive 只放 Hive 查询 Hudi 需要的 hudi-hadoop-mr-bundle-1.0.2.jar
  4. 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-bundlehudi-cli-bundlehudi-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 学习环境的单机部署:

  1. Hadoop HDFS 和 YARN 已启动,并通过 Web UI 验证。
  2. MySQL 已开启 binlog,后续可以做 CDC 测试。
  3. Hive Metastore 和 HiveServer2 已跑通,Beeline 可以查询。
  4. Spark 3.5.6 已加载 Hudi Spark bundle,并成功写入第一张 Hudi COW 表。
  5. Flink 1.20.1 已加载 Hudi Flink bundle,Web UI 和 SQL Client 都验证通过。
  6. Kafka 3.9.1 使用 KRaft 模式启动,Topic 生产消费正常。
  7. Hudi 1.0.2 的 Spark、Flink、Hive、Utilities、CLI bundle 都已准备好。

下一篇开始进入 Hudi 的核心概念:Timeline、Instant、FileGroup 和 FileSlice。环境搭完以后再看这些概念,会更容易把原理和 HDFS 上的真实目录结构对应起来。


文章作者: hnbian
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hnbian !
评论
  目录