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 核心架构图 Hudi 核心架构图 写入请求 索引查找 文件组定位 写入决策 COW:合并写新文件 MOR:追加增量日志 提交事务 查询引擎 COW:直接读最新文件片 MOR:合并基础文件 + 日志

这张图展示了 Hudi 的核心流程:写入请求先经过索引查找,定位到目标文件组;然后根据表类型(COW/MOR)执行不同的写入策略;最后提交事务。查询时,引擎根据表类型选择读取方式。

总结一下

这一讲我们覆盖了 Hudi 的三个核心点:

  • 核心概念:表、分区、文件组、文件片、提交——这些是 Hudi 的「零件」
  • 表类型:COW 适合读多写少,MOR 适合写多读少。选型时考虑你的写入频率和查询延迟要求
  • 索引机制:Bloom Index 是默认选择,HBase Index 用于超大规模场景。索引选错了,写入性能会差一个数量级

说实话,Hudi 的入门门槛不高,但要把这些概念吃透,还是需要一些实践。我建议你拿个小数据集(比如 100 万条),分别用 COW 和 MOR 跑一遍,看看写入时间和查询时间的差异。亲手试过,印象才深。