1 Hudi简述
1.1 hudi内容介绍
Apache Hudi(Hadoop Upserts Delete and Incremental)是下一代流数据湖平台。它是由Uber开发并开源的Data Lakes解决方案。Hudi 用于管理的数据库层上构建具有增量数据管道的流式数据湖。针对湖引擎和常规批处理进行了优化。Hudi提供了表、事务、高效的upserts/delete、高级索引、流摄取服务、数据集群/压缩优化和并发,同时保持数据的开源文件格式。
Hudi是一种针对分析型业务的、扫描优化的数据存储抽象,它能够使DFS数据集在分钟甚至秒级的时延内支持变更,也支持下游系统对这个数据集的增量处理。
1.2 hudi特性
- 主键到文件级的索引,支持快速Upsert/Delete。
- 可流式摄入数据,内置CDC源和工具。支持增量拉取表变更以进行处理。
- 支持事务提交及回滚。
- 支持Spark、Presto、Hive、Flink。。。等引擎的SQL读写。
- 自动管理小文件,数据合并,清理。
2 Hudi核心概念
2.1 时间轴元数据
Hudi的核心是维护表上在不同的即时时间(instants)执行的所有操作的时间轴(timeline),这有助于提供表的即时视图,同时还有效地支持按到达顺序检索数据,一个instant由以下三个部分组成:
2.1.1 Instant action:在表上执行的操作类型
- COMMITS:一次commit表示将一批数据原子性地写入一个表。
- CLEANS:清除表中不再需要的旧版本文件的后台活动。
- DELTA_COMMIT:将一批数据原子性地写入一个MergeOnRead类型的表,其中部分或所有数据可以写入增量日志。
- COMPACTION:合并Hudi内部差异数据结构的后台活动,例如:将更新操作从基于行的log日志文件合并到列式存储的数据文件。在内部,COMPACTION体现为timeline上的特殊提交。
- ROLLBACK:表示当commit/delta_commit不成功时进行回滚,其会删除在写入过程中产生的部分文件。
- SAVEPOINT:对某一instant备份操作,后续可以回滚到该instant。
2.1.2 Instant time:时间戳
通常是一个时间戳(例如:20190117010349),它按照动作开始时间的顺序单调增加。
2.1.3 State:事件状态
- REQUESTED:表示某个action已经调度,但尚未执行。
- INFLIGHT:表示action当前正在执行。
- COMPLETED:表示timeline上的action已经完成。
2.2 文件结构
Hudi存储分为两个部分:
- 元数据:.hoodie目录对应着表的元数据信息,包括表的版本管理(Timeline)、归档目录(存放过时的instant也就是版本),一个instant记录了一次提交(commit)的行为、时间戳和状态,Hudi以时间轴的形式维护了在数据集上执行的所有操作的元数据;
- 数据区:和hive一样,以分区方式存放数据;分区里面存放着Base File(.parquet)和Log File(.log.*)
(1)Hudi将数据表组织成分布式文件系统基本路径(basepath)下的目录结构
(2)表被划分为多个分区,这些分区是包含该分区的数据文件的文件夹,类似于Hive表
(3)在每个分区中,文件被组织成文件组,由文件ID唯一标识
(4)每个文件组包含几个文件片(FileSlice)
(5)每个文件片包含:
- 一个基本文件(.parquet):在某个commit/compaction即时时间操作(instant time)生成的(MOR可能没有)
- 多个日志文件(.log.*),这些日志文件包含自生成基本文件以来对基本文件的插入/更新(COW没有)
(6)Hudi采用了多版本并发控制(MVCC)
- compaction操作:合并日志和基本文件以产生新的文件片
- clean操作:清除不使用的/旧的文件片,回收文件系统上的空间
(7)Hudi的base file(parquet 文件)在 footer 区的 元数据区记录了 record key 组成的 BloomFilter,用于在 file based index 的实现中实现高效率的 key contains 检测。
(8)Hudi 的 log (avro 文件)是自己编码的,通过积攒数据 buffer 以 LogBlock 为单位写出,每个 LogBlock 包含 magic number、size、content、footer 等信息,用于数据读、校验和过滤。
parquet文件结构:www.cnblogs.com/yangsy0915/…
2.3 索引
Hudi通过索引机制提供高效的upserts,具体是将给定的hoodie key(record key + partition path)与文件id(文件组)建立唯一映射。这种映射关系,数据第一次写入文件后保持不变,所以一个FileGroup包含了一批record的所有版本记录。Index用于区分消息是INSERT还是UPDATE
Hudi 为了消除不必要的读写,引入了索引的实现。在有了索引之后,更新的数据可以快速被定位到对应的 File Group。上图为例,白色是基本文件,黄色是更新数据,有了索引机制,可以做到:避免读取不需要的文件、避免更新不必要的文件、无需将更新数据与历史数据做分布式关联,只需要在 File Group 内做合并。
2.3.1 索引类型
| Index类型 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| Bloom Index | 默认配置,使用布隆过滤器来判断记录存在与否,也可选使用record key的范围裁剪需要的文件 | 小数据量下效率高,不依赖外部系统,数据和索引保持一致性 | 因假阳性问题,还需回溯原文件再查找一遍 |
| Simple Index | 把update/delete操作的新数据和老数据进行join | 实现最简单,无需额外的资源 | 性能比较差 |
| HBase Index | 把index存放在HBase里面。在插入 File Group定位阶段所有task向HBase发送 Batch Get 请求,获取 Record Key 的 Mapping 信息 | 对于小批次的keys,查询效率高 | 需要外部的系统,增加了运维压力 |
| Flink State-based Index | 在 0.8.0 版本中实现,使用了 Flink 的 state 作为底层的 index 存储,每个 records 在写入之前都会先计算目标 group ID。 | 不同于 BloomFilter Index,避免了每次重复的文件 index 查找 | 只能flink使用,多flink任务不能共享state |
| 自定义索引 | 实现HoodieIndex接口,实现自定义索引 |
字节使用hudi优化索引案例:zhuanlan.zhihu.com/p/580624904
2.3.2 全局索引与非全局索引
全局索引:全局索引在全表的所有分区范围下强制要求键的唯一性,也就是确保对给定的键有且只有一个对应的记录。全局索引提供了更强的保证,但是随着表增大,update/delete操作损失的性能越高,因此更适用于小表。
非全局索引:默认的索引实现,只能保证数据在分区的唯一性。非全局索引依靠写入器为同一个记录的update/delete提供一致的分区路径,同时大幅提高了效率,更适用于大表。
从index的维护成本和写入性能的角度考虑,维护一个global index的难度更大,对写入性能的影响也更大,所以需要non-global index。
HBase索引本质上是一个全局索引,bloom和simple index都有全局选项:
- hoodie.index.type=GLOBAL_BLOOM
- hoodie.index.type=GLOBAL_SIMPLE
2.4 表类型
2.4.1 cow(copy on write写时拷贝)
在COW表中,只有数据文件/基本文件(.parquet),没有增量日志文件(.log.*)。
对每一个新批次写入都将创建相应数据文件的新版本(新的FileSlice),新版本文件包括旧版本文件的记录以及来自传入批次的记录(全量最新)。
假设有 3 个文件组,其中包含如下数据文件。
进行一批新的写入,在索引后,发现这些记录与File group1 和File group 2 匹配,然后有新的插入,将为其创建一个新的文件组(File group4)。
因此data_file1 和 data_file2 都将创建更新的版本,data_file1 V2 是data_file1 V1 的内容与data_file1 中传入批次匹配记录的记录合并。
由于在写入期间进行合并,COW 会产生一些写入延迟。但是COW 的优势在于它的简单性,不需要其他表服务(如压缩),也相对容易调试。
2.4.2 mor(merge on read读时合并)
MOR表中,包含列存的基本文件(.parquet)和行存的增量日志文件(基于行的avro格式,.log.*)。
顾名思义,MOR表的合并成本在读取端。因此在写入期间不会合并或创建较新的数据文件版本。标记/索引完成后,对于具有要更新记录的现有数据文件,Hudi 创建增量日志文件并适当命名它们,以便它们都属于一个文件组。
读取端将实时合并基本文件及其各自的增量日志文件。每次的读取延迟都比较高(因为查询时进行合并),所以 Hudi 使用压缩机制来将数据文件和日志文件合并在一起并创建更新版本的数据文件。
可以选择内联或异步模式运行压缩。Hudi也提供了不同的压缩策略供用户选择,最常用的一种是基于提交的数量。例如可以将压缩的最大增量日志配置为 4。这意味着在进行 4 次增量写入后,将对数据文件进行压缩并创建更新版本的数据文件。压缩完成后,读取端只需要读取最新的数据文件,而不必关心旧版本文件。
MOR表的写入行为,依据index 的不同会有细微的差别:
- 对于 BloomFilter这种无法对 log file生成 index 的索引方案,对于 INSERT 消息仍然会写 base file (parquet format),只有 UPDATE 消息会 append log 文件(因为 base file 已经记录了该 UPDATE 消息的 FileGroup ID)。
- 对于可以对 log file生成 index 的索引方案,例如 Flink写入中基于 state 的索引,每次写入都是 log format,并且会不断追加和 roll over。
2.4.3 COW和MOR对比
| CopyOnWrite | MergeOnRead | |
|---|---|---|
| 数据延迟 | 高 | 低 |
| 查询延迟 | 低 | 高 |
| Update(I/O) 更新成本 | 高(重写整个Parquet文件) | 低(追加到增量日志) |
| Parquet文件大小 | 低(更新成本I/O高) | 较大(低更新成本) |
| 写入放大 | 大 | 低(取决于压缩策略) |
2.5查询类型
2.5.1 快照查询
快照查询,可以查询指定commit/delta_commit即时操作后表的最新快照。
在读时合并(MOR)表的情况下,它通过即时合并最新文件片的基本文件和增量文件来提供近实时表(几分钟)。
对于写时复制(COW),它可以替代现有的parquet表(或相同基本文件类型的表),同时提upsert/delete和其他写入方面的功能,可以理解为查询最新版本的Parquet数据文件。
下图是COW的快照查询:
2.5.2 增量查询
增量查询,可以查询指定commit/delta_commit操作以后新写入的数据。
2.5.3 读优化查询
读优化查询,可查看给定的commit/compact即时操作的表的最新快照。仅将最新文件片的基本/列文件暴露给查询。
下图是MOR表的快照查询与读优化查询的对比:
- 快照查询和读优化查询对比:
| Snapshot | Read Optimized | |
|---|---|---|
| 数据延迟 | 低 | 高 |
| 查询延迟 | 高(合并列式基础文件+行式增量日志文件) | 低(原始列式基础文件) |
2.7 写入流程
- 去重(选择):根据输入数据定义的键值对数据去重
- 索引查询:进行索引查询。匹配对应的file group
- 写入文件:索引定位后根据文件大小进行写入
- 更新索引:写入完成后,返回并更新索引信息
- 提交完成:原子式提交所有这些更改。 回调通知
- 清理(选择):如果需要,将会清理老版本file slice
- 压缩(选择):如果满足压缩策略,则会执行压缩操作,合并mor类型表的log file
- 归档:将旧的timeline移动到归档文件夹中