实时分为处理的实时和数据的实时,即席分析是要求对数据实时的处理,马上要得到对应的结果,Flink、Spark Streaming是用来对实时数据的实时处理,数据要求实时,处理也要迅速,数据不实时,处理也不及时的场景则是我们的数仓T+1数据
而本文探讨的Apache Hudi,对应的场景是数据的实时,而非处理的实时。它旨在将Mysql中的时候以近实时的方式映射到大数据平台,比如Hive中。
数据调度:T+1,慢,实时化
数据同步:T+1,merge,数据量大,周期长
修复回刷:修复好后数据回刷,数据量大
传统的离线数仓,通常数据是T+1的,不能满足对当日数据分析的需求,而流式计算一般是基于窗口,并且窗口逻辑相对比较固定。
现有一类特殊的需求,业务分析比较熟悉现有事务数据库的数据结构,并且希望有很多即席分析,这些分析包含当日比较实时的数据。惯常他们是基于Mysql从库,直接通过Sql做相应的分析计算。但很多时候会遇到如下障碍
因此,一些弥合在OLTP和OLAP之间的技术框架出现,典型有TiDB。它能同时支持OLTP和OLAP。而诸如Apache Hudi和Apache Kudu则相当于现有OLTP和OLAP技术的桥梁。他们能够以现有OLTP中的数据结构存储数据,支持CRUD,同时提供跟现有OLAP框架的整合(如Hive,Impala),以实现OLAP分析
首先来看Hudi的架构体系,通过Spark/Flink将上游数据同步到数据湖的Raw Tables中,并可对Raw Tables进行增删改查,与内部需求及痛点匹配度非常高。
Apache Hudi
Apache Hudi 在基于 HDFS 数据存储之上,提供了两种流原语:
一般来说,我们会将大量数据存储到HDFS,新数据增量写入,而旧数据鲜有改动,特别是在经过数据清洗,放入数据仓库的场景。而且在数据仓库如 hive中,对于update的支持非常有限,计算昂贵。另一方面,若是有仅对某段时间内新增数据进行分析的场景,则hive、presto、hbase等也未提供原生方式,而是需要根据时间戳进行过滤分析。
在此需求下,Hudi可以提供这两种需求的实现。第一个是对record级别的更新,另一个是仅对增量数据的查询。且Hudi提供了对Hive、presto、Spark的支持,可以直接使用这些组件对Hudi管理的数据进行查询。
这两种原语分别是:
设计原则
Hudi提供了以下功能来对基础数据进行写入、查询,这使其成为大型数据湖的重要模块:
在深入研究 COW 和 MOR 之前,让我们先了解一下 Hudi 中使用的一些术语,以便更好地理解以下部分。
Hudi将数据以列存格式(Parquet/ORC)存放,称为数据文件/基础文件,该列出格式是非常高效的并在整个行业中广泛使用,数据文件和基本文件通常可以互换使用,但两者的含义相同。
在 MOR 表格式中,更新被写入到增量日志文件中,该文件以 avro 格式存储。这些增量日志文件始终与基本文件相关联。假设有一个名为 data_file_1 的数据文件,对 data_file_1 中记录的任何更新都将写入到新的增量日志文件。在服务读取查询时,Hudi 将实时合并基础文件及其相应的增量日志文件中的记录。
通常根据存储的数据量,可能会有很多数据文件。每个数据文件及其对应的增量日志文件形成一个文件组。在 COW 的情况下,它要简单得多,因为只有基本文件。
我们以 COW 格式表为例来解释文件版本。每当数据文件发生更新时,将创建数据文件的较新版本,其中包含来自较旧数据文件和较新传入记录的合并记录。
对于每个文件组,可能有不同的文件版本。因此文件切片由特定版本的数据文件及其增量日志文件组成。对于 COW,最新的文件切片是指所有文件组的最新数据/基础文件。对于 MOR,最新文件切片是指所有文件组的最新数据/基础文件及其关联的增量日志文件。
写时复制(copy on write):仅使用列式文件(parquet)存储数据。在写入/更新数据时,直接同步合并原文件,生成新版本的基文件(需要重写整个列数据文件,即使只有一个字节的新数据被提交)。此存储类型下,写入数据非常昂贵,而读取的成本没有增加,所以适合频繁读的工作负载,因为数据集的最新版本在列式文件中始终可用,以进行高效的查询。
COW,他是在数据写入的时候,复制一份原来的拷贝,在其基础上添加新数据。正在读数据的请求,读取的是近的完整副本,这类似Mysql 的MVCC的思想。
对 Hudi 的每一个新批次写入都将创建相应数据文件的新版本,新版本文件包括旧版本文件的记录以及来自传入批次的记录。
假设我们有 3 个文件组,其中包含如下数据文件。
我们进行一批新的写入,在索引后,我们发现这些记录与File group 1 和File group 2 匹配,然后有新的插入,我们将为其创建一个新的文件组(File group 4)。
因此data_file1 和 data_file2 都将创建更新的版本,数据文件 1 V2 是数据文件 1 V1 的内容与数据文件 1 中传入批次匹配记录的记录合并。
由于在写入期间进行合并,COW 会产生一些写入延迟。但是COW 的优势在于它的简单性,不需要其他表服务(如压缩),也相对容易调试。
简称MOR。新插入的数据存储在delta log 中。定期再将delta log合并进行parquet数据文件。读取数据时,会将delta log跟老的数据文件做merge,得到完整的数据返回。当然,MOR表也可以像COW表一样,忽略delta log,只读取最近的完整数据文件。
MOR表写数据时,记录首先会被快速的写进日志文件,稍后会使用时间轴上的压缩操作将其与基础文件合并。根据查询是读取日志中的合并快照流还是变更流,还是仅读取未合并的基础文件,MOR表支持多种查询类型。在高层次上,MOR writer在读取数据时会经历与COW writer 相同的阶段。这些更新将追加到最新文件篇的最新日志文件中,而不会合并。
读时合并(merge on read):使用列式(parquet)与行式(avro)文件组合,进行数据存储。在更新记录时,更新到增量文件中(avro),然后进行异步(或同步)的compaction,创建列式文件(parquet)的新版本。此存储类型适合频繁写的工作负载,因为新记录是以appending 的模式写入增量文件中。但是在读取数据集时,需要将增量文件与旧文件进行合并,生成列式文件。
顾名思义,合并成本从写入端转移到读取端。因此在写入期间我们不会合并或创建较新的数据文件版本。标记/索引完成后,对于具有要更新记录的现有数据文件,Hudi 创建增量日志文件并适当命名它们,以便它们都属于一个文件组。
读取端将实时合并基本文件及其各自的增量日志文件。你可能会想到这种方式,每次的读取延迟都比较高(因为查询时进行合并),所 以 Hudi 使用压缩机制来将数据文件和日志文件合并在一起并创建更新版本的数据文件。
用户可以选择内联或异步模式运行压缩。Hudi也提供了不同的压缩策略供用户选择,最常用的一种是基于提交的数量。例如您可以将压缩的最大增量日志配置为 4。这意味着在进行 4 次增量写入后,将对数据文件进行压缩并创建更新版本的数据文件。压缩完成后,读取端只需要读取最新的数据文件,而不必关心旧版本文件。
5.1 写入延迟
正如我们之前所讨论,由于写入期间发生同步合并,与 MOR 相比COW 具有更高的写入延迟。
5.2 读取延迟
由于我们在 MOR 中进行实时合并,因此与 COW 相比MOR 往往具有更高的读取延迟。但是如果根据需求配置了合适的压缩策略,MOR 可以很好地发挥作用。
5.3 更新代价
由于我们为每批写入创建更新的数据文件,因此 COW 的 I/O 成本将更高。由于更新进入增量日志文件,MOR 的 I/O 成本非常低。
5.4 写放大
同样当我们创建更新版本的数据文件时,COW 会更高。假设您有一个大小为 100Mb 的数据文件,并且每次更新 10% 的记录进行 4 批写入,4 次写入后,Hudi 将拥有 5 个大小为 100Mb 的 COW 数据文件。你可以配置你的清理器(将在后面的博客中讨论)清理旧版本文件,但如果没有进行清理,最终会有 5 个版本的数据文件,总大小约500Mb。MOR 的情况并非如此,由于更新进入日志文件,写入放大保持在最低限度。对于上面的例子,假设压缩还没有开始,在 4 次写入后,我们将有 1x100Mb 的文件和 4 个增量日志文件(10Mb) 的大小约140Mb。
hudi维护了一个时间轴,记录了在不同时刻对数据集进行的所有操作。
hudi拥有2种存储优化。
读优化(Copy On Write):在每次commit后都将最新的数据compaction成列式存储(parquet);
写优化(Merge On Read):对增量数据使用行式存储(avro),后台定期将它compaction成列式存储。
hudi维护着一个索引,以支持在记录key存在情况下,将新记录的key快速映射到对应的fileId。索引的实现是插件式的,默认是bloomFilter,也可以使用HBase。
hudi提供3种查询视图。
读优化视图:仅提供compaction后的列式存储的数据;
增量视图:仅提供一次compaction/commit前的增量数据;
实时视图:包括读优化的列式存储数据和写优化的行式存储数据。
hudi写数据的时候需要指定PRECOMBINE_FIELD_OPT_KEY、RECORDKEY_FIELD_OPT_KEY和PARTITIONPATH_FIELD_OPT_KEY。
RECORDKEY_FIELD_OPT_KEY:每条记录的唯一id,支持多个字段;
PRECOMBINE_FIELD_OPT_KEY:在数据合并的时候使用到,当 RECORDKEY_FIELD_OPT_KEY 相同时,默认取 PRECOMBINE_FIELD_OPT_KEY 属性配置的字段最大值所对应的行;
PARTITIONPATH_FIELD_OPT_KEY:用于存放数据的分区字段。
hudi更新数据和插入数据很相似(写法几乎一样),更新数据时,会根据 RECORDKEY_FIELD_OPT_KEY、PRECOMBINE_FIELD_OPT_KEY 以及 PARTITIONPATH_FIELD_OPT_KEY三个字段对数据进行Merge。
Hudi表的数据文件,可以使用操作系统的文件系统存储,也可以使用HDFS这种分布式的文件系统存储。为了后续分析性能和数据的可靠性,一般使用HDFS进行存储。以HDFS存储来看,一个Hudi表的存储文件分为两类。
_partition_key
相关的路径是实际的数据文件,按分区存储,当然分区的路径key是可以指定的,我这里使用的是_partition_keyHudi真实的数据文件使用Parquet文件格式存储
.hoodie文件
Hudi把随着时间流逝,对表的一系列CRUD操作叫做Timeline。Timeline中某一次的操作,叫做Instant。Instant包含以下信息
.hoodie文件夹中存放对应操作的状态记录
Hudi还对存储在Hudi数据集中的数据执行几个关键的存储管理功能。在DFS上存储数据的关键方面是管理文件大小和数量以及回收存储空间。
例如,HDFS在处理小文件上性能很差,这会对Name Node的内存及RPC施加很大的压力,并可能破坏整个集群的稳定性。
通常,查询引擎可在较大的列文件上提供更好的性能,因为它们可以有效地摊销获得列统计信息等的成本。
即使在某些云数据存储上,列出具有大量小文件的目录也常常比较慢。
以下是一些有效管理Hudi数据集存储的方法。
Apache Kudu,需要单独部署集群。而Apache Hudi则不需要,它可以利用现有的大数据集群比如HDFS做数据文件存储,然后通过Hive做数据分析,相对来说更适合资源受限的环境。
Apache Kudu是一个与Hudi具有相似目标的存储系统,该系统通过对upserts
支持来对PB级数据进行实时分析。 一个关键的区别是Kudu还试图充当OLTP工作负载的数据存储,而Hudi并不希望这样做。 因此,Kudu不支持增量拉取(Incremental Pulling)(截至2017年初),而Hudi支持以便进行增量处理。
Kudu与分布式文件系统抽象和HDFS完全不同,Kudu剥离了HDFS(Hadoop Distribute File System)及其分布式文件系统抽象接口,它自己的一组存储服务器通过RAFT相互通信,通过RAFT一致性算法管理自己的一组存储服务器。 与之不同的是,Hudi旨在与底层Hadoop兼容的文件系统(HDFS,S3或Ceph)一起使用,并且没有自己的存储服务器群,而是依靠Apache Spark来完成繁重的工作。 因此,Hudi可以像其他Spark作业一样轻松扩展,而Kudu则需要硬件和运营支持,特别是HBase或Vertica等数据存储系统。 到目前为止,我们还没有做任何直接的基准测试来比较Kudu和Hudi(鉴于RTTable正在进行中)。 但是,如果我们要使用CERN, 我们预期Hudi在摄取parquet上有更卓越的性能。
截止目前,尚没有一份官方的基准测试可以全面地评估两者的性能。
kudu
kudu的存储机制和hudi的写优化方式有些相似。kudu的最新数据保存在内存,称为MemRowSet(行式存储,基于primary key有序),当MemRowSet写满(默认1G或者120s)后flush到磁盘,形成DiskRowSet(列式存储)。
DiskRowSet包含baseData与DeltaStores两部分,DeltaStores包含一个DeltMemStore和多个DeltaFile,后续的更新数据存放在DeltMemStore中,增长到一定程度后flush成DeltaFile文件。
kudu会定期执行compaction操作,将DeltaFile中的更新合并到DiskRowSet,或者合并DiskRowSet,清除已删除的数据,并减少DiskRowSet的数量。
hudi
hudi维护了一个时间轴,记录了在不同时刻对数据集进行的所有操作。
hudi拥有2种存储优化,读优化适合读多写少的场景,写优化适合读少写多的场景。
读数据
kudu
先根据要扫描数据的主键范围,定位到目标的tablets,然后读取tablets中的DiskRowSet。
在读取每个DiskRowSet时,先根据主键过滤要scan的范围,然后加载范围内的baseData,再找到对应的DeltaStores,
应用所有变更,最后union上MemRowSet中的内容,最后返回数据给client。
kudu提供range分区和hash分区两种分区方式,通过多种索引(主键范围索引、bloomfilter、主键索引),支持随机读取数据和高效的批量读取数据。
hudi
hudi也维护着一个索引,以此将key快速映射到对应的fileId。索引的实现是插件式的,默认是bloomFilter,也可以使用HBase。
hudi提供3种查询视图。
更新数据
kudu
client向master发出请求,通过索引定位到具体的tablet,然后根据元数据连接tablet对应的tserver。
若数据在磁盘(DiskRowSet)上,则将更新信息写入DeltMemStore中,若数据在内存(MemRowSet)中,则将信息写入所在行的mutation链表中。
hudi
hudi没有传统意义的更新,只有append和重写。
hudi写数据的时候需要指定以下3个key。
hudi更新数据和插入数据很相似(写法几乎一样),更新数据时,会根据以上三个字段对数据进行Merge。
kudu
不同于hudi和delta lake是作为数据湖的存储方案,kudu设计的初衷是作为hive和hbase的折中,因此它同时具有随机读写和批量分析的特性。
kudu允许对不同列使用单独的编码和压缩格式,拥有强大的索引支持,搭配range分区和hash分区的合理划分,
对分区查看、扩容和数据高可用性的支持都非常好,适用于既有随机访问,也有批量数据扫描的复合场景。
kudu可以和impala、spark集成,支持sql操作,除此之外,kudu能够充分发挥高性能存储设备的优势。
相比较其他两者,kudu不支持云存储,也不支持版本回滚和增量处理。
hudi
hudi的产生背景是为了解决Uber的增量更新问题,它提供了丰富的视图和存储优化方式,
可以适配批量访问场景,增量处理场景,以及近实时查询场景,无论是读多写少还是读少写多,hudi都能提供对应的优化方案,
用户可以根据自身场景灵活选择合适的配置。
三者之中,hudi的兼容性最好,它原生支持spark、presto、hive和mapreduce等大数据生态系统,并且读写底层文件实现了自己的InputFormat,更容易与其它系统做兼容。
hudi目前还不支持通过sql操作数据,(19年12月)社区已经将其作为下一步的方向,2121版本支持sql。
hudi不存在锁机制,因此不支持多客户端同时写一张表,这是需要注意的一点。
Hive事务/ACID是另一项类似的工作,它试图实现在ORC文件格式之上的存储读取时合并
(merge-on-read
)的存储功能。 可以理解,此功能与Hive以及LLAP之类的其他工作紧密相关。 与Hudi相比,Hive Transactions不不支持读时优化(Read-Optimized)存储和增量拉取(Incremental Pulling)。 在实现选择方面,Hudi充分利用了类似Spark的处理框架的功能,而Hive事务特性则在用户或Hive Metastore启动的Hive任务/查询的下实现。根据Uber工程师的实际生产经验,与其他方法相比,将Hudi作为一个三方依赖库嵌入到现有的Spark管道中要容易得多,并且操作不会太繁琐。 Hudi还设计用于与Presto/Spark等非Hive引擎合作,并计划引入除parquet以外的文件格式。
虽然HBase是面向OLTP场景的键值存储(key-value store),典型的应用场景就是不断插入新的记录且不怎么修改。但由于本身运行于HDFS之上,用户往往倾向于在HBase做一些分析相关的业务。鉴于HBase经过大量写入优化,它支持开箱即用的亚秒级upsert,而Hive-on-HBase则允许用户查询该数据。但就分析类业务场景的实际性能而言,由于这类场景负载主要在读取上,像Parquet/ORC这样的混合列式存储格式轻松击败HBase,因为这些工作负载主要是读取繁重的工作。Hudi弥补了更快的数据与分析存储格式之间的差距。 Hudi打破了数据快速入库和基于该数据进行分析业务之间的壁障。从可操作性上来说,相比于Hbase需要管理一个含有大量Region Server的集群来满足分析性业务场景,而Hudi主需要一个三方依赖库就可以实现,可维护性和可扩展性更强,为用户提供可更快给出数据的库更具可扩展性。最后,和Hudi相比,HBase不支持增量处理原语,如commit times
,incremental pull
。提交时间
、增量拉取
之类的增量处理原语。
一个普遍的问题:”Hudi与流处理系统有何关系?”,我们将在这里尝试回答。简而言之,Hudi可以与当今的批处理(写时复制存储
)和流处理(读时合并存储
)作业集成,以将计算结果存储在Hadoop中。 对于Spark应用程序,这可以通过将Hudi库与Spark/Spark Steaming的DAG直接集成来实现。在非Spark处理系统(例如Flink、Hive)情况下,可以在相应的系统中进行处理,然后通过Kafka Topics /HDFS中间文件将其发送到Hudi表中。从概念上讲,数据处理 管道仅由三个部分组成:source
, processing
和sink,输入
,处理
,输出
,用户最终针对输出运行查询以便使用管道的结果。Hudi可以充当将数据存储在DFS上的输入source
或输出sink
,前者读取存储在HDFS上的Hudi表,后者将数据写人存储于HDFS的Hudi表。
流式处理保存的Hudi表,最终交给Presto/Spark SQL/Hive做查询。