2. Hudi核心概念:表格式、Copy-on-Write与Merge-on-Read、时间线、文件组与文件片
2.1 表格式:Hudi如何组织数据与元数据
Hudi本质上是一种表格式(Table Format),它构建在分布式文件系统(如HDFS、S3)之上,通过一组约定好的元数据文件来管理海量数据文件。与传统的Hive表(仅将分区目录映射为表)不同,Hudi在表目录中额外维护了一个.hoodie元数据目录,用于记录表的完整“操作历史”和“数据文件版本”。
一个Hudi表的基本目录结构如下:
user_events/
├── .hoodie/
│ ├── hoodie.properties # 表配置(表类型、主键、payload类等)
│ ├── timeline/ # 时间线目录(核心元数据)
│ │ ├── 20241101120000.commit
│ │ ├── 20241101120000.commit.requested
│ │ ├── 20241101120000.inflight
│ │ ├── 20241101130000.deltacommit
│ │ └── ...
│ └── filegroup/ # 文件组元数据(部分版本)
├── partition_path=2024/11/01/
│ ├── 8f2a1c5e-...-0_0-0-0_20241101120000.parquet
│ └── .8f2a1c5e-...-0_0-0-0_20241101120000.log.1_0-0-1
└── partition_path=2024/11/02/
└── ...
从存储角度看,Hudi表由数据文件(Parquet/ORC等列式文件)和日志文件(Avro格式的增量记录)组成;从逻辑角度看,Hudi通过时间线(Timeline)记录每一次写入、压缩、清理等操作,并通过文件组(File Group)与文件片(File Slice)将物理文件组织成可追溯、可版本化的数据集合。
2.2 Copy-on-Write 与 Merge-on-Read:两种表类型
Hudi根据数据写入时对文件的处理策略,将表分为两种类型:Copy-on-Write(COW)和Merge-on-Read(MOR)。这是Hudi最核心的存储模型选择,直接影响写入放大、读取延迟和实时性。
2.2.1 Copy-on-Write(写时复制)
COW表在每次写入时,直接读取目标文件组的最新文件片,将新数据与旧数据合并后重写为一个新的Parquet文件,并更新时间线。旧文件在提交完成后即被标记为无效(但物理删除由清理服务执行)。
- 写入路径:新数据 → 读取旧文件 → 合并 → 写新Parquet → 提交commit
- 优点:查询时只需读取列式文件,无日志合并开销,查询性能最佳;文件数量少,适合分析型工作负载。
- 缺点:每次写入都会重写整个文件组的最新文件片,写入放大明显;写入延迟较高,不适合频繁的小批量更新。
2.2.2 Merge-on-Read(读时合并)
MOR表采用基础文件(Base File)+ 日志文件(Log File)的组合。写入时新记录以追加方式写入Avro格式的日志文件,不直接重写Parquet;查询时需将基础文件与日志文件动态合并,或由压缩(Compaction)服务异步将日志合并生成新的基础文件。
- 写入路径:新数据 → 追加到.log文件 → 提交deltacommit(轻量级)
- 读取路径:读取Base Parquet + 读取Log → 合并记录 → 返回结果
- 优点:写入延迟低,适合高频增量写入;支持近实时查询(配合压缩策略可平衡读性能)。
- 缺点:查询时若日志文件过多,合并开销大;需要额外管理压缩策略。
| 对比维度 | Copy-on-Write (COW) | Merge-on-Read (MOR) |
|---|---|---|
| 数据文件格式 | 仅Parquet(列式) | Parquet(Base)+ Avro(Log) |
| 写入方式 | 合并重写整个文件片 | 追加写日志文件 |
| 提交类型 | commit(较重) | deltacommit(轻量) |
| 写入放大 | 高 | 低 |
| 查询性能 | 最优(无合并) | 受日志大小影响,需压缩 |
| 典型场景 | 批量ETL、分析查询为主 | 高频写入、近实时消费 |
2.3 时间线(Timeline):Hudi的事务日志
Hudi的时间线是表的中心元数据,记录了表从创建以来发生的所有操作。每个操作对应一个Instant(瞬间),包含三个状态:
- Requested(已请求):操作已提交请求,但尚未执行。
- Inflight(进行中):操作正在执行中。
- Completed(已完成):操作成功完成,数据已可见。
时间线中的操作类型主要包括:
- commit:COW表的写入提交。
- deltacommit:MOR表的写入提交(写日志文件)。
- compaction:MOR表的压缩操作,将日志合并到基础文件。
- clean:清理旧版本文件片,回收存储空间。
- rollback:回滚失败的写入。
- savepoint:设置恢复点,防止clean删除特定版本。
- replacecommit:用于分区替换或批量删除等操作。
时间线是Hudi实现多版本并发控制(MVCC)和时间旅行(Time Travel)的基础。通过时间线,Hudi可以快速定位某个时间点表的最新状态,也可以回溯历史版本的数据。
2.4 文件组与文件片:数据文件的版本化组织
Hudi将每个分区下的数据文件组织为文件组(File Group),每个文件组包含一系列文件片(File Slice)。文件组是Hudi数据更新的最小逻辑单元,同一文件组内的数据具有相同的主键范围。
文件片是文件组内的一次版本快照,由以下部分组成:
- 一个基础文件(Base File,Parquet格式),包含截至某个提交时刻的全部数据。
- 零个或多个日志文件(Log File,Avro格式),记录该基础文件之后的增量更新(仅MOR表)。
每次写入操作会为涉及的文件组生成一个新的文件片。例如,一个COW表文件组经历了三次提交,则会产生三个文件片,但只有最新文件片是活跃的,旧文件片会被清理服务异步删除。
文件组ID: fg-8f2a1c5e
├── 文件片 v1 (commit 20241101120000)
│ └── 8f2a1c5e_0-0-0_20241101120000.parquet
├── 文件片 v2 (commit 20241101130000)
│ └── 8f2a1c5e_0-0-0_20241101130000.parquet
└── 文件片 v3 (commit 20241101140000) ← 当前活跃
└── 8f2a1c5e_0-0-0_20241101140000.parquet
对于MOR表,一个文件片可能同时包含基础文件和多个日志文件:
文件组ID: fg-9b3d7e21
└── 文件片 v5 (deltacommit 20241101150000)
├── 9b3d7e21_0-0-0_20241101100000.parquet (基础文件)
├── .9b3d7e21_0-0-0_20241101120000.log.1_0-0-1
└── .9b3d7e21_0-0-0_20241101150000.log.2_0-0-1
文件组与文件片的设计带来了三个关键能力:
- 增量查询:通过时间线定位某个提交之后的文件片变化,即可读取增量数据。
- 版本回滚:指定时间点,读取该时刻对应的文件片版本。
- 高效更新:只需更新受影响的文件组,而非全表重写。
2.5 综合案例:一次更新操作的全链路
假设有一个MOR表user_events,分区dt=2024-11-01下已有1个文件组,包含1个基础文件(提交于10:00)和1个日志文件(提交于11:00)。现在12:00有一批新的更新事件到达,Hudi的处理流程如下:
- 步骤1:写入端根据主键哈希将新记录路由到对应的文件组(本例为
fg-9b3d7e21)。 - 步骤2:在时间线上创建
deltacommit.requested(12:00)。 - 步骤3:将新记录追加写入一个新的日志文件
.log.3_0-0-1,状态变为inflight。 - 步骤4:写入完成后,提交
deltacommit,状态变为completed。此时文件组的最新文件片包含:基础文件(10:00)+ 日志1(11:00)+ 日志2(12:00)。 - 步骤5:查询时,Hudi读取基础文件和所有日志文件,按主键合并后返回最新结果。
- 步骤6:后台压缩服务(若配置)在达到阈值后,将基础文件与日志合并生成新的基础文件,并更新时间线为
compaction完成。
下图展示了MOR表在一次更新和一次压缩后的文件片与时间线状态:
2.6 常见误区与复盘
误区1:认为COW表不会产生小文件。实际上,如果写入频率高且每次写入的数据量小,COW表会不断生成新的Parquet文件(每个文件组一个),同样会产生大量小文件。解决方式是合理设置文件大小参数(如target.file.size.bytes)和写入批大小。
误区2:MOR表的日志文件可以无限增长。如果不配置压缩策略,日志文件会持续膨胀,导致查询时需要合并大量日志,性能急剧下降。必须设置合理的压缩触发策略(如compaction.delta_commits或时间间隔)。
复盘总结:理解Hudi表格式的关键在于把握三条主线:
- 时间线是表的“操作日志”,记录所有变更的先后顺序和状态;
- 文件组/文件片是数据的“版本化存储结构”,决定了数据如何被更新和查询;
- COW与MOR是两种不同的“写入策略”,在写入延迟、查询性能和存储成本之间做出权衡。
在实际调优中,需要根据业务读写比例、数据新鲜度要求和集群资源,综合选择表类型并配置合理的文件大小、压缩策略和清理策略。