☰
Hudi入门实战:从零搭建本地COW表与时间旅行验证
2026/10/17 23:30:47 网站建设 项目流程

1. 为什么“Hudi详解”不能从官网文档开始讲起

Apache Hudi 这个词,最近两年在数据湖技术圈里出现的频率,已经高到让不少刚转岗做实时数仓的工程师产生幻听——开会时听到“Hudi”两个音节,下意识就摸键盘想敲spark-submit --conf hoodie...。但真正动手搭第一个表、跑通第一条 upsert 流程、查出第一条hoodie_commit_time的人,不到三成。我见过太多团队:PPT里写着“基于Hudi构建准实时湖仓一体架构”,生产环境里却还卡在ClassNotFoundException: org.apache.hudi.DataSourceWriteOptions上整整三天。

这不是能力问题,是信息断层。Hudi 官网文档写得极严谨,像一本法律条文汇编:每个参数有定义、有默认值、有取值范围,但唯独不告诉你——这个参数在什么场景下必须改,改错会触发哪类静默失败,以及为什么 Spark 3.3 和 Flink 1.16 对同一个hoodie.datasource.write.recordkey.field的解析逻辑会差出 4 小时的延迟。这种细节,不会出现在 Javadoc 里,也不会写进 Release Notes,它只活在某次凌晨三点的线上事故复盘会上,某个开发者盯着日志里反复出现的DuplicateKeyException喃喃自语:“早知道这里要加前缀……”

所以这篇“Hudi详解一”,我们不照搬概念定义,也不堆砌 API 列表。我们从一个真实可复现的最小闭环切入:用最简配置,在本地伪分布式 Spark 环境中,完成一次带主键更新的增量写入,并验证其时间旅行(Time Travel)能力。整个过程不依赖 HDFS、不启动 Hive Metastore、不配置 AWS S3 权限——所有操作都在单机上完成,耗时不超过 22 分钟。你不需要提前装好 Hadoop 生态全家桶,只需要确认本机 Java 8+、Spark 3.3.0(或 3.4.0)、Python 3.8+ 已就绪。后面我会把每一步命令背后的意图拆开说透,比如为什么--conf spark.sql.adaptive.enabled=false是必加项,为什么hoodie.datasource.write.operation=upsert在首次写入时反而会报错,这些不是玄学,是 Spark Catalyst 优化器与 Hudi 文件版本管理机制碰撞出的真实火花。

关键词里虽然没填,但这一篇的核心锚点非常明确:Hudi 的写入语义一致性、文件组织结构的物理约束、以及 Spark DataSource API 层的隐式契约。这三个点,是所有后续扩展(Flink 集成、Clustering 优化、Compaction 策略)的地基。地基没打牢,往上盖多漂亮的 Delta Stream 或 MOR 表,都可能在某次小版本升级后突然崩塌。接下来的内容,每一行代码、每一个配置、每一次验证,都服务于这三点的具象化呈现。

2. 从零构建第一个 Hudi 表:避开 90% 新手踩过的初始化陷阱

很多教程一上来就甩出一长串spark-submit命令,参数密密麻麻,像一张加密电报。结果读者复制粘贴执行,第一行就报NoClassDefFoundError。这不是你的错,是教程省略了最关键的上下文:Hudi 不是一个开箱即用的 Spark 内置模块,它是一套需要显式加载的第三方数据源插件,且版本兼容性极其苛刻。Spark 3.2.x 和 3.3.x 对 Hudi 0.13.0 的支持程度,差异大到足以让整个作业在AnalysisException阶段直接退出,而错误日志里甚至找不到 “hudi” 字样。

2.1 环境准备:三步锁定“能跑通”的最小组合

第一步,确认 Spark 版本与 Hudi 二进制包的精确匹配。别信“兼容 Spark 3.x”这种模糊描述。实测下来,最稳的组合是:

  • Spark 3.3.0(Scala 2.12) + Hudi 0.13.0(hudi-spark3.3-bundle_2.12-0.13.0.jar)
  • Spark 3.4.0(Scala 2.12) + Hudi 0.14.0(hudi-spark3.4-bundle_2.12-0.14.0.jar)

为什么强调 Scala 版本?因为 Spark 3.3.0 的二进制发行版默认是 Scala 2.12 编译的,而 Hudi 的 bundle jar 必须与之完全一致。如果你用的是 Spark 3.3.0(Scala 2.13),那hudi-spark3.3-bundle_2.12就是废品。检查方法很简单,在 Spark 安装目录下运行:

$SPARK_HOME/bin/spark-shell --version | grep "Scala version"

输出Scala version 2.12.17,就锁死要用_2.12后缀的 Hudi jar。

第二步,下载并放置 bundle jar。去 Apache Hudi 官网的 Releases 页面 ,找到对应版本,下载hudi-sparkX.X-bundle_2.12-Y.Y.Y.jar。把它放到$SPARK_HOME/jars/目录下。注意:不要放在--jars参数里动态加载,那样会导致 Spark SQL 的 Catalog 初始化失败。必须物理放入jars/目录,让 Spark Driver 在启动时就能扫描到HoodieDataSource类。

第三步,禁用 Spark 自适应查询执行(AQE)。这是绝大多数新手忽略的致命开关。Hudi 的写入流程严重依赖对 DataFrame 物理计划的精确控制,而 AQE 会在运行时动态合并 shuffle 分区、重写 join 策略,导致 Hudi 的HoodieMergeOnReadTable无法正确识别preCombineField字段,最终生成大量重复记录。在spark-defaults.conf中添加:

spark.sql.adaptive.enabled false spark.sql.adaptive.coalescePartitions.enabled false

或者在每次spark-shell启动时强制指定:

$SPARK_HOME/bin/spark-shell \ --conf spark.sql.adaptive.enabled=false \ --conf spark.sql.adaptive.coalescePartitions.enabled=false

提示:如果你跳过这一步,后续写入的数据在read时会出现诡异的字段错位,比如name字段显示为age的值,且count()结果远大于实际行数。这不是数据损坏,是 AQE 重写了执行计划后,Hudi 的列映射逻辑失效了。

2.2 创建第一个 COW 表:用最简 SQL 验证核心链路

进入spark-shell后,我们不写一行 Scala,全部用 Spark SQL 操作。这样更贴近数仓同学的实际使用场景,也更容易暴露配置问题。

首先,创建一个内存表作为原始数据源:

CREATE OR REPLACE TEMP VIEW source_data AS SELECT * FROM VALUES ('user_001', 'Alice', 28, '2023-10-01 10:00:00'), ('user_002', 'Bob', 35, '2023-10-01 10:05:00'), ('user_003', 'Charlie', 42, '2023-10-01 10:10:00') AS t(user_id, name, age, ts);

然后,执行 Hudi 写入。注意,这里不用INSERT INTO,而是用CREATE TABLE ... USING HUDI语法,这是 Hudi 0.12+ 推荐的声明式建表方式:

CREATE TABLE IF NOT EXISTS hudi_users_cow ( user_id STRING, name STRING, age INT, ts STRING, `partition` STRING ) USING HUDI LOCATION '/tmp/hudi_users_cow' TBLPROPERTIES ( 'primaryKey' = 'user_id', 'preCombineField' = 'ts', 'hoodie.table.name' = 'hudi_users_cow', 'hoodie.datasource.write.recordkey.field' = 'user_id', 'hoodie.datasource.write.partitionpath.field' = 'partition', 'hoodie.datasource.write.table.name' = 'hudi_users_cow', 'hoodie.datasource.write.operation' = 'bulk_insert', 'hoodie.datasource.write.hive_style_partitioning' = 'true' ) AS SELECT user_id, name, age, ts, substring(ts, 1, 10) AS `partition` FROM source_data;

这段 SQL 看似简单,但每个TBLPROPERTIES都有深意:

  • 'primaryKey' = 'user_id':这是 Hudi 逻辑层面的主键,用于 upsert 时判断是否为更新。它必须与'hoodie.datasource.write.recordkey.field'的值完全一致,否则后续 update 会变成 insert。
  • 'preCombineField' = 'ts':当同一批数据中出现相同user_id的多条记录时,Hudi 会按ts字段取最大值(最新时间戳)的那条保留。这个字段必须是STRING或TIMESTAMP类型,且在 SELECT 子句中必须显式写出。
  • 'hoodie.datasource.write.partitionpath.field' = 'partition':注意,这里的partition是你表结构里的一个物理列名,不是 Hive 的分区字段名。Hudi 会读取该列的值(如'2023-10-01')作为 HDFS/S3 路径的一部分(/tmp/hudi_users_cow/2023-10-01/)。如果写成'ts',而ts是2023-10-01 10:00:00这种格式,路径就会变成/tmp/hudi_users_cow/2023-10-01 10:00:00/,空格会导致后续读取失败。
  • 'hoodie.datasource.write.operation' = 'bulk_insert':首次建表必须用bulk_insert,而不是upsert。upsert要求底层已存在.hoodie元数据目录和 commit timeline,首次写入时该目录为空,强行用upsert会抛HoodieTableNotFoundException。

执行成功后,你会看到类似这样的日志:

Commit Time: 20231001101522000 Total Files Inserted: 3 Total Files Updated: 0

这表示第一次写入完成,commit time 是20231001101522000(精确到毫秒的时间戳)。这个 commit time,就是 Hudi 时间旅行的钥匙。

2.3 验证写入结果:用原生 Spark SQL 读取,而非 Hudi CLI

很多教程教大家用hudi-cli.sh查看表结构,这在本地开发阶段反而是障碍。CLI 需要额外配置 Hadoop classpath,容易出错。我们直接用 Spark SQL 读取:

-- 读取最新快照 SELECT * FROM hudi_users_cow ORDER BY user_id; -- 读取特定 commit time 的快照(时间旅行) SELECT * FROM hudi_users_cow VERSION AS OF '20231001101522000' ORDER BY user_id;

如果一切正常,两条查询应返回完全相同的结果。但如果VERSION AS OF报错AnalysisException: The table hudi_users_cow does not support time travel,说明hoodie.datasource.read.enable.incremental没有生效,根源往往在spark.sql.adaptive.enabled没关掉,或者 Hudi jar 没放对位置。

注意:VERSION AS OF语法是 Spark 3.3+ 原生支持的,无需额外配置。但前提是 Hudi 表必须是通过USING HUDI创建的,且底层文件系统(这里是本地磁盘)支持listStatus操作。如果换成 S3,还需要确保spark.hadoop.fs.s3a.impl配置正确。

3. 解剖 Hudi 的文件组织:.hoodie目录里藏着什么秘密

当你执行完上面的CREATE TABLE ... USING HUDI命令后,打开/tmp/hudi_users_cow/目录,会看到两个东西:一个叫.hoodie的隐藏目录,和若干以2023-10-01开头的分区目录。前者是 Hudi 的“大脑”,后者是它的“身体”。理解.hoodie里每个文件的作用,是掌握 Hudi 运维和排错能力的分水岭。

3.1.hoodie目录结构全景图:commit、clean、savepoint 的物理映射

进入/tmp/hudi_users_cow/.hoodie/,你会看到:

. ├── 20231001101522000.commit # 主要 commit 文件,记录本次写入的元数据 ├── 20231001101522000.commit.requested # commit 开始前的请求文件(临时) ├── 20231001101522000.inflight # commit 正在进行中的标记(临时) ├── archived/ # 归档目录,存放旧的 commit 文件 │ ├── .commits_.archive.1 # 归档索引 │ └── 20231001101000000.commit # 更早的 commit ├── hoodie.properties # 表级核心配置,相当于 Hudi 的“身份证” └── rollback/ # 回滚相关文件(本次无)

其中,20231001101522000.commit是最关键的文件。它不是一个二进制,而是一个纯文本的 JSON 文件。用cat打开它,内容类似:

{ "commitTime" : "20231001101522000", "operationType" : "BULK_INSERT", "metadata" : { "totalRecords" : 3, "totalLogFiles" : 0, "totalUpdatedRecords" : 0, "totalDeletedRecords" : 0, "totalInsertRecords" : 3, "totalUpsertRecords" : 0, "totalFailedRecords" : 0 }, "fileIdAndRelativePaths" : { "f0b1e2d3-4a56-7890-1234-567890abcdef-0" : [ "2023-10-01/0f0b1e2d3-4a56-7890-1234-567890abcdef-0_0-0-1_20231001101522000.parquet" ] } }

这个 JSON 里藏着三个关键事实:

  1. operationType是BULK_INSERT:证明我们确实用了正确的首次写入模式。如果这里显示UPSERT,说明建表时TBLPROPERTIES里的hoodie.datasource.write.operation没生效,可能是拼写错误(比如写成了upsertt)或引号缺失。

  2. fileIdAndRelativePaths映射了逻辑 fileId 到物理 parquet 文件路径:f0b1e2d3-...-0是 Hudi 自动生成的 fileId,它和partition、commitTime共同构成文件的唯一标识。这个 fileId 在后续的upsert操作中会被复用,用于定位需要合并的 base file。

  3. totalLogFiles: 0:因为我们建的是 Copy-On-Write (COW) 表,所有数据都直接写成 parquet,没有生成任何.log文件。如果是 Merge-On-Read (MOR) 表,这里就会是1或更多,对应.log文件数量。

再看hoodie.properties文件,它是整个表的“宪法”:

# Created by Hudi 0.13.0 hoodie.table.version=3 hoodie.table.name=hudi_users_cow hoodie.database.name=default hoodie.table.type=COPY_ON_WRITE hoodie.timeline.layout.version=1 hoodie.archive.log.file.max.size=1073741824 hoodie.cleaner.policy=KEEP_LATEST_COMMITS hoodie.cleaner.commits.retained=10

其中hoodie.table.type=COPY_ON_WRITE是核心。它决定了:

  • 写入时:所有更新都重写整个 parquet 文件(因此bulk_insert很快,但upsert较慢);
  • 读取时:无需合并 log 文件,性能稳定,适合 OLAP 查询;
  • 存储成本:比 MOR 高约 15%-20%,因为每次 update 都生成新文件。

实操心得:很多团队在选型时纠结 COW 还是 MOR,其实有个简单判据:如果你的业务要求“读延迟 < 1 秒”,且写入 QPS < 1000,选 COW;如果写入峰值 QPS > 5000,且能接受读取时 100ms 级别的额外开销,再考虑 MOR。别被“Merge-On-Read 听起来更先进”误导,先进不等于适合你。

3.2 分区目录下的文件命名规则:fileId、instantTime、taskIndex的含义

进入/tmp/hudi_users_cow/2023-10-01/,你会看到一个文件:

0f0b1e2d3-4a56-7890-1234-567890abcdef-0_0-0-1_20231001101522000.parquet

这个超长文件名不是随机生成的,它由四部分用下划线_连接:

<fileId>_<instantTime>_<taskIndex>_<commitTime>.parquet
  • <fileId>:0f0b1e2d3-4a56-7890-1234-567890abcdef-0,全局唯一的文件标识符,由 UUID + 后缀-0构成。同一个fileId在不同 commit 中会生成不同文件(如..._20231001101522000.parquet和..._20231001102000000.parquet),代表同一份数据的不同版本。

  • <instantTime>:0-0-1,这是一个三元组,格式为<partitionId>-<bucketId>-<taskIndex>。0-0-1表示这是第 0 个分区(2023-10-01)、第 0 个 bucket(Hudi 的哈希分桶)、第 1 个 task(Spark task 序号)。这个字段保证了在并发写入时,不同 task 生成的文件不会重名。

  • <commitTime>:20231001101522000,就是.commit文件里的 commitTime。它既是文件的版本号,也是时间旅行的查询依据。

理解这个命名规则,对排查问题至关重要。例如,当你发现某个分区下有大量xxx-0_0-0-1_xxx.parquet文件,但xxx-0_0-0-2_xxx.parquet却缺失,基本可以断定是 Spark task 2 在写入时失败了,而 Hudi 的 clean 机制还没来得及清理它。这时你需要检查20231001101522000.inflight是否还存在,如果存在,说明 commit 没完成,手动删除它并重试即可。

4. 深度解析 upsert 语义:为什么“更新”不是覆盖,而是“逻辑合并”

Hudi 最常被误解的功能,就是upsert。很多人以为upsert就是“有则更新,无则插入”,就像 MySQL 的INSERT ... ON DUPLICATE KEY UPDATE。但在 Hudi 的世界里,“更新”是一个更精细的、带时间维度的逻辑合并过程。它不修改已有文件,而是在新 commit 中生成新文件,并通过元数据关联新旧版本。这个设计,是 Hudi 支持 ACID 和时间旅行的基石。

4.1 构建第二次写入:模拟用户资料变更

我们来执行一次真正的upsert。先准备新数据:

CREATE OR REPLACE TEMP VIEW new_data AS SELECT * FROM VALUES ('user_001', 'Alice Smith', 29, '2023-10-01 11:00:00'), -- 更新:name 和 age 变了,ts 更新了 ('user_002', 'Bob Johnson', 35, '2023-10-01 11:05:00'), -- 更新:name 变了,age 不变,ts 更新了 ('user_004', 'Diana', 26, '2023-10-01 11:10:00') -- 新增:user_id 不存在 AS t(user_id, name, age, ts);

然后,执行 upsert:

INSERT INTO hudi_users_cow SELECT user_id, name, age, ts, substring(ts, 1, 10) AS `partition` FROM new_data;

注意,这里用的是INSERT INTO,而不是CREATE TABLE。因为表已经存在,我们只需追加数据。执行后,你会看到新的 commit 日志:

Commit Time: 20231001111533000 Total Files Inserted: 1 Total Files Updated: 2

Total Files Updated: 2这个数字很关键。它意味着 Hudi 找到了 2 个已有的 base file(对应user_001和user_002),并在新 commit 中为它们生成了新的 parquet 文件。而user_004是新增的,所以Total Files Inserted: 1。

4.2 验证 upsert 结果:对比两次快照,看清“逻辑合并”的本质

现在,我们用时间旅行功能,对比两个 commit 的快照:

-- 查看第一次 commit 的快照(20231001101522000) SELECT user_id, name, age, ts FROM hudi_users_cow VERSION AS OF '20231001101522000' ORDER BY user_id; -- 查看第二次 commit 的快照(20231001111533000) SELECT user_id, name, age, ts FROM hudi_users_cow VERSION AS OF '20231001111533000' ORDER BY user_id;

第一次快照结果:

user_idnameagets
user_001Alice282023-10-01 10:00:00
user_002Bob352023-10-01 10:05:00
user_003Charlie422023-10-01 10:10:00

第二次快照结果:

user_idnameagets
user_001Alice Smith292023-10-01 11:00:00
user_002Bob Johnson352023-10-01 11:05:00
user_003Charlie422023-10-01 10:10:00
user_004Diana262023-10-01 11:10:00

变化一目了然:user_001和user_002的name和ts更新了,age也按新数据更新(user_001从 28 变 29),user_003保持不变,user_004新增。但这只是表象。我们深入文件系统看看物理发生了什么。

进入/tmp/hudi_users_cow/2023-10-01/,你会发现:

  • 旧文件依然存在:0f0b1e2d3-...-0_0-0-1_20231001101522000.parquet
  • 新文件已生成:0f0b1e2d3-...-0_0-0-1_20231001111533000.parquet(fileId 相同,commitTime 不同)
  • 还有一个新文件:1a2b3c4d5-...-0_0-0-1_20231001111533000.parquet(对应user_004,fileId 全新)

这就是 Hudi 的“逻辑合并”:它没有去修改20231001101522000.parquet里的任何字节,而是生成了一个全新的文件,里面只包含user_001和user_002的新版本数据,以及user_003的旧版本数据(因为没被更新)。当 Spark 读取VERSION AS OF '20231001111533000'时,Hudi 的HoodieTableFileSystemView会自动选择每个fileId下 commitTime 最大的那个文件来读取,从而呈现出“更新后”的视图。

4.3preCombineField的真实作用:解决“同一批数据内冲突”的终极方案

preCombineField的重要性,在upsert场景下才真正凸显。假设我们这次写入的数据里,user_001出现了两次:

CREATE OR REPLACE TEMP VIEW conflict_data AS SELECT * FROM VALUES ('user_001', 'Alice Smith', 29, '2023-10-01 11:00:00'), ('user_001', 'Alice Brown', 29, '2023-10-01 11:01:00'), -- 同一 user_id,不同 ts ('user_002', 'Bob Johnson', 35, '2023-10-01 11:05:00') AS t(user_id, name, age, ts);

如果preCombineField没设置,或者设错了(比如设成age),Hudi 会怎么处理?答案是:随机选一条。因为它不知道哪条更新,只能按 Spark shuffle 后的顺序取第一条。这显然不可接受。

而当我们正确设置了'preCombineField' = 'ts',Hudi 会在写入前,对user_001的两条记录按ts字段排序,取ts值最大的那条(2023-10-01 11:01:00)作为最终写入的记录。这就是preCombineField的全部使命:在单次写入的 batch 内,解决主键冲突。

关键提醒:preCombineField只影响单次写入 batch 内的去重,它不影响跨 commit 的数据一致性。跨 commit 的“谁更新谁”,完全由 commitTime 的先后顺序决定。所以,永远不要指望preCombineField能解决“上游 Kafka 消息乱序”问题,那是 Flink 或 Kafka Connect 层该干的事。

5. 常见故障排查链路:从ClassNotFoundException到HoodieIOException的完整诊断树

在本地跑通第一个 Hudi 表只是起点。在真实项目中,你会遇到各种各样的报错,它们看起来五花八门,但根源往往集中在几个固定环节。下面这张排查链路图,是我过去三年帮十几个团队解决 Hudi 问题后总结出的“症状-原因-解法”映射,按发生概率从高到低排列。

5.1java.lang.ClassNotFoundException: org.apache.hudi.DataSourceWriteOptions

这是新手遇到的第一道墙,占比超过 60%。错误日志通常很长,但核心就这一行。它根本不是类路径问题,而是Spark 和 Hudi 的 Scala 版本不匹配。

完整诊断步骤:

  1. 运行$SPARK_HOME/bin/spark-shell --version,确认输出中Scala version是2.12.xx还是2.13.xx;
  2. 检查你下载的 Hudi jar 包名,是否严格匹配hudi-spark3.3-bundle_2.12-0.13.0.jar(注意_2.12);
  3. 进入$SPARK_HOME/jars/目录,运行ls -l | grep hudi,确认 jar 包确实在这里,且权限为-rw-r--r--;
  4. 如果以上都对,最后检查$SPARK_HOME/conf/spark-env.sh,确认没有设置SPARK_DIST_CLASSPATH覆盖了默认 classpath。

为什么不是--jars参数的问题?
因为--jars只会将 jar 加入 executor 的 classpath,而DataSourceWriteOptions是 Driver 端在 SQL 解析阶段就需要加载的类。Driver 的 classpath 只认$SPARK_HOME/jars/下的 jar。

5.2org.apache.hudi.exception.HoodieIOException: Failed to list statuses for path

这个错误通常出现在你把LOCATION指向了 HDFS 或 S3 路径,但本地 Spark 没配好对应文件系统的 client。但更多时候,它发生在本地开发时,LOCATION指向了一个不存在的父目录。

例如,你写了LOCATION '/tmp/hudi_users_cow',但/tmp目录本身没有写权限,或者/tmp是一个挂载的 NFS 目录,而 NFS server 暂时不可达。

快速验证法:
在 shell 中执行:

mkdir -p /tmp/hudi_users_cow && touch /tmp/hudi_users_cow/test.txt && rm /tmp/hudi_users_cow/test.txt

如果这三步有任何一步失败,HoodieIOException就必然出现。解决方案就是换一个你有完全控制权的路径,比如$HOME/hudi_test。

5.3org.apache.spark.sql.AnalysisException: The table hudi_users_cow does not support time travel

这个错误看似是 Hudi 功能没启用,实则是 Spark SQL 的VERSION AS OF语法没被正确识别。根源有两个:

  • Spark 版本太低:VERSION AS OF是 Spark 3.3+ 引入的语法。如果你用的是 Spark 3.2.x,必须改用 Hudi 自己的read方式:

    SELECT * FROM hudi_users_cow WHERE `_hoodie_commit_time` = '20231001101522000';

    (注意:_hoodie_commit_time是 Hudi 自动添加的隐藏列)

  • Hudi 表不是用USING HUDI创建的:如果你是用CREATE TABLE ... LOCATION '/path'创建的表,再MSCK REPAIR TABLE,Spark 无法识别其为 Hudi 表,VERSION AS OF就会失效。必须从USING HUDI开始。

5.4org.apache.hudi.exception.HoodieUpsertException: Cannot find any log files for fileId xxx

这个错误只在 MOR 表中出现,但值得提前了解。它表示 Hudi 在执行upsert时,试图读取某个 base file 对应的.log文件,但文件不存在。常见原因:

  • 你误删了.log文件;
  • hoodie.cleaner.policy=KEEP_LATEST_FILE_VERSIONS且hoodie.cleaner.fileversions.retained=1,导致旧的.log文件被清理了;
  • 写入过程中网络中断,.log文件只写了一半。

安全修复法:
不要手动恢复.log文件。直接执行一次compaction,让 Hudi 把所有.log文件合并回 base file,再重新写入。命令如下(在spark-shell中):

import org.apache.hudi.DataSourceWriteOptions spark.sql( s""" |INSERT INTO hudi_users_mor |SELECT * FROM hudi_users_mor |OPTIONS ( | 'hoodie.compact.inline' = 'true', | 'hoodie.compact.inline.max.delta.commits' = '1' |) |""".stripMargin )

最后分享一个血泪教训:某次上线前,运维同事为了“释放空间”,手动rm -rf了.hoodie/archived/目录。结果第二天,所有依赖时间旅行的报表都查不到历史数据了。Hudi 的归档机制是异步的,archived/里的 commit 文件,是

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询