2、Hudi 基础:数据湖与 Hudi 简介、表格式、核心特性
2.1 从“数据仓库”到“数据湖”再到“湖仓一体”
在理解 Hudi 之前,需要先厘清它解决的问题。传统数据仓库(如 Teradata、Snowflake)以结构化数据为中心,通过严格的 ETL 流程保证数据质量,但存储成本高、扩展性受限,且难以容纳非结构化数据。数据湖(如基于 Amazon S3 或 HDFS 的存储)则允许以原始格式存储任意类型的数据,成本低廉、扩展性强,但随之而来的是“数据沼泽”问题——缺乏事务保障、难以执行增量更新、文件级粒度导致数据一致性难以保证。
Hudi(Hadoop Upserts and Deletes Incrementally)正是诞生于这一矛盾之中。它不是一个存储引擎,而是一种表格式(Table Format),运行在数据湖存储之上,为数据湖赋予数据库级别的 ACID 事务、记录级更新/删除、时间旅行和增量消费能力。简单来说,Hudi 让数据湖上的文件从“一批静态快照”变成了“可变更、可追踪、可流式读取的数据集”。
2.2 Hudi 表格式的核心机制
Hudi 表格式的核心在于它如何组织和管理数据文件。与传统的 Hive 表(仅记录目录与分区)不同,Hudi 在表级别维护了一个时间轴(Timeline),用于记录对表的所有操作(commit、compaction、clean 等)。这个时间轴是 Hudi 实现 ACID、时间旅行和增量查询的基础。
在物理存储层面,Hudi 将表数据划分为两种文件类型:基础文件(Base File) 和 增量日志文件(Log File)。基础文件采用列式存储(Parquet),保存某一时刻的完整数据快照;日志文件采用行式存储(Avro),记录对基础文件的增量变更。这种设计使得 Hudi 能够在不重写整个表的情况下高效处理更新操作。
两种主要的表类型
| 特性 | Copy on Write(COW) | Merge on Read(MOR) |
|---|---|---|
| 数据存储 | 仅基础文件(Parquet) | 基础文件 + 日志文件(Avro) |
| 写入放大 | 高(每次更新重写整个文件组) | 低(更新追加到日志) |
| 读取性能 | 高(无需合并) | 低(需合并日志与基础文件) |
| 适用场景 | 读多写少、对查询延迟敏感 | 写多读少、允许一定查询延迟 |
COW 表在每次更新时都会重写包含被更新记录的基础文件,因此查询时无需合并任何日志文件,读取性能最佳。MOR 表则将更新先追加到日志文件,通过异步的 compaction 过程在后台合并,从而显著降低写入延迟,但读取时需要实时合并日志与基础文件。
2.3 Hudi 核心特性与操作流程
Hudi 的核心特性可以归纳为以下四个方面,它们共同构成了 Hudi 作为数据湖表格式的完整价值。
- 记录级更新与删除:支持对单条或多条记录进行精确的 UPDATE 和 DELETE,而非重写整个分区或表。这是 Hudi 区别于 Hive 等传统数据湖表的最大优势。
- ACID 事务保障:基于时间轴机制,Hudi 保证并发写入的隔离性。每个写入操作都会在时间轴上生成一个 commit 元数据,读取方只能看到已提交的数据快照。
- 时间旅行(Time Travel):通过时间轴上的 commit 时间点,可以查询表在过去任意时刻的状态,便于数据回溯、审计和 bug 修复。
- 增量查询(Incremental Query):支持从指定 commit 之后读取变更数据(包括新增和更新的记录),这是构建流式数据管道(如从 Hudi 表实时同步到下游)的关键能力。
read.option("as.of.instant", "20240101000000") 进行时间旅行查询,或通过 read.option("begin.instanttime", "20240101000000") 进行增量查询。这些操作都直接依赖 Hudi 时间轴上记录的元数据。
Hudi 写入操作的标准流程
// 以 Spark 为例,使用 DataFrame 写入 Hudi 表
inputDF.write()
.format("hudi")
.option("hoodie.table.name", "user_events")
.option("hoodie.datasource.write.recordkey.field", "event_id")
.option("hoodie.datasource.write.partitionpath.field", "dt")
.option("hoodie.datasource.write.precombine.field", "ts")
.option("hoodie.datasource.write.operation", "upsert") // upsert / insert / delete
.mode("Append")
.save("/path/to/hudi/table")
上述代码展示了 Hudi 写入的基本流程:指定表名、记录键(record key)、分区字段和预组合字段(precombine field,用于处理同一批次内重复记录)。写入操作类型可以是 upsert(更新已存在记录并插入新记录)、insert(仅插入)或 delete(按记录键删除)。Hudi 会根据记录键自动定位到对应的文件组,并执行相应的文件合并或日志追加操作。
2.4 典型应用场景与常见误区
场景一:实时数仓中的变更数据捕获(CDC)。 业务数据库(如 MySQL)通过 Debezium 等工具捕获 binlog 变更,写入 Kafka,再通过 Flink 或 Spark Streaming 将变更数据以 upsert 方式写入 Hudi 表。下游应用可以通过 Hudi 的增量查询实时获取变更流,实现近实时的数据同步和报表更新。
场景二:大规模数据集的合规性删除。 在 GDPR 等数据隐私法规要求下,需要从海量历史数据中精确删除某个用户的所有记录。使用 Hudi 的 delete 操作,只需指定该用户的 ID 作为记录键,Hudi 会自动定位并重写包含该记录的文件组,而无需扫描全表。
误区三:认为 COW 表一定优于 MOR 表。 实际上,如果写入负载很高(例如每秒数万次更新),COW 表会因频繁重写文件而导致严重的写入放大和延迟。此时 MOR 表通过日志追加的方式能显著提升写入吞吐,虽然牺牲了部分读取性能,但在写入密集型场景下是更合理的选择。
在实际架构设计中,建议根据数据访问模式进行选择:对于下游分析查询频繁、对查询延迟敏感的场景,优先考虑 COW;对于数据摄入量大、更新频繁且查询可以容忍秒级延迟的场景,MOR 更为合适。同时,Hudi 支持在表级别配置自动清理(clean)和压缩(compaction)策略,以平衡存储成本与查询性能。