1. 前言
如果要深入了解apache hudi技术的应用或是性能调优,那么明白源码中的原理对我们会有很大的帮助。在apache hudi 中upsert 是他的核心功能之一,主要完成增量数据在hdfs上的修改,并可以支持事务。在hive中修改数据需要重新分区或重新整个表,但是对于hudi更新可以是文件级别的重写或是数据先进行追加后续再重写,对比Hive 大大地提高了更新性能。upsert支持两种模式的写入copy on write和merge on read ,下面本文将介绍Apache Hudi 在spark中upsert的内核原理。
2. Upsert场景执行流程介绍
对于hudi upsert 操作整理了比较核心的几个操作如图:

-
开始提交:判断上次任务是否失败,如果失败会触发回滚操作。 然后会根据当前时间生成一个事务开始的请求标识元数据。
-
构造HoodieRecord Rdd对象:hudi 会根据元数据信息构造HoodieRecord Rdd 对象,方便后续数据去重和数据合并。
-
数据去重:一批增量数据中可能会有重复的数据,hudi会根据主键对数据进行去重避免重复数据写入hudi 表。
-
数据fileId位置信息获取:在修改记录中可以根据索引获取当前记录所属文件的fileid,在数据合并时需要知道数据update操作向那个fileId文件写入新的快照文件。
-
数据合并:hudi 有两种模式cow和mor。在cow模式中会重写索引命中的fileId快照文件;在mor 模式中根据fileId 追加到分区中的log 文件。
-
完成提交:在元数据中生成xxxx.commit文件,只有生成commit 元数据文件,查询引擎才能根据元数据查询到刚刚upsert 后的数据。
-
compaction压缩:主要是mor 模式中才会有,他会将mor模式中的xxx.log 数据合并到xxx.parquet 快照文件中去。
-
hive元数据同步:hive 的元素数据同步这个步骤需要配置非必需操作,主要是对于hive 和presto 等查询引擎,需要依赖hive 元数据才能进行查询。所以在hive 中的同步就是构造外表提供查询。

介绍完hudi的upsert运行流程,在来看下hudi如何进行存储并且保证事务,在每次upsert完成后都会产生commit 文件记录每次重新的快照文件。
例如上图时间1初始化写入三个分区文件分别是xxx-1.parquet,在时间3场景如果会修改分区1和分区2xxx-1.parquet的数据,那么写入完成后会生成新的快照文件分别是分区1和分区2xxx-3.parquet文件。(上述是cow模式的过程,而对于MOR模式的更新会生成log文件,如果log文件存在追加数据)。如果时间5在去修改分区1的数据那么同理会生成分区1下的新快照文件。可以看出对于hudi 每次修改都是会在文件级别重新写入数据快照。查询的时候就会根据最后一次快照元数据加载每个分区小于等于当前的元数据的parquet文件。hudi事务的原理就是通过元数据mvcc多版本控制写入新的快照文件,在每个时间阶段根据最近的元数据查找快照文件。因为是重写数据所以同一时间只能保证一个事务去重写parquet 文件。不过当前hudi版本加入了并发写机制,原理是zookeeper分布锁控制或者HMS提供锁的方式, 会保证同一个文件的修改只有一个事务会写入成功。
下面将根据spark 调用write方法开始剖析upsert操作每个步骤的执行流程。
2.1 开始提交&数据回滚
在构造好spark 的rdd 后会调用 df.write.format("hudi") 方法执行数据的写入,实际会调用HudiHoodieSparkSqlWriter#write方法实现。在执行任务前hudi 会创建HoodieWriteClient 对象,并构造HoodieTableMetaClient调用startCommitWithTime方法开始一次事务。在开始提交前会获取hoodie 目录下的元数据信息,判断上一次写入操作是否成功,判断的标准是上次任务的快照元数据有xxx.commit后缀的元数据文件。如果不存在那么hudi 会触发回滚机制,回滚是将不完整的事务元数据文件删除,并新建xxx.rollback元数据文件。如果有数据写入到快照parquet 文件中也会一起删除。

2.2 构造HoodieRecord Rdd 对象
HoodieRecord Rdd 对象得构造先是通过map 算子,提取spark dataframe中的schema和数据,构造avro的genericRecords Rdd, 然后hudi会在进行map算子封装为HoodierRecord Rdd。对于HoodileRecord Rdd 主要由currentlocation,newlocation,hoodiekey,data 组成。HoodileRecord数据结构是为后续数据去重和数据合并时提供基础。

- currentLocation 当前数据位置信息:只有数据在当前hudi表中存在才会有,主要存放parquet文件的fileId,构造时默认为空,在查找索引位置信息时被赋予数据。
- newLocation 数据新位置信息:与currentLocation不同不管是否存在都会被赋值,newLocation是存放当前数据需要被写入到那个fileID文件中的位置信息,构造时默认为空,在merge阶段会被赋予位置信息。
- HoodieKey 主键信息:主要包含recordKey 和patitionPath 。recordkey 是由hoodie.datasource.write.recordkey.field 配置项根据列名从记录中获取的主键值。patitionPath 是分区路径。hudi 会根据hoodie.datasource.write.partitionpath.field 配置项的列名从记录中获取的值作为分区路径。
- data 数据:data是一个泛型对象,泛型对象需要实现HoodieRecordPayload类,主要是实现合并方法和比较方法。默认实现OverwriteWithLatestAvroPayload类,需要配置hoodie.datasource.write.precombine.field配置项获取记录中列的值用于比较数据大小,去重和合并都是需要保留值最大的数据。
2.3 数据去重
在upsert 场景中数据去重是默认要做的操作,如果不进行去重会导致数据重复写入parquet文件中。当然upsert 数据中如果没有重复数据是可以关闭去重操作。配置是否去重参数为hoodie.combine.before.upsert,默认为true开启。

在spark client调用upsert 操作是hudi会创建HoodieTable对象,并且调用upsert 方法。对于HooideTable 的实现分别有cor和mor 两种模式的实现。但是在数据去重阶段和索引查找阶段的操作都是一样的。调用HoodieTable upsert方法后底层的实现都是spark AbstractWriteHelper。在去重操作中,会先使用map 算子
Apache Hudi Upsert 原理

本文深入解析Apache Hudi中upsert的核心流程,包括提交与回滚、HoodieRecordRdd构建、数据去重、索引查找、数据合并及索引更新等关键环节,帮助理解Hudi如何在HDFS上高效处理增量数据。
最低0.47元/天 解锁文章
3937

被折叠的 条评论
为什么被折叠?



