一、业务场景设定
假设有一张用户订单表,按天分区,记录用户的订单状态变更:
vbnet
表名: hudi_orders
Record Key: order_id
Precombine Key: update_ts
Partition Key: dt (订单日期)
目标文件大小: 128MB
模拟以下操作序列:
vbnet
t1: 批量写入 1000 条新订单 (Insert)
t2: 更新其中 200 条订单状态为"已支付" (Upsert)
t3: 再写入 500 条新订单 + 更新 100 条为"已发货" (Upsert)
t4: 触发 Compaction (仅 MOR)
t5: 触发 Clean
二、COW 表文件布局演进
1.初始写入 t1:Insert 1000 条记录
less
hudi_orders/
├── .hoodie/
│ ├── hoodie.properties
│ └── 20240101120000.commit ← t1 commit 完成标记
│ (instant: 20240101120000, action: commit)
│
└── dt=2024-01-01/
├── file_group_1/
│ └── fg001_0_20240101120000.parquet ← 600条记录, ~76MB
└── file_group_2/
└── fg002_0_20240101120000.parquet ← 400条记录, ~51MB
- 1000 条记录按目标文件大小拆分为 2 个 File Group
- 每个 File Group 有且仅有一个 Base File(Parquet)
- 文件名格式:
[fileId]_[writeToken]_[instantTime].parquet
2.第二次写入 t2:Upsert 更新 200 条记录状态
scss
hudi_orders/
├── .hoodie/
│ ├── 20240101120000.commit
│ └── 20240101130000.commit ← t2 commit
│
└── dt=2024-01-01/
├── file_group_1/
│ ├── fg001_0_20240101120000.parquet ← 旧版本 (600条)
│ └── fg001_0_20240101130000.parquet ← 新版本 (600条,含120条更新)
└── file_group_2/
├── fg002_0_20240101120000.parquet ← 旧版本 (400条)
└── fg002_0_20240101130000.parquet ← 新版本 (400条,含80条更新)
COW 核心行为:

3.第三次写入 t3:Insert 500 + Update 100
sql
hudi_orders/
├── .hoodie/
│ ├── 20240101120000.commit
│ ├── 20240101130000.commit
│ └── 20240101140000.commit ← t3 commit
│
└── dt=2024-01-01/
├── file_group_1/
│ ├── fg001_0_20240101120000.parquet ← 旧版本v1
│ ├── fg001_0_20240101130000.parquet ← 旧版本v2
│ └── fg001_0_20240101140000.parquet ← 最新版本 (600+300=900条)
│ (含60条update + 300条insert写入此FG)
├── file_group_2/
│ ├── fg002_0_20240101120000.parquet ← 旧版本v1
│ ├── fg002_0_20240101130000.parquet ← 旧版本v2
│ └── fg002_0_20240101140000.parquet ← 最新版本 (400+200=600条含40条update)
│
└── file_group_3/ ← 新建FileGroup (Insert溢出)
└── fg003_0_20240101140000.parquet ← 新文件 (0条旧数据, 纯新insert)
- Small File Handling 机制:500 条 Insert 优先填充未满的 fg001 和 fg002
- 当已有文件组都达到目标大小后,创建新的 fg003
- Update 的 100 条通过索引定位到对应 File Group,触发整文件重写
4.Clean 后 t5:清理旧版本
sql
hudi_orders/
├── .hoodie/
│ ├── 20240101120000.commit
│ ├── 20240101130000.commit
│ ├── 20240101140000.commit
│ └── 20240101150000.clean ← Clean action
│
└── dt=2024-01-01/
├── fg001_0_20240101140000.parquet ← 只保留最新版本
├── fg002_0_20240101140000.parquet ← 只保留最新版本
└── fg003_0_20240101140000.parquet ← 只有一个版本
Clean 规则:根据hoodie.cleaner.commits.retained(默认保留最近 10 个 commit 的文件版本),超出部分被清理。
三、MOR 表文件布局演进
1.初始写入 t1:Insert 1000 条(与 COW 相同)
arduino
hudi_orders_mor/
├── .hoodie/
│ ├── hoodie.properties
│ └── 20240101120000.deltacommit ← 注意是 deltacommit
│
└── dt=2024-01-01/
├── fg001_0_20240101120000.parquet ← Base File, 600条
└── fg002_0_20240101120000.parquet ← Base File, 400条
MOR 表的 Insert 首次写入也是生成 Parquet Base File(与 COW 相同),区别在于后续 Upsert。
2.第二次写入 t2:Upsert 更新 200 条
arduino
hudi_orders_mor/
├── .hoodie/
│ ├── 20240101120000.deltacommit
│ └── 20240101130000.deltacommit ← t2 deltacommit
│
└── dt=2024-01-01/
├── fg001_0_20240101120000.parquet ← Base File 不变!
├── .fg001_20240101130000.log.1_0-1-0 ← Log File (120条更新)
├── fg002_0_20240101120000.parquet ← Base File 不变!
└── .fg002_20240101130000.log.1_0-1-0 ← Log File (80条更新)
MOR 核心行为:

3.第三次写入 t3:Insert 500 + Update 100
sql
hudi_orders_mor/
├── .hoodie/
│ ├── 20240101120000.deltacommit
│ ├── 20240101130000.deltacommit
│ └── 20240101140000.deltacommit ← t3
│
└── dt=2024-01-01/
│
│── (FileGroup 1: fg001)
├── fg001_0_20240101120000.parquet ← 原始Base (600条)
├── .fg001_20240101130000.log.1_0-1-0 ← t2的log (120条update)
├── .fg001_20240101140000.log.1_0-2-0 ← t3的log (60条update + 200条insert)
│
│── (FileGroup 2: fg002)
├── fg002_0_20240101120000.parquet ← 原始Base (400条)
├── .fg002_20240101130000.log.1_0-1-0 ← t2的log (80条update)
├── .fg002_20240101140000.log.1_0-2-0 ← t3的log (40条update + 150条insert)
│
│── (FileGroup 3: fg003 - 新建)
└── .fg003_20240101140000.log.1_0-2-0 ← 纯insert的log (150条)
(注: 无Base File!)
- MOR 表的 Insert 可以直接写入 Log 文件(无需 Base File 先存在)
- 同一 File Group 的不同 commit 产生不同的 Log 文件
- 此时 fg001 的完整数据 = Base(600) + log_t2(120 update) + log_t3(60 update + 200 insert) = 实际 800 条有效记录
4. Compaction 执行 - t4
scss
hudi_orders_mor/
├── .hoodie/
│ ├── 20240101120000.deltacommit
│ ├── 20240101130000.deltacommit
│ ├── 20240101140000.deltacommit
│ ├── 20240101150000.compaction.requested ← 调度
│ ├── 20240101150000.compaction.inflight ← 执行中
│ └── 20240101150000.commit ← 完成(注意变为commit)
│
└── dt=2024-01-01/
│
│── (FileGroup 1: fg001) - Compaction后
├── fg001_0_20240101120000.parquet ← 旧Base (待Clean)
├── .fg001_20240101130000.log.1_0-1-0 ← 旧Log (待Clean)
├── .fg001_20240101140000.log.1_0-2-0 ← 旧Log (待Clean)
├── fg001_0_20240101150000.parquet ← 新Base! (800条完整数据)
│
│── (FileGroup 2: fg002) - Compaction后
├── fg002_0_20240101120000.parquet ← 旧Base (待Clean)
├── .fg002_20240101130000.log.1_0-1-0 ← 旧Log (待Clean)
├── .fg002_20240101140000.log.1_0-2-0 ← 旧Log (待Clean)
├── fg002_0_20240101150000.parquet ← 新Base! (570条完整数据)
│
│── (FileGroup 3: fg003) - Compaction后
├── .fg003_20240101140000.log.1_0-2-0 ← 旧Log (待Clean)
└── fg003_0_20240101150000.parquet ← 新Base! (150条)
5. Clean 后 t5:清理旧版本
ini
hudi_orders_mor/
├── .hoodie/
│ └── ... (所有timeline instants保留)
│
└── dt=2024-01-01/
├── fg001_0_20240101150000.parquet ← 干净的最新Base
├── fg002_0_20240101150000.parquet
└── fg003_0_20240101150000.parquet