Hudi原理学习
一、Hudi是什么
Hudi(Hadoop Upsert Delete and Incremental)是什么:围绕数据库内核构建的流式数据湖平台(Streaming Data Lake Platform)
简而言之,它是一个对计算和存储进行解耦的数据湖方案:
数据存储:底层的数据可以存储到hdfs、s3、azure、alluxio 等存储
计算引擎:可以使用spark/flink 计算引擎来消费 kafka、pulsar 等消息队列的数据
查询引擎:支持的查询引擎有trino、hive、impala、spark、presto 等
二、Hudi的四个核心概念
根据官网推荐的学习路线,需要熟悉以下四个核心概念
(1)时间线
官方文档:Hudi 的核心是维护在不同时间对表执行的所有操作,这有助于提供表的即时视图, 同时还有效地支持按到达顺序检索数据。
个人理解:对表的所有操作都带时间。
Hudi 即时由以下组件组成:
Instant action
:对表执行的操作类型Instant time
:即时时间通常是一个时间戳(例如:20190117010349),它按操作开始时间的顺序单调增加。state
:即时的当前状态
其中Instant action
:对表执行的操作类型,执行的关键操作包括:
COMMITS
- 提交表示将一批记录原子写入表中。CLEANS
- 后台活动,用于删除表中不再需要的旧版本文件。DELTA_COMMIT
- 增量提交是指将一批记录原子写入 MergeOnRead 类型表,其中部分/全部数据可以只写入增量日志。COMPACTION
- 用于协调 Hudi 中差异数据结构的后台活动,例如:将更新从基于行的日志文件移动到列格式。在内部,压缩表现为时间轴上的特殊提交ROLLBACK
- 指示提交/增量提交不成功并回滚,删除在此类写入期间生成的任何部分文件SAVEPOINT
- 将某些文件组标记为“已保存”,以便清理器不会删除它们。它有助于在发生灾难/数据恢复的情况下将表还原到时间线上的某个点。
任何给定的时刻都可以是 处于以下状态之一
REQUESTED
- 表示已计划操作,但尚未启动INFLIGHT
- 表示当前正在执行操作COMPLETED
- 表示在时间轴上完成操作
有两个时间上的概念:arrival time
-到达时间、event time
-事件发生时间,需要理解。
按照官网上的案例:
例如:预计9点要更新一批数据,但这批数据延迟到达,10点20才到。
数据仍然被存储到更旧时间段的文件夹里,也就是9点。
而这批数据的提交时间是10:20。
这样的处理方式保证了数据的完整性,和查询时的便捷性。
在时间线的帮助下,可以直接尝试获取10:00以后成功提交的所有新数据的增量,高效地使用已经更改的文件,而不需要扫描之前的时间段。
(2)文件布局
>表1
>……
>表n
>分区1
>……
>分区n
>文件组1(由文件 ID 唯一标识)
>……
>文件组n(由文件 ID 唯一标识)
>文件切片1
>……
>文件切片n
>1个基本文件 (.parquet)
>n个日志文件 (.log.*)(包含自生成基本文件以来对基本文件的插入/更新)
- 压缩:合并日志和基本文件以生成新的
文件切片
(.parquet) - 清理:删除未使用/较旧的
文件切片
(.parquet),以回收文件系统上的空间。
(3)表类型
Hudi支持两种表类型
表类型 | 支持的查询类型 | 对比 | 含义 |
---|---|---|---|
Copy On Write -写入时复制 | 快照查询 + 增量查询 | 数据延迟-高 查询延迟-低 更新成本 (I/O)-高 文件大小-小 写入放大-高 | 官方说明:完全使用柱状文件格式(如parquet)来存储数据。更新时只需在写的过程中进行同步合并,并重写文件。 个人理解:这种表类型的文件切片里只有一个基本文件(.parquet),每次更新就是重写这个基本文件。 |
Merge On Read -读取时合并 | 快照查询 + 增量查询 + 读取优化查询 | 数据延迟-低 查询延迟-高 更新成本 (I/O)-低 文件大小-大 写入放大-较低,取决于压缩策略 | 官方说明:使用列(如parquet)和行(如avro)文件格式的组合来存储数据。更新被记录到delta文件中,随后被压缩以同步或异步产生新版本的柱状文件。 个人理解:这种表类型的文件切片里包含一个基本文件(.parquet)和n个日志文件,每次更新会将更新写入日志文件中,执行定期压缩操作会根据日志文件生成新版本的基本文件(.parquet)。 |
(4)查询类型
Hudi 支持三种查询类型
- 快照查询:查询查看给定提交或压缩操作时表的最新快照。在读取表上合并的情况下,它通过合并公开近实时数据(几分钟) 动态最新文件片的基本文件和增量文件。对于写入表上的复制,它提供了现有镶木地板表的直接替代品,同时提供更新/删除和其他写入端功能。
- 增量查询:查询仅看到自给定提交/压缩以来写入表的新数据。这有效地提供了更改流以启用增量数据管道。
- 读取优化查询:查询查看截至给定提交/压缩操作的表的最新快照。仅公开最新文件切片中的基/列式文件,并保证 与非 Hudi 列式表相比,列式查询性能相同。
查询类型 | 查询的数据 | 含义 |
---|---|---|
Snapshot Queries -快照查询 | 实时数据 | 分为两种情况: 1. Copy On Write :读 parquet 文件2. Merge On Read :读 parquet + log 文件组合得到的最新数据 |
Incremental Queries -增量查询 | 增量数据 | 分为两种情况: 1. Copy On Write :查询parquet 文件中指定时间之后更新过的数据2. Merge On Read :查询指定时间后parquet 文件和log 文件组合得到的更新数据 |
Read Optimized Queries -读取优化查询 | 最近快照 | 分为两种情况: 1. Copy On Write :读 parquet 文件2. Merge On Read :读 parquet 文件 |
三、其它概念
(1)索引 Index
Hudi通过索引机制提供高效的Upsert操作,该机制会将一个RecordKey+PartitionPath组合的方式作为唯一标识映射到一个文件ID,而且,这个唯一标识和文件组/文件ID之间的映射自记录被写入文件组开始就不会再改变。
目前,Hudi 支持以下索引选项。
Bloom Index (default)
:采用由主键(record key)构建的布隆过滤器,还可以选择使用主键范围修剪候选文件。Simple Index
:针对从存储表中提取的键执行传入更新/删除记录的精益联接。HBase Index
:管理外部 Apache HBase 表中的索引映射。Bring your own implementation
:您可以扩展此公共 API 以实现自定义索引。
按照类型分,索引可以分为全局索引和非全局索引
Bloom Index (default)
和Simple Index
都有全局选项,HBase Index
则本质上就是一个全局索引。
-
全局索引:全局索引在表的所有分区中强制键的唯一性,即保证表中对于给定的记录键只存在一条记录。全局索引提供了更强的保证,但是更新/删除成本会随着表O(表的大小)的大小而增加,这对于较小的表来说仍然是可以接受的。
-
非全局索引:另一方面,默认索引实现仅在特定分区内强制执行此约束。可以想象,非全局索引依赖于写入器在更新/删除期间为给定的记录键提供相同的分区路径,但是可以提供更好的性能,因为索引查找操作变成O(更新/删除的记录数量),并且可以很好地扩展写入量。
(2)写操作流程
Hudi数据湖框架中支持三种方式写入数据:UPSERT
(插入更新)、INSERT
(插入)和BULK INSERT
(写排序)。
3.2.1 UPSERT
-
Copy On Write
第一步:先对 records 按照 record key 去重;
第二步:首先对这批数据创建索引 (HoodieKey => HoodieRecordLocation);通过索引区分哪些 records 是 update,哪些 records 是 insert(key 第一次写入);
第三步:对于 update 消息,会直接找到对应 key 所在的最新 FileSlice 的 base 文件,并做 merge 后写新的 base file (新的 FileSlice);
第四步:对于 insert 消息,会扫描当前 partition 的所有 SmallFile(小于一定大小的 base file),然后 merge 写新的 FileSlice;如果没有 SmallFile,直接写新的 FileGroup + FileSlice;
-
Merge On Read
第一步:先对 records 按照 record key 去重(可选)
第二步:首先对这批数据创建索引 (HoodieKey => HoodieRecordLocation);通过索引区分哪些 records 是 update,哪些 records 是 insert(key 第一次写入)
第三步:如果是 insert 消息,如果 log file 不可建索引(默认),会尝试 merge 分区内最小的 base file (不包含 log file 的 FileSlice),生成新的 FileSlice;如果没有 base file 就新写一个 FileGroup + FileSlice + base file;如果 log file 可建索引,尝试 append 小的 log file,如果没有就新写一个 FileGroup + FileSlice + base file
第四步:如果是 update 消息,写对应的 file group + file slice,直接 append 最新的 log file(如果碰巧是当前最小的小文件,会 merge base file,生成新的 file slice)log file 大小达到阈值会 roll over 一个新的
3.2.2 INSERT
-
Copy On Write
第一步:先对 records 按照 record key 去重(可选);
第二步:不会创建 Index;
第三步:如果有小的 base file 文件,merge base file,生成新的 FileSlice + base file,否则直接写新的 FileSlice + base file;
-
Merge On Read
第一步:先对 records 按照 record key 去重(可选);
第二步:不会创建 Index;
第三步:如果 log file 可索引,并且有小的 FileSlice,尝试追加或写最新的 log file;如果 log file 不可索引,写一个新的 FileSlice + base file;