Apache Hudi 深度解析(一):核心概念、表类型与索引机制
好,咱们进入 Hudi 的正题。说实话,Hudi 是我在数据湖项目里用得最多的组件之一。它不像某些框架那样「大而全」,但它在增量处理这个点上,做得非常扎实。今天这一讲,我们先把 Hudi 的骨架搭起来——核心概念、两种表类型、还有索引机制。这些东西搞明白了,后面你写代码、调优才不会懵。
Hudi 的核心概念
先聊几个最基础的概念。你想想看,Hudi 本质上是个什么?它就是个「带版本管理的文件系统」——每次写入,都会生成新的文件版本,同时保留历史数据。这跟 Git 有点像,但它是为大数据设计的。
核心概念速览:
- 表(Table):Hudi 管理的数据集合,对应 Hive 表或 Spark 表
- 分区(Partition):按时间、地域等字段划分的数据子集
- 文件组(File Group):每个分区内,由同一主键的数据组成的文件集合
- 文件片(File Slice):文件组内某个时间点的快照
- 提交(Commit):一次原子写入操作,记录变更的时间戳
我个人习惯把 Hudi 的表想象成一个「日志 + 快照」的组合体。每次写入,Hudi 都会记录一条提交日志,同时生成新的文件片。查询的时候,你可以选择读最新快照,也可以读某个时间点的增量数据。
这里有个关键点——主键。Hudi 要求每条记录必须有唯一主键,这是它做 upsert 的基础。我在项目中遇到过有人把主键设成自增 ID,结果数据重跑时 ID 冲突,搞得文件组膨胀得厉害。后来我建议用业务主键(比如订单号 + 行号),问题就解决了。
表类型:Copy on Write vs Merge on Read
这是 Hudi 最核心的设计决策。两种表类型,对应不同的读写场景。我直接说结论:读多写少用 COW,写多读少用 MOR。但实际选型没这么简单,咱们细看。
Copy on Write(COW)
COW 的逻辑很直白:每次写入,直接把数据写成一个新的 Parquet 文件,旧文件标记为删除。查询时,只读最新的文件片。
我的经验:COW 适合数据量不大、更新频率低的场景。比如每天跑一次的离线报表,用 COW 就很稳。但如果你每秒写入几千条更新,COW 会产生大量小文件,性能会崩。
COW 的写入流程大概是这样的:
1. 读取当前文件组的最新文件片
2. 将新数据与旧数据合并(按主键)
3. 写入新的 Parquet 文件
4. 提交事务,标记旧文件为过期
你看,每次写入都要读旧数据、写新文件。数据量大的时候,这个「读-写」开销很可观。我记得有个项目,COW 表每天增量 500 万条,每次写入要扫描 2 亿条历史数据,跑一次要 40 分钟。后来改成 MOR,写入时间降到了 5 分钟。
Merge on Read(MOR)
MOR 的思路是「写时懒,读时勤」。写入时,只写增量日志(Avro 格式),不合并历史数据。查询时,再把日志和基础文件合并。
MOR 的写入流程:
1. 将新数据写入增量日志文件(.log)
2. 更新索引,记录变更位置
3. 提交事务(轻量级)
查询时,MOR 有两种模式:
- 读优化(Read Optimized):只读基础 Parquet 文件,忽略增量日志。速度快,但数据可能不是最新的。
- 实时(Snapshot):合并基础文件 + 增量日志,返回最新数据。速度慢,但数据准。
避坑指南:我曾经在 MOR 表上跑实时查询,结果发现查询延迟从 2 秒涨到了 30 秒。排查后发现,增量日志文件积累太多,合并开销巨大。解决方案是定期执行 compaction(压缩),把日志合并到基础文件里。建议每天至少做一次 compaction。
COW vs MOR 对比
| 维度 | Copy on Write | Merge on Read |
|---|---|---|
| 写入速度 | 慢(需要合并) | 快(只写日志) |
| 查询速度 | 快(直接读文件) | 慢(需要合并日志) |
| 数据新鲜度 | 写入即最新 | 读优化模式有延迟 |
| 存储开销 | 高(每次写入全量文件) | 低(增量日志小) |
| 适用场景 | 低频更新、读密集型 | 高频更新、写密集型 |
嗯,这里要注意:MOR 的「快写慢读」特性,在实时数仓场景下特别有用。比如你接的是 CDC 数据(数据库变更日志),每秒几千条变更,用 MOR 写入几乎无压力。查询时,如果业务能接受秒级延迟,用读优化模式就够了。
索引机制
索引是 Hudi 的「大脑」。没有索引,Hudi 就不知道一条记录该去哪个文件组找。索引的作用很简单:给定主键,快速定位文件组 ID。
Hudi 支持多种索引类型,我挑几个常用的讲讲。
Bloom Index(布隆过滤器索引)
这是默认索引。每个文件组维护一个布隆过滤器,记录该文件组包含的所有主键。查询时,先检查布隆过滤器,如果「可能包含」,再去实际文件里查。
布隆过滤器的特点是:空间小、速度快,但有误判率。它可能会说「这个主键存在」,但实际上不存在。不过没关系,Hudi 会做二次校验,不会返回错误数据。
调优建议:布隆过滤器的误判率默认是 0.0001(万分之一)。如果你的主键数量特别大(比如 10 亿级),建议把误判率调到 0.00001,虽然会多占点内存,但能减少不必要的文件读取。
HBase Index
把索引存在 HBase 里。主键 -> 文件组 ID 的映射关系,直接查 HBase 就行。优点是查询快、支持高并发;缺点是要额外维护 HBase 集群。
我在项目中用过 HBase 索引,场景是每天 10 亿条更新的实时流。布隆过滤器扛不住,HBase 索引稳稳的。但运维成本确实高,HBase 集群挂了,Hudi 写入就停了。
Simple Index
最简单的索引:遍历所有文件组,逐个检查主键是否存在。适合小表(百万级),大表千万别用。
索引选择建议
| 数据规模 | 推荐索引 | 理由 |
|---|---|---|
| 百万级 | Simple Index | 实现简单,遍历开销可接受 |
| 千万级 | Bloom Index | 空间小,速度快,误判率可控 |
| 亿级 | Bloom Index(调优) | 降低误判率,配合分区裁剪 |
| 十亿级 | HBase Index | 高并发、低延迟,但运维成本高 |
Hudi 核心架构图
下面这张图,把 Hudi 的核心组件串起来了。你仔细看,从写入到查询,数据是怎么流转的。
这张图展示了 Hudi 的核心流程:写入请求先经过索引查找,定位到目标文件组;然后根据表类型(COW/MOR)执行不同的写入策略;最后提交事务。查询时,引擎根据表类型选择读取方式。
总结一下
这一讲我们覆盖了 Hudi 的三个核心点:
- 核心概念:表、分区、文件组、文件片、提交——这些是 Hudi 的「零件」
- 表类型:COW 适合读多写少,MOR 适合写多读少。选型时考虑你的写入频率和查询延迟要求
- 索引机制:Bloom Index 是默认选择,HBase Index 用于超大规模场景。索引选错了,写入性能会差一个数量级
说实话,Hudi 的入门门槛不高,但要把这些概念吃透,还是需要一些实践。我建议你拿个小数据集(比如 100 万条),分别用 COW 和 MOR 跑一遍,看看写入时间和查询时间的差异。亲手试过,印象才深。