路径窄处留一步与人行;
滋味浓处减三分让人尝。
—《菜根谭》
Apache Hudi代表Hadoop Upserts anD Incrementals,管理大型分析数据集在HDFS上的存储。
Hudi的主要目的是高效减少摄取过程中的数据延迟。由Uber开发并开源,HDFS上的分析数据集通过两种类型的表提供服务:
读优化表(Read Optimized Table),通过列式存储提供查询性能
近实时表(Near-Real-Time Table),提供实时(基于行的存储和列式存储的组合)查询。
Hudi是一个开源Spark库,用于在Hadoop上执行诸如更新,插入和删除之类的操作。它还允许用户仅摄取更改的数据,从而提高查询效率。它可以像任何作业一样进一步水平扩展,并将数据集直接存储在HDFS上。
Hudi针对HDFS上的数据集提供以下原语
Hudi维护在数据集上执行的所有操作的时间轴(timeline),以提供数据集的即时视图。Hudi将数据集组织到与Hive表非常相似的基本路径下的目录结构中。数据集分为多个分区,文件夹包含该分区的文件。每个分区均由相对于基本路径的分区路径唯一标识。
分区记录会被分配到多个文件。每个文件都有一个唯一的文件ID和生成该文件的提交(commit)。如果有更新,则多个文件共享相同的文件ID,但写入时的提交(commit)不同。
存储类型–处理数据的存储方式
视图–处理数据的读取方式
读取优化视图-输入格式仅选择压缩的列式文件
近实时视图
增量视图
Hudi存储层由三个不同的部分组成
元数据–它以时间轴的形式维护了在数据集上执行的所有操作的元数据,该时间轴允许将数据集的即时视图存储在基本路径的元数据目录下。
时间轴上的操作类型包括
Hudi解决了以下限制
HDFS的可伸缩性限制
需要在Hadoop中更快地呈现数据
没有直接支持对现有数据的更新和删除
快速的ETL和建模
要检索所有更新的记录,无论这些更新是添加到最近日期分区的新记录还是对旧数据的更新,Hudi都允许用户使用最后一个检查点时间戳。此过程不用执行扫描整个源表的查询
4.1 下载Hudi
$ mvn clean install -DskipTests -DskipITs
$ mvn clean install -DskipTests -DskipITs -Dhive11
4.2 版本兼容性
Hadoop Hive Spark 构建命令
Apache Hadoop-2.8.4 Apache Hive-2.3.3 spark-2.[1-3].x spark-2.[1-3].x mvn clean install -DskipTests
Apache Hadoop-2.7.3 Apache Hive-1.2.1 spark-2.[1-3].x spark-2.[1-3].x mvn clean install -DskipTests
4.3 生成Hudi数据集
设置环境变量
export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64/jre/
export HIVE_HOME=/var/hadoop/setup/apache-hive-1.1.0-cdh5.7.2-bin
export HADOOP_HOME=/var/hadoop/setup/hadoop-2.6.0-cdh5.7.2
export HADOOP_INSTALL=/var/hadoop/setup/hadoop-2.6.0-cdh5.7.2
export HADOOP_CONF_DIR=$HADOOP_INSTALL/etc/hadoop
export SPARK_HOME=/var/hadoop/setup/spark-2.3.1-bin-hadoop2.7
export SPARK_INSTALL=$SPARK_HOME
export SPARK_CONF_DIR=$SPARK_HOME/conf
export PATH=$JAVA_HOME/bin:$HIVE_HOME/bin:$HADOOP_HOME/bin:$SPARK_INSTALL/bin:$PATH
4.4 Api支持
使用DataSource API,只需几行代码即可快速开始读取或写入Hudi数据集及使用RDD API操作Hudi数据集。
使用一种新的HoodieRecordPayload类型,并保留以前的持久类型作为CombineAndGetUpdateValue(…)的输出。否则前一次提交的提交时间一直更新到最新,会使得下游增量ETL将此记录计数两次。
左连接(left join)包含所有通过键保留的数据的数据框(data frame),并插入persisted_data.key为空的记录。但不确定是否充分利用了BloomIndex/metadata。
添加一个新的标志字段至从HoodieRecordPayload元数据读取的HoodieRecord中,以表明在写入过程中是否需要复制旧记录。
在数据框(data frame)选项中传递一个标志位以强制整个作业会复制旧记录。
HDFS中的可伸缩性限制。
Hadoop中数据的快速呈现
支持对于现有数据的更新和删除
快速的ETL和建模
6.1 时间轴
在它的核心,Hudi维护一条包含在不同的即时时间所有对数据集操作的时间轴,从而提供,从不同时间点出发得到不同的视图下的数据集。Hudi即时包含以下组件
操作类型 : 对数据集执行的操作类型
即时时间 : 即时时间通常是一个时间戳(例如:20190117010349),该时间戳按操作开始时间的顺序单调增加。
状态 : 即时的状态
Hudi保证在时间轴上执行的操作的原子性和基于即时时间的时间轴一致性。
执行的关键操作包括
COMMITS - 一次提交表示将一组记录原子写入到数据集中。
CLEANS - 删除数据集中不再需要的旧文件版本的后台活动。
DELTA_COMMIT - 增量提交是指将一批记录原子写入到MergeOnRead存储类型的数据集中,其中一些/所有数据都可以只写到增量日志中。
COMPACTION - 协调Hudi中差异数据结构的后台活动,例如:将更新从基于行的日志文件变成列格式。在内部,压缩表现为时间轴上的特殊提交。
ROLLBACK - 表示提交/增量提交不成功且已回滚,删除在写入过程中产生的所有部分文件。
SAVEPOINT - 将某些文件组标记为"已保存",以便清理程序不会将其删除。在发生灾难/数据恢复的情况下,它有助于将数据集还原到时间轴上的某个点。
任何给定的即时都可以处于以下状态之一
REQUESTED - 表示已调度但尚未启动的操作。
INFLIGHT - 表示当前正在执行该操作。
COMPLETED - 表示在时间轴上完成了该操作。
上面的示例显示了在Hudi数据集上大约10:00到10:20之间发生的更新事件,大约每5分钟一次,将提交元数据以及其他后台清理/压缩保留在Hudi时间轴上。
观察的关键点是:提交时间指示数据的到达时间(上午10:20),而实际数据组织则反映了实际时间或事件时间,即数据所反映的(从07:00开始的每小时时段)。在权衡数据延迟和完整性时,这是两个关键概念。
如果有延迟到达的数据(事件时间为9:00的数据在10:20达到,延迟 >1 小时),我们可以看到upsert将新数据生成到更旧的时间段/文件夹中。
在时间轴的帮助下,增量查询可以只提取10:00以后成功提交的新数据,并非常高效地只消费更改过的文件,且无需扫描更大的文件范围,例如07:00后的所有时间段。
6.2 文件组织
Hudi将DFS上的数据集组织到基本路径下的目录结构中。数据集分为多个分区,这些分区是包含该分区的数据文件的文件夹,这与Hive表非常相似。
每个分区被相对于基本路径的特定分区路径区分开来。
在每个分区内,文件被组织为文件组,由文件id唯一标识。
每个文件组包含多个文件切片,其中每个切片包含在某个提交/压缩即时时间生成的基本列文件(*.parquet)以及一组日志文件(*.log*),该文件包含自生成基本文件以来对基本文件的插入/更新。
Hudi采用MVCC设计,其中压缩操作将日志和基本文件合并以产生新的文件片,而清理操作则将未使用的/较旧的文件片删除以回收DFS上的空间。
Hudi通过索引机制将给定的hoodie键(记录键+分区路径)映射到文件组,从而提供了高效的Upsert。
一旦将记录的第一个版本写入文件,记录键和文件组/文件id之间的映射就永远不会改变。简而言之,映射的文件组包含一组记录的所有版本。
6.3 写时复制存储
写时复制存储中的文件片仅包含基本/列文件,【仅使用列文件格式(例如parquet)】
并且每次提交都会生成新版本的基本文件。
换句话说,我们压缩每个提交,从而所有的数据都是以列数据的形式储存。在这种情况下,写入数据非常昂贵(我们需要重写整个列数据文件,即使只有一个字节的新数据被提交),而读取数据的成本则没有增加。
这种视图有利于读取繁重的分析工作。
以下内容说明了将数据写入写时复制存储并在其上运行两个查询时,它是如何工作的。
随着数据的写入,对现有文件组的更新将为该文件组生成一个带有提交即时时间标记的新切片,而插入分配一个新文件组并写入该文件组的第一个切片。
这些文件切片及其提交即时时间在上面用颜色编码。
针对这样的数据集运行SQL查询(例如:select count(*)统计该分区中的记录数目),首先检查时间轴上的最新提交并过滤每个文件组中除最新文件片以外的所有文件片。
如您所见,旧查询不会看到以粉红色标记的当前进行中的提交的文件,但是在该提交后的新查询会获取新数据。因此,查询不受任何写入失败/部分写入的影响,仅运行在已提交数据上。
写时复制存储的目的是从根本上改善当前管理数据集的方式,通过以下方法来实现
优先支持在文件级原子更新数据,而无需重写整个表/分区
能够只读取更新的部分,而不是进行低效的扫描或搜索
严格控制文件大小来保持出色的查询性能(小的文件会严重损害查询性能)。
6.4 读时合并存储
支持:读优化视图、增量视图、实时视图
读时合并存储是写时复制的升级版,从某种意义上说,它仍然可以通过读优化表提供数据集的读取优化视图(写时复制的功能)。
此外,它将每个文件组的更新插入存储到基于行的增量日志中,通过文件id,将增量日志和最新版本的基本文件进行合并,从而提供近实时的数据查询。因此,此存储类型智能地平衡了读和写的成本,以提供近乎实时的查询。
这里最重要的一点是压缩器,它现在可以仔细挑选需要压缩到其列式基础文件中的增量日志(根据增量日志的文件大小),以保持查询性能(较大的增量日志将会提升近实时的查询时间,并同时需要更长的合并时间)。
以下内容说明了存储的工作方式,并显示了对近实时表和读优化表的查询。
此示例中发生了很多有趣的事情,这些带出了该方法的微妙之处。
现在,我们每1分钟左右就有一次提交,这是其他存储类型无法做到的。
现在,在每个文件id组中,都有一个增量日志,其中包含对基础列文件中记录的更新。
在示例中,增量日志包含10:05至10:10的所有数据。与以前一样,基本列式文件仍使用提交进行版本控制。
因此,如果只看一眼基本文件,那么存储布局看起来就像是写时复制表的副本。
定期压缩过程会从增量日志中合并这些更改,并生成基础文件的新版本,就像示例中10:05发生的情况一样。
有两种查询同一存储的方式:读优化(RO)表和近实时(RT)表,具体取决于我们选择查询性能还是数据新鲜度。
对于RO表来说,提交数据在何时可用于查询将有些许不同。请注意,以10:10运行的(在RO表上的)此类查询将不会看到10:05之后的数据,而在RT表上的查询总会看到最新的数据。
何时触发压缩以及压缩什么是解决这些难题的关键。
通过实施压缩策略,在该策略中,与较旧的分区相比,我们会积极地压缩最新的分区,从而确保RO表能够以一致的方式看到几分钟内发布的数据。
读时合并存储上的目的是直接在DFS上启用近实时处理,而不是将数据复制到专用系统,后者可能无法处理大数据量。
该存储还有一些其他方面的好处,例如通过避免数据的同步合并来减少写放大,即批量数据中每1字节数据需要的写入数据量。
Apache Kudu与Hudi非常相似;Apache Kudu用于对PB级数据进行实时分析,也支持插入更新。
Apache Kudu和Hudi之间的主要区别在于Kudu试图充当OLTP(在线事务处理)工作负载的数据存储,而Hudi却不支持,它仅支持OLAP(在线分析处理)。
Apache Kudu不支持增量拉取,但Hudi支持增量拉取。
还有其他主要的主要区别,Hudi完全基于Hadoop兼容的文件系统,例如HDFS,S3或Ceph,而Hudi也没有自己的存储服务器,Apache Kudu的存储服务器通过RAFT进行相互通信。
对于繁重的工作流,Hudi依赖于Apache Spark,因此可以像其他Spark作业一样轻松地扩展Hudi。
Hudi填补了在HDFS上处理数据的巨大空白,因此可以与一些大数据技术很好地共存。
Hudi最好用于在HDFS之上对parquet格式数据执行插入/更新操作。
对于非Spark处理系统(例如:Flink,Hive),处理过程可以在各自的系统中完成,然后以Kafka Topics 或者HDFS中间文件的形式发送到Hudi表中。从相对抽象的维度上来说,数据处理管道只包含三个组件:source, processing和sink,用户最终面向sink运行查询以使用管道的结果。Hudi可以作为source或sink,前者读取存储在HDFS上的Hudi表,后者将数据写人存储于HDFS的Hudi表。流式处理保存的Hudi表,最终交给Presto/Spark SQL/Hive做查询