核心概念: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有点抽象。后来画了一张图,一下就明白了。
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 三者如何协同工作
好了,三个核心概念都讲完了。它们是怎么配合的?我画个流程:
- 写入触发:应用发起写入请求,Hudi根据分区键找到对应的FileGroup
- 创建Instant:Hudi在Timeline上创建一个COMMIT Instant,状态设为REQUESTED
- 写入数据:根据存储类型(COW/MOR),写入Base文件或Log文件,生成新的FileSlice
- 提交完成:写入成功后,Instant状态变为COMPLETED
- 查询读取:查询时,读取Timeline找到最新COMPLETED Instant,然后读取对应的FileSlice
你想想看,这个过程保证了什么?保证了事务的原子性和一致性——要么全部写入成功,要么全部回滚。Timeline就是那个"裁判",FileGroup和FileSlice就是"运动员",COW/MOR就是"比赛规则"。
总结一下:
- COW:写时复制,读快写慢,适合写少读多
- MOR:读时合并,写快读慢,适合写多读少
- Timeline:事务日志,记录所有操作,保证ACID
- FileGroup:数据组织单元,支持并行
- FileSlice:版本单元,对应一次提交