前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住给大家分享一下。点击跳转到网站:https://www.captainai.net/dongkelun
前言
在最开始学习Hudi源码时,就发现了Hudi有一个Bootstrap功能,但是一直没用过,通过官网文档https://hudi.apache.org/cn/docs/migration_guide/可知,它可以将现有的表件转化为Hudi表,而且有两种类型METADATA_ONLY
和FULL_RECORD
,但是文档并不详细,比如这两种类型的区别具体是啥,支持哪些文件类型的源表。于是带着这些疑问来学习一下它是如何使用的以及源码原理的实现,这样可以更全面的了解Hudi。
版本
Hudi 0.12.0
Spark 2.4.4/3.1.2
支持的文件类型
如题,目前只支持两种文件类型:parquet和orc,对于其他格式的Hive表,比如text就不能用Bootstrap进行转化了
其实官方文档并没有说明支持哪些类型,我们可以在源码里找到答案:
METADATA_ONLY
1 | public static BootstrapMetadataHandler getMetadataHandler(HoodieWriteConfig config, HoodieTable table, HoodieFileStatus srcFileStatus) { |
FULL_RECORD
FullRecordBootstrapDataProvider
只有两个实现类
1 | public abstract class FullRecordBootstrapDataProvider<I> implements Serializable { |
Bootstrap 类型
METADATA_ONLY
:只会生成主键、页脚的基本框架文件,不会重写全部数据,这样可以节省时间,提升效率,而查询时,会根据元数据信息去查源表的文件,也就是原来的表数据文件不能删除,这算是它的一个弊端,不过当有后续数据写入,第一次commit对应的parquet文件变为历史文件时,删除源表的文件就不会影响正常读取了。_hoodie_commit_time
为00000000000001
,对应源码HoodieTimeline.METADATA_BOOTSTRAP_INSTANT_TSFULL_RECORD
:首先读取源表文件生成RDD,然后将RDD以bulkInsert的形式写到新的Hudi表,也就是以Hudi表的形式执行数据的完整复制/重写。_hoodie_commit_time
为00000000000002
,对应源码HoodieTimeline.FULL_BOOTSTRAP_INSTANT_TS
不管用哪种类型,我们后续都可以在这个转化的Hudi表上执行upsert等各种写操作和查询,需要注意的是要求源表不能有重复数据,因为利用Hudi Bootstrap转化Hudi表时不会去重,否则我们在后续的upsert时,也会有重复数据。
使用方式一:Spark代码
完整代码已提交到github:https://github.com/dongkelun/hudi-demo
1 | def runMetadataBootstrap(srcPath: String, basePath: String, partitionField: String = "", extraOpts: Map[String, String] = Map.empty): Unit = { |
METADATA_ONLY
1 | def testMetadataBootstrapCOWHiveStylePartitioned2(): Unit = { |
1 | +-------------------+---------------------+------------------+----------------------+------------------------------------------------------------------------+---+----+-----+----+----------+ |
问题:hive分区表对应的parquet文件中没有分区字段,对于这种情况,Spark读取转化后的Hudi表时不能读取分区字段值,不过同步到Hive表,用Hive SQL 查询是可以正常查到hive分区字段的,解决方式:修改源码使Spark Hudi Bootstartp读写支持配置项
hoodie.datasource.write.drop.partition.columns
,参考PR:https://github.com/apache/hudi/pull/5201
验证逻辑
前提:先不要append,否则会变为历史数据
首先直接读取Hudi表下面的parquet文件看一下内容
1 | spark.read.parquet("/tmp/bootstrap/metadata/base/f17f9a8d-a409-4023-a8a4-90a5ed21f745_1499-14-1511_00000000000001.parquet").show(false) |
1 | +-------------------+---------------------+------------------+----------------------+------------------------------------------------------------------------+ |
确实只有元数据信息,然后再删除源表的parquet文件
1 | spark.read.format("hudi").load("/tmp/bootstrap/metadata/base").show() |
然后会抛出异常1
java.io.FileNotFoundException: File file:/tmp/bootstrap/metadata/src/part-00000-5429fabc-94be-4bc0-b1d7-f15adff0a619-c000.snappy.parquet does not exist
说明读取时会读源表的parquet文件
FULL_RECORD
1 | def testFullBootstrapCOWHiveStylePartitioned2(): Unit = { |
1 | +-------------------+---------------------+------------------+----------------------+--------------------------------------------------------------------------+---+----+-----+----+----------+ |
问题:1、hive分区表对应的parquet文件中没有分区字段,对于这种情况
FULL_RECORD
目前还不支持,解决方式:修改源码使Spark Hudi Bootstartp读写支持配置项hoodie.datasource.write.drop.partition.columns
,参考PR:https://github.com/apache/hudi/pull/5201
2、对于parquet文件内容有要求,当sourceDF存在不是String类型的字段,且nullable=false时,转化的Hudi表会乱码,所以示例代码里先将sourceDF的所有字段的nullable设置成了true,原因应该是因为源码里读取源表parquet文件时对某些schema不兼容,修改读取parquet文件相关的源码应该就可以解决
使用方式2:HoodieDeltaStreamer
METADATA_ONLY
1 | bin/spark-submit \ |
1 | select * from test_hudi_bootstrap_metadata; |
问题:这里设置的hive_style_partitioning=true没有生效
FULL_RECORD
只需要配置:hoodie.bootstrap.mode.selector=org.apache.hudi.client.bootstrap.selector.FullRecordBootstrapModeSelector,类型就会变成FULL_RECORD
,还有其他配置项可以修改类型,可以自己验证
1 | bin/spark-submit \ |
1 | select * from test_hudi_bootstrap_full; |
官网博客上的问题
1 | Here is an example for running METADATA_ONLY bootstrap using Delta Streamer. |
这里说的是METADATA_ONLY
的例子,经验证实际为FULL_RECORD
,虽然配置了regex.mode=METADATA_ONLY,其实默认值就是METADATA_ONLY,但是不生效,原因是因为配置了hoodie.bootstrap.mode.selector=org.apache.hudi.client.bootstrap.selector.BootstrapRegexModeSelector,这个类里的defaultMode的逻辑应该有问题,我已经提交了PR:https://github.com/apache/hudi/pull/6928
更新:对于BootstrapRegexModeSelector
我理解有误,它的逻辑应该是这样的:
首先有配置:hoodie.bootstrap.mode.selector.regex.mode 默认值METADATA_ONLY、hoodie.bootstrap.mode.selector.regex默认值.*
但是如果不是默认值的话,比如上面的2020/08/2[0-9]
,假设我们有分区”2020/08/10,2020/08/10/11,2020/08/20,2020/08/21”,那么匹配成功的2020/08/20
和2020/08/21
对应的类型为METADATA_ONLY
,匹配不成功的2020/08/10
和2020/08/10/11
则为FULL_RECORD
。而至于我的为啥都是FULL_RECORD
,原因是regex设置错误,我设置的是2022/10/0[0-9]
,但实际的分区值为2022-10-08
和2022-10-09
(分隔符不一样),而如果用默认的.*
的话,则全部能匹配上,也就都是METADATA_ONLY
,但是博客上的示例多少还是会误导读者的~
具体看源码:
1 | public BootstrapRegexModeSelector(HoodieWriteConfig writeConfig) { |
其他转化现有表为Hudi表的方法
可以自己写代码
1 | val sourceDf = spark.table("tableName") |
也可以使用HoodieDeltaStreamer
的其他功能,可以参考我之前写的文章:Hudi DeltaStreamer使用总结
总结
本文介绍了如何利用Hudi Bootstrap转化现有Hive表为Hudi表,提供了完整的代码示例,并分析了METADATA_ONLY
和FULL_RECORD
的区别以及利弊,指出了目前版本存在的一些问题,虽然有着些许问题,但是我相信社区以后肯定会完善解决这些问题,这样我们转化Hudi表时也多了一个选择。