2. Hudi核心概念:存储类型、时间线与文件组织

这一章,我们来啃Hudi最核心的几个概念。说实话,刚接触Hudi的时候,我也被这些术语搞得有点晕——Copy-on-Write、Merge-on-Read、Timeline、FileGroup、FileSlice……但搞懂了它们,你才算真正入了Hudi的门。

我习惯把这三块内容比作Hudi的“骨架”:存储类型决定了数据怎么写、怎么读;时间线记录了数据发生了什么变化;文件组织则定义了数据在磁盘上怎么摆放。三者缺一不可。

Hudi 核心概念关系图 Hudi 表 存储类型 Timeline 时间线 文件组织 Copy-on-Write Merge-on-Read Instant(提交/完成/回滚…) FileGroup + FileSlice 三者关系 存储类型决定写入策略 → Timeline记录每次操作 → 文件组织定义物理布局

2.1 存储类型:Copy-on-Write vs Merge-on-Read

Hudi提供了两种存储类型,说白了就是两种不同的数据写入和读取策略。选哪个,取决于你的业务场景。

2.1.1 Copy-on-Write(COW)

COW的意思是“写时复制”。每次写入数据时,Hudi会把涉及的文件整个重写一遍。嗯,听起来有点笨重,但换来的是读取时极致的性能。

核心特点:

  • 写入时:读取原文件 → 合并新数据 → 写入新文件 → 原子替换
  • 读取时:直接读取最新版本的列式文件(Parquet),无需任何合并
  • 文件格式:仅Parquet

我在项目中遇到过这样一个场景:某金融客户需要做实时风控查询,要求单条记录查询延迟低于100ms。COW就是最佳选择——因为读取时不需要合并任何增量日志,直接扫Parquet文件就行。

适用场景:

  • 读多写少的分析型查询
  • 对查询延迟敏感的场景(如实时报表、仪表盘)
  • 数据量中等,写入频率不高

2.1.2 Merge-on-Read(MOR)

MOR的思路就完全不一样了。它把写入和读取分开优化:写入时只写增量日志(Avro格式),读取时再把基础文件和日志合并。

核心特点:

  • 写入时:追加写入Avro格式的增量日志文件(.log),速度极快
  • 读取时:需要合并基础Parquet文件 + 增量日志文件
  • 文件格式:基础文件Parquet + 日志文件Avro

你想想看,如果业务场景是IoT传感器数据——每秒成千上万条写入,但查询频率很低,MOR就特别合适。写入延迟极低,读取时虽然慢一点,但可以通过压缩(Compaction)来优化。

注意:MOR读取时如果不做压缩,随着日志文件增多,读取性能会逐渐下降。我建议根据业务情况设置合理的压缩策略,比如每写入10000条记录触发一次小压缩。

2.1.3 对比总结

对比维度 Copy-on-Write Merge-on-Read
写入放大 高(每次重写整个文件) 低(仅追加日志)
读取性能 优(直接读Parquet) 中(需合并日志)
写入延迟 较高 低
文件格式 仅Parquet Parquet + Avro
适合场景 读多写少、低延迟查询 写多读少、流式写入

2.2 Timeline时间线机制

Timeline是Hudi的“操作日志”。它记录了表上发生的每一次操作——提交、完成、回滚、压缩……说白了,就是Hudi的元数据核心。

我个人习惯把Timeline想象成一本账本:每一笔操作都是一个“Instant”(瞬间记录),按时间顺序排列。这样,任何时候你都能知道表处于什么状态。

2.2.1 Instant的组成

每个Instant包含三个要素:

  • 时间戳:精确到毫秒的唯一标识,比如 20250214103000000
  • 操作类型:COMMIT、COMPLETED、ROLLBACK、COMPACTION等
  • 状态:REQUESTED(请求中)、INFLIGHT(执行中)、COMPLETED(已完成)

常见的Instant类型:

  • COMMIT:一次写入操作的提交
  • COMPLETED:提交完成
  • ROLLBACK:回滚操作
  • COMPACTION:MOR表的压缩操作
  • CLEAN:清理旧版本文件
  • RESTORE:恢复到某个时间点

举个例子,一次写入操作的Timeline看起来是这样的:

20250214103000000.commit.requested
20250214103000000.commit.inflight
20250214103000000.commit.completed

嗯,这里要注意:如果写入过程中失败了,你会看到 inflight 状态一直挂着,没有对应的 completed。这时候就需要手动回滚或者触发自动回滚机制。

避坑指南:我曾经遇到过一个问题——写入任务挂了,但Timeline上还留着 inflight 的Instant,导致后续写入一直报错。解决方案是调用Hudi的 rollback API清理掉这个挂起的Instant。所以,监控Timeline的健康状态很重要。

2.2.2 Timeline的查询能力

Timeline最强大的地方在于——你可以基于它做时间旅行(Time Travel)查询。比如查询“今天上午10点的数据快照”,Hudi会通过Timeline找到那个时间点对应的文件版本。

-- 查询指定时间点的数据快照
SELECT * FROM hudi_table
TIMESTAMP AS OF '2025-02-14 10:00:00';

2.3 文件组织:FileGroup与FileSlice

理解了存储类型和时间线,我们再来看数据在磁盘上到底是怎么组织的。这里有两个核心概念:FileGroup 和 FileSlice。

2.3.1 FileGroup

FileGroup是一组文件的逻辑集合。你可以把它理解成Hudi表的一个“分区内的数据分片”。每个FileGroup都有一个唯一的FileGroup ID。

举个例子,假设一张Hudi表按日期分区,分区 dt=2025-02-14 下有3个FileGroup,每个FileGroup负责一部分数据。写入新数据时,Hudi会决定数据落到哪个FileGroup里。

FileGroup的特点:

  • 一个FileGroup包含一个基础文件(Base File)和多个日志文件(Log Files)
  • FileGroup内的数据是互斥的,不会重复
  • FileGroup的数量决定了写入的并行度

2.3.2 FileSlice

FileSlice是FileGroup内的一个“版本快照”。每个FileSlice包含:

  • 一个基础文件(Parquet格式)
  • 零个或多个日志文件(Avro格式,仅MOR模式有)

你想想看,每次提交(Commit)都会在FileGroup内生成一个新的FileSlice。这样,同一个FileGroup下就有多个FileSlice,对应不同的数据版本。

我画个图帮你理解:

FileGroup-001
├── FileSlice-1 (commit_time=100)
│   ├── base_file_100.parquet
│   └── (无日志文件,COW模式)
├── FileSlice-2 (commit_time=200)
│   ├── base_file_200.parquet
│   └── (无日志文件,COW模式)
└── FileSlice-3 (commit_time=300)
    ├── base_file_300.parquet
    └── (无日志文件,COW模式)

对于MOR模式,FileSlice会包含日志文件:

FileGroup-002
├── FileSlice-1 (commit_time=100)
│   ├── base_file_100.parquet
│   └── log_file_101.avro, log_file_102.avro
└── FileSlice-2 (commit_time=200)
    ├── base_file_200.parquet
    └── (无日志文件,刚压缩完)

注意:FileSlice的数量直接影响存储空间和查询性能。如果FileSlice太多(比如频繁写入导致大量小版本),建议开启Hudi的清理(Clean)策略,保留最近N个版本即可。

2.3.3 文件组织与查询的关系

当Hudi执行查询时,它会:

  1. 根据查询条件定位到具体分区
  2. 在分区内找到所有FileGroup
  3. 对每个FileGroup,找到最新的FileSlice
  4. 读取FileSlice中的基础文件和日志文件(如果需要合并)

这个过程听起来复杂,但Hudi内部做了大量优化。比如,它会跳过那些不包含查询数据的FileGroup,减少IO开销。

个人经验:我在调优Hudi查询性能时,最常做的一件事就是检查FileGroup的数量。如果FileGroup太多(比如超过1000个),查询计划阶段就会变慢。这时候我会调整 hoodie.parquet.small.file.limit 参数,让小文件自动合并,减少FileGroup数量。

2.4 小结

这一章我们聊了Hudi的三个核心概念:

  • 存储类型:COW适合读多写少,MOR适合写多读少
  • Timeline:记录了所有操作,支持时间旅行查询
  • 文件组织:FileGroup是数据分片,FileSlice是版本快照