核心概念:Copy-on-Write vs Merge-on-Read、Timeline架构、FileGroup与FileSlice

好,咱们进入第二章。这一章要啃的,是Hudi最核心的几个概念。说实话,我当年刚接触Hudi时,被这些术语绕得有点晕——什么Copy-on-Write、Merge-on-Read,还有Timeline、FileGroup、FileSlice……感觉像是一堆新名词砸过来。

但后来我发现,搞懂这几个概念,Hudi的ACID事务管理你就理解了一半。咱们一个一个来拆。

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

Hudi提供了两种存储类型,说白了就是数据写入时的两种策略。它们决定了你的数据怎么存、怎么读、延迟多高。

2.1.1 Copy-on-Write(COW)

COW,写时复制。每次写入数据,都会生成一个全新的列式文件(Parquet),把旧文件替换掉。读的时候,直接读最新文件就行,不需要合并。

核心特点:

  • 写放大:每次写入都要重写整个文件
  • 读性能:读的时候只需要扫描一个文件,延迟低
  • 适合场景:写少读多,对查询延迟敏感

我记得有个实时报表项目,数据每小时更新一次,但查询频率很高。当时我选了COW,查询响应基本在秒级。嗯,这里要注意——如果写入太频繁,COW的写放大问题会很明显,磁盘IO扛不住。

2.1.2 Merge-on-Read(MOR)

MOR,读时合并。写入时先写到增量日志文件(Avro),后台再异步合并成列式文件。读的时候,需要把日志和列式文件合并起来。

核心特点:

  • 写性能:写入快,因为只追加日志
  • 读性能:读的时候需要合并,延迟相对高
  • 适合场景:写多读少,对写入延迟敏感

我曾经在IoT场景用过MOR——设备每秒上报数据,写入量巨大。用MOR后,写入延迟从秒级降到了毫秒级。但查询时,如果合并没做完,延迟会飙上去。这个坑我踩过,后来加了定时合并策略才解决。

对比维度 Copy-on-Write Merge-on-Read
写入方式 重写整个文件 追加日志 + 异步合并
写放大 高 低
读性能 优(直接读) 中(需合并)
写性能 中 优
存储格式 仅Parquet Parquet + Avro日志
适合场景 写少读多 写多读少

我的建议:如果拿不准选哪个,先看你的写入频率。写入间隔超过5分钟,COW基本够用;写入间隔小于1分钟,MOR更合适。当然,最终还是要压测验证。

2.2 Timeline架构:Hudi的事务日志

Timeline是Hudi的"黑匣子"。它记录了表上发生的所有操作——写入、提交、回滚、压缩……每个操作都有一个时间戳,按时间顺序排列。

我个人觉得,理解Timeline是理解Hudi事务的关键。为什么?因为Hudi的ACID就是靠Timeline来保证的。

2.2.1 Timeline的核心组成

  • Instant(瞬间):每个操作对应一个Instant,包含操作类型、时间戳、状态
  • 操作类型:COMMIT(提交)、COMPACT(压缩)、ROLLBACK(回滚)、CLEAN(清理)等
  • 状态机:每个Instant经历 REQUESTED → INFLIGHT → COMPLETED 三个阶段

举个例子。你执行一次写入,Hudi会先创建一个COMMIT类型的Instant,状态设为REQUESTED。写入过程中,状态变为INFLIGHT。写入成功,状态变为COMPLETED。如果写入失败,状态变为ROLLED_BACK。

注意:Timeline是Hudi事务的"证据链"。如果Timeline损坏,整个表的数据一致性就没了。我曾经遇到过磁盘故障导致Timeline元数据丢失,恢复起来非常麻烦。所以,一定要做好Timeline的备份。

2.2.2 Timeline的查询机制

Hudi查询时,会先读取Timeline,找到最新的COMPLETED Instant。然后根据这个Instant,确定要读取哪些文件。这就是Hudi能提供"快照读"的原因——它知道哪个时间点的数据是完整的。

你想想看,如果没有Timeline,你怎么知道哪些数据是已提交的?哪些是写到一半的?Timeline就是Hudi的"事务日志",保证你读到的数据要么全部提交,要么全部回滚。

2.3 FileGroup与FileSlice:数据的物理组织

这两个概念,说白了就是Hudi怎么在文件系统上组织数据。我刚开始学的时候,觉得FileGroup和FileSlice有点抽象。后来画了一张图,一下就明白了。

Hudi 数据组织:FileGroup 与 FileSlice FileGroup 1(分区:dt=2024-01-01) FileSlice 1.1 Commit: 202401010800 FileSlice 1.2 Commit: 202401011200 FileSlice 1.3 Commit: 202401011600 FileGroup 2(分区:dt=2024-01-02) FileSlice 2.1 Commit: 202401020800 FileSlice 2.2 Commit: 202401021200 说明: • FileGroup:一个分区内的一组文件,对应一个分区键值 • FileSlice:FileGroup内的一个版本,对应一次提交 • 每个FileSlice包含一个Base文件(Parquet)和零个或多个Log文件(Avro) • 箭头表示时间顺序,最新的FileSlice包含最新数据

2.3.1 FileGroup

FileGroup是Hudi数据组织的基本单元。每个分区下可以有多个FileGroup,每个FileGroup包含一个分区键的所有数据。比如,按日期分区,那么dt=2024-01-01这个分区下,可能有多个FileGroup。

为什么要有FileGroup?说白了就是为了并行。多个FileGroup可以分布在不同的节点上,写入和查询都能并行处理。

2.3.2 FileSlice

FileSlice是FileGroup内的一个版本。每次提交(COMMIT)都会生成一个新的FileSlice。一个FileSlice包含:

  • Base文件:一个Parquet文件,存储完整数据
  • Log文件(仅MOR):零个或多个Avro文件,存储增量数据

我举个例子。假设你有一个FileGroup,初始数据在FileSlice 1.1。你更新了10条记录,Hudi会生成FileSlice 1.2。对于COW,FileSlice 1.2包含一个全新的Parquet文件;对于MOR,FileSlice 1.2可能只包含一个Log文件。

避坑指南:我曾经遇到过一个场景——FileGroup数量太多,每个FileGroup只有几条数据。这会导致大量小文件,查询性能很差。后来我调整了Hudi的配置,控制了FileGroup的数量,性能才上来。建议根据数据量合理设置FileGroup数量,一般每个FileGroup在100MB-1GB之间比较合适。

2.4 三者如何协同工作

好了,三个核心概念都讲完了。它们是怎么配合的?我画个流程:

  1. 写入触发:应用发起写入请求,Hudi根据分区键找到对应的FileGroup
  2. 创建Instant:Hudi在Timeline上创建一个COMMIT Instant,状态设为REQUESTED
  3. 写入数据:根据存储类型(COW/MOR),写入Base文件或Log文件,生成新的FileSlice
  4. 提交完成:写入成功后,Instant状态变为COMPLETED
  5. 查询读取:查询时,读取Timeline找到最新COMPLETED Instant,然后读取对应的FileSlice

你想想看,这个过程保证了什么?保证了事务的原子性和一致性——要么全部写入成功,要么全部回滚。Timeline就是那个"裁判",FileGroup和FileSlice就是"运动员",COW/MOR就是"比赛规则"。

总结一下:

  • COW:写时复制,读快写慢,适合写少读多
  • MOR:读时合并,写快读慢,适合写多读少
  • Timeline:事务日志,记录所有操作,保证ACID
  • FileGroup:数据组织单元,支持并行
  • FileSlice:版本单元,对应一次提交