目录
Hudi 基础功能
Hudi 简介
Hudi 功能
Hudi 的特性
Hudi 的 架构
Hudi 数据管理
Hudi 表数据结构
hoodie 文件
数据文件
数据存储概述
Metadata 元数据
Index 索引
索引策略
Data 数据
Hudi 核心点解析
基本概念
时间轴Timeline
文件管理
索引 Index
表的存储类型
数据计算模型
批式模型(Batch)
流式模型(Stream)
增量模型(Incremental)
查询类型(Query Type)
快照查询(Snapshot Queries)
增量查询(Incremental Queries)
读优化查询(Read Optimized Queries)
Hudi 支持表类型
写时复制表(COW)
读时合并表(MOR)
COW VS MOR
数据写操作类型
写流程(upsert)
写流程(Insert)
Apache Hudi 由 Uber 开发并开源,该项目在 2016 年开始开发,并于 2017 年开源,2019年 1 月进入 Apache 孵化器,且 2020 年 6 月称为 Apache 顶级项目,目前最新版本:0.10.1 版本。Hudi(Hadoop Upserts anD Incrementals缩写)是目前市面上流行的三大开源数据湖方案之一。Hudi 一开始支持 Spark 进行数据摄入(批量 Batch 和流式 Streaming),从 0.7.0 版本开始,逐渐与 Flink 整合,主要在于 Flink SQL 整合,还支持 Flink SQL CDC。
Hudi(Hadoop Upserts anD Incrementals缩写)是目前市面上流行的三大开源数据湖方案之一。
用于管理分布式文件系统 DFS 上大型分析数据集存储。
简单来说,Hudi 是一种针对分析型业务的、扫描优化的数据存储抽象,它能够使 DFS 数据集在分钟级的时延内支持变更,也支持下游系统对这个数据集的增量处理。
Hudi具有如下基本特性/能力:
Apache Hudi 使得用户能在 Hadoop 兼容的存储之上存储大量数据,同时它还提供两种原语,不仅可以批处理,还可以在数据湖上进行流处理。
Hudi 表的数据文件,可以使用操作系统的文件系统存储,也可以使用 HDFS 这种分布式的文件系统存储。为了后续分析性能和数据的可靠性,一般使用 HDFS 进行存储。以 HDFS 存储来看,一个 Hudi 表的存储文件分为两类。
Hudi 把随着时间流逝,对表的一系列 CRUD 操作叫做 Timeline,Timeline 中某一次的操作,叫做 Instant。Hudi 的核心是维护 **Timeline **在不同时间对表执行的所有操作,instant 这有助于提供表的即时视图,同时还有效地支持按到达顺序检索数据。Hudi Instant 由以下组件组成:
.hoodie 文件夹中存放对应操作的状态记录:
Hudi 真实的数据文件使用 Parquet 文件格式存储,其中包含一个 metadata 元数据文件和数据文件 parquet 列式存储。Hudi 为了实现数据的 CRUD,需要能够唯一标识一条记录,Hudi 将把数据集中的 唯一字段(record key ) + 数据所在分区 (partitionPath) 联合起来当做 数据的唯一键。
Hudi 数据集的 组织目录结构 与 Hive 表示非常相似,一份数据集对应这一个根目录。数据集被 打散为多个分区,分区字段以文件夹形式存在,该文件夹包含该分区的所有文件。
在根目录下,每个分区都有唯一的分区路径,每个分区数据存储在多个文件中。
每个文件都有唯一的 fileId 和生成文件的 commit 标识。如果发生更新操作时,多个文件共享相同的 fileId,但会有不同的 commit。
以时间轴(Timeline)的形式将数据集上的各项操作元数据维护起来,以支持数据集的瞬态视图,这部分元数据存储于根目录下的元数据目录。一共有三种类型的元数据:
Commits:一个单独的commit包含对数据集之上一批数据的一次原子写入操作的相关信息。我们用单调递增的时间戳来标识commits,标定的是一次写入操作的开始。
Cleans:用于清除数据集中不再被查询所用到的旧版本文件的后台活动。
Compactions:用于协调Hudi内部的数据结构差异的后台活动。例如,将更新操作由基于行存的日志文件归集到列存数据上
Hudi 维护着一个索引,以支持在记录 key 存在情况下,将新记录的 key 快速映射到对应的fileId。
Bloom filter:存储于数据文件页脚。默认选项,不依赖外部系统实现。数据和索引始终保持一致。
Apache HBase :可高效查找一小批 key。在索引标记期间,此选项可能快几秒钟。
工作负载 1:对事实表
许多公司将大量事务数据存储在 NoSQL 数据存储中。例如,拼车情况下的行程表、股票买卖、电子商务网站中的订单。这些表通常会随着对最新数据的随机更新而不断增长,而长尾更新会针对较旧的数据,这可能是由于交易在以后结算/数据更正所致。换句话说,大多数更新进入最新的分区,很少有更新进入较旧的分区。
对于这样的工作负载,BLOOM 索引表现良好,因为索引查找 将基于大小合适的布隆过滤器修剪大量数据文件。此外,如果可以构造键以使它们具有一定的顺序,则要比较的文件数量会通过范围修剪进一步减少。
Hudi 使用所有文件键范围构建一个区间树,并有效地过滤掉更新/删除记录中与任何键范围不匹配的文件。
为了有效地将传入的记录键与布隆过滤器进行比较,即最小数量的布隆过滤器读取和跨执行程序的统一工作分配,Hudi 利用输入记录的缓存并采用可以使用统计信息消除数据偏差的自定义分区器。有时,如果布隆过滤器误报率很高,它可能会增加混洗的数据量以执行查找。
Hudi 支持动态布隆过滤器(使用启用 hoodie.bloom.index.filter.type=DYNAMIC_V0),它根据存储在给定文件中的记录数调整其大小,以提供配置的误报率。
工作负载 2:对事件表
事件流无处不在。来自 Apache Kafka 或类似消息总线的事件通常是事实表大小的 10-100 倍,并且通常将 时间(事件的到达时间/处理时间)视为一等公民。
例如,**物联网事件流、点击流数据、广告印象 **等。插入和更新仅跨越最后几个分区,因为这些大多是仅附加数据。鉴于可以在端到端管道中的任何位置引入重复事件,因此在存储到数据湖之前进行重复数据删除是一项常见要求。
一般来说,这是一个非常具有挑战性的问题,需要以较低的成本解决。虽然,我们甚至可以使用键值存储来使用 HBASE 索引执行重复数据删除,但索引存储成本会随着事件的数量线性增长,因此可能会非常昂贵。
实际上,BLOOM 带有范围修剪的索引是这里的最佳解决方案。人们可以利用时间通常是一等公民这一事实并构造一个键,event_ts + event_id 例如插入的记录具有单调递增的键。即使在最新的表分区中,也可以通过修剪大量文件来产生巨大的回报。
工作负载 3:随机更新/删除维度表
这些类型的表格通常包含高维数据并保存参考数据,例如 用户资料、商家信息。这些是高保真表,其中更新通常很小,但也分布在许多分区和数据文件中,数据集从旧到新。通常,这些表也是未分区的,因为也没有对这些表进行分区的好方法。
如前所述,BLOOM 如果无法通过比较范围/过滤器来删除大量文件,则索引可能不会产生好处。在这样的随机写入工作负载中,更新最终会触及表中的大多数文件,因此布隆过滤器通常会根据一些传入的更新指示所有文件的真阳性。因此,我们最终会比较范围/过滤器,只是为了最终检查所有文件的传入更新。
SIMPLE 索引将更适合,因为它不进行任何基于预先修剪的操作,而是直接与每个数据文件中感兴趣的字段连接 。HBASE 如果操作开销是可接受的,并且可以为这些表提供更好的查找时间,则可以使用索引。
在使用全局索引时,用户还应该考虑设置 hoodie.bloom.index.update.partition.path=true或hoodie.simple.index.update.partition.path=true 处理分区路径值可能因更新而改变的情况,例如用户表按家乡分区;用户搬迁到不同的城市。这些表也是 Merge-On-Read 表类型的绝佳候选者。
Hudi 以两种不同的存储格式存储所有摄取的数据,用户可选择满足下列条件的任意数据格式:
Hudi 提供了Hudi 表的概念,这些表支持 CRUD 操作,可以利用现有的大数据集群比如 HDFS 做数据文件存储,然后使用 SparkSQL 或 Hive 等分析引擎进行数据分析查询。
Hudi 表的三个主要组件:
1) 有序的时间轴元数据,类似于数据库事务日志。
2) 分层布局的数据文件:实际写入表中的数据;
3)索引(多种实现方式):映射包含指定记录的数据集。
Hudi 核心:
Timeline 是 Hudi 用来管理提交(commit)的抽象,每个 commit 都绑定一个固定时间戳,分散到时间线上。
在 Timeline 上,每个 commit 被抽象为一个 HoodieInstant,一个 instant 记录了一次提交 (commit) 的行为、时间戳、和状态。
图中采用时间(小时)作为分区字段,从 10:00 开始陆续产生各种 commits,10:20 来了一条 9:00 的数据,该数据仍然可以落到 9:00 对应的分区,通过 timeline 直接消费 10:00 之后的增量更新(只消费有新 commits 的 group),那么这条延迟的数据仍然可以被消费到。
时间轴(Timeline)的实现类(位于hudi-common-xx.jar中),时间轴相关的实现类位于 org.apache.hudi.common.table.timeline 包下.
Hudi 将 DFS 上的数据集组织到基本路径(HoodieWriteConfig.BASEPATHPROP)下的目录结构中。
数据集分为多个分区(DataSourceOptions.PARTITIONPATHFIELDOPT_KEY),这些分区与Hive表非常相似,是包含该分区的数据文件的文件夹。
在每个分区内,文件被组织为文件组,由文件 id 充当唯一标识。每个文件组包含多个文件切片,其中每个切片包含在某个即时时间的提交/压缩生成的基本列文件(.parquet)以及一组日志文件(.log),该文件包含自生成基本文件以来对基本文件的插入/更新。
Hudi 的 base file (parquet 文件) 在 footer 的 meta 去记录了 record key 组成的 BloomFilter,用于在 file based index 的实现中实现高效率的 key contains 检测。
Hudi 的 log (avro 文件)是自己编码的,通过积攒数据 buffer 以 LogBlock 为单位写出,每个 LogBlock 包含 magic number、size、content、footer 等信息,用于数据读、校验和过滤。
- 全局索引:在全表的所有分区范围下强制要求键保持唯一,即确保对给定的键有且只有一个对应的记录。
- 非全局索引:仅在表的某一个分区内强制要求键保持唯一,它依靠写入器为同一个记录的更删提供一致的分区路径。
批式模型就是使用 MapReduce、Hive、Spark 等典型的批计算引擎,以小时任务或者天任务的形式来做数据计算。
流式模型,典型的就是使用 Flink 来进行实时的数据计算。
针对批式和流式的优缺点,Uber 提出了增量模型(Incremental Mode),相对批式来讲,更加实时;相对流式而言,更加经济。
增量模型,简单来讲,是以 mini batch 的形式来跑准实时任务。Hudi 在增量模型中支持了两个最重要的特性:
Hudi支持三种不同的查询表的方式:Snapshot Queries、Incremental Queries和Read Optimized Queries。
类型一:Snapshot Queries(快照查询)
类型二:Incremental Queries(增量查询)
类型三:Read Optimized Queries(读优化查询)
Hudi提供两类型表:写时复制(Copy on Write,COW)表和读时合并(Merge On Read,MOR)表。
Copy on Write 简称 COW,顾名思义,它是在数据写入的时候,复制一份原来的拷贝,在其基础上添加新数据。
正在读数据的请求,读取的是最近的完整副本,这类似Mysql 的MVCC的思想。
COW表主要使用列式文件格式(Parquet)存储数据,在写入数据过程中,执行同步合并,更新数据版本并重写数据文件,类似RDBMS中的B-Tree更新。
Merge On Read 简称MOR,新插入的数据存储在delta log 中,定期再将delta log合并进行parquet数据文件。
读取数据时,会将delta log跟老的数据文件做merge,得到完整的数据返回。下图演示了MOR的两种数据读写方式
MOR 表是 COW 表的升级版,它使用列式(parquet)与行式(avro)文件混合的方式存储数据。在更新记录时,类似NoSQL中的LSM-Tree更新。
对于写时复制(COW)和读时合并(MOR)writer来说,Hudi的WriteClient是相同的。
在 Hudi 数据湖框架中支持三种方式写入数据:UPSERT(插入更新)、INSERT(插入)和BULK INSERT(写排序)。
(1)Copy On Write类型表,UPSERT 写入流程
第一步、先对 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;
(2)Merge On Read类型表,UPSERT 写入流程
第一步、先对 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 一个新的
(1) Copy On Write类型表,INSERT 写入流程
第一步、先对 records 按照 record key 去重(可选);
第二步、不会创建 Index;
第三步、如果有小的 base file 文件,merge base file,生成新的 FileSlice + base file,否则直接写新的 FileSlice + base file;
(2) Merge On Read类型表,INSERT 写入流程
第一步、先对 records 按照 record key 去重(可选);
第二步、不会创建 Index;
第三步、如果 log file 可索引,并且有小的 FileSlice,尝试追加或写最新的 log file;如果 log file 不可索引,写一个新的 FileSlice + base file;