代码编译
这里我们使用的是Master分支的Hudi进行编译
git clone https://github.com/apache/hudi.git
# 我使用的是scala版本为2.11的Flink,如果是2.12的scala,请在下面语句的最后加上 -Pscala-2.12 -Dscala-2.12
mvn clean install -s $MAVEN_HOME/conf/settings.xml -DskipTests
# 编译完成之后,packaging/hudi-flink-bundle/target/hudi-flink-bundle_2.11-0.9.0-SNAPSHOT.jar 就是要放到我们环境里面的Jar包
Jar包配置
在使用Zeppelin进行Flink X Hudi的开发时,如果你使用flink.execution.jars
的方式加载Hudi的包,那么会有一些问题,不推荐这样使用。如图所示
这里我是建完Hudi表,然后向表中插入数据的时候抛出的异常Caused by: java.lang.ClassNotFoundException: org.apache.hudi.common.metrics.LocalRegistry
按理说这根本不可能,但是如果加载这个类的加载器,和执行这段代码的类加载器不一样,那么就会有这样的问题,所以我建议还是简单粗暴的将hudi-flink-bundle_2.11-0.9.0-SNAPSHOT.jar
这个包放在${FLINK_HOME}/lib
下面
继续踩坑
在环境配置完成之后,我们自然需要去简单的验证一下功能,于是我们建了个表
又出问题了,不过这次这个问题很好解决,在packaging/hudi-flink-bundle/pom.xml
的第164行插入
然后重新打包就行,这个BUG我已经提PR了,当我这篇文章发出去的时候,应该已经合并到Master分支了,到时候大家就不会有这个问题了
这里简单的和大家说一下如何处理这种包冲突的问题
${FLINK_HOME}/lib
下执行grep -w 'com.example.class' *
看看是否有Jar有重复的类,如果有多个结果直接Vim每个Jar包,然后再重新搜索你的类,看看类名是否完全一致。如果确实存在多个Jar中有多个同名类,那么有两种解决方式
mvn dependency:tree
或者Idea中的插件Maven Helper
进行排包解决完了环境问题之后,我们接下来开始正式的学习之旅
create database if not exists hudi_db;
use hudi_db;
drop table if exists hudi_test_dijie;
CREATE TABLE hudi_test_dijie(
a varchar,
dt string
)
PARTITIONED BY (`dt`)
WITH (
'connector'= 'hudi',
'path'= 'hdfs:///hudi/hudi_test_dijie',
'table.type'= 'MERGE_ON_READ',
'read.streaming.enabled'= 'true',
-- 'read.streaming.start-commit'= '20210401134557',
'read.streaming.check-interval'= '4'
);
我们来看一下这个语句,有几个特殊的地方来和大家说一下
首先我们先说一下Hudi的一些基本概念
Apache Hudi (pronounced Hoodie) stands for Hadoop Upserts Deletes and Incrementals.
Hudi的4个字母分别代表的是Hadoop Upserts Delete Incrementals
那既然有更新和删除,那么主键就是必不可少的,不然也没法知道更新哪条数据
在Hudi中,每条数据都有HoodieKey唯一标识,HoodieKey由数据的key和数据所属的分区组成
在Flink中我们可以通过指定hoodie.datasource.write.recordkey.field
来指定我们的Key是数据中的哪个字段,也可以通过PRIMARY KEY(a) NOT ENFORCED
来指定,前者会被后者覆盖
表数据的更新和删除过程在不同类型的Hudi表中有不同的体现,Hudi中目前有两种表类型,分别是
这里为了方便理解,我概括的比较简单,更多的内容可以参考官方文档
read.streaming.start-commit
指的是从哪一个提交位点进行消费
Hudi维护了一个timeline
的概念,它记录了在不同时刻对于表提交的操作,这有助于提供表在不同时刻的状态,同时还可以高效的按照时间进行数据查询
read.streaming.check-interval
比较简单,代表我们每个多久去检验一次有没有新的操作在timeline
上产生
简单的灌几条重复数据进去,顺带验证能否Upsert
INSERT INTO hudi_test_dijie VALUES ('id','2020-04-25',TIMESTAMP '1970-01-01 00:00:01'), ('id','2020-04-25',TIMESTAMP '1970-01-01 00:00:01'), ('id','2020-04-25',TIMESTAMP '1970-01-01 00:00:01'), ('id','2020-04-25',TIMESTAMP '1970-01-01 00:00:01'), ('id','2020-04-25',TIMESTAMP '1970-01-01 00:00:01'), ('id','2020-04-25',TIMESTAMP '1970-01-01 00:00:01'), ('id','2020-04-25',TIMESTAMP '1970-01-01 00:00:01')
报错了,可这次报错看不懂了,哪来的uuid
这个字段?我们并没有在DDL中指定这个,莫慌,我们用IDE打开Hudi的源码一探究竟
先看看异常的堆栈信息,从这条日志信息at org.apache.flink.streaming.api.operators.StreamMap.processElement(StreamMap.java:38)
可以看出来,是Flink在处理每条数据的时候抛出的异常,于是我们到源码中的Hudi-Flink模块,搜索uuid
从描述中能看出来,是为了组装我们前面说的HoodieKey,从数据中取uuid
这个字段,而我们在进行表定义的时候,并没有这个字段,难怪会抛出异常。
再看一下这个字段在哪被引用过
可以看出来,会先取出表定义的主键,如果主键不为空,则用主键组成的联合字段去替换uuid
,如果为空就还是uuid
所以接下来,我们重新建一下有主键的表
create database if not exists hudi_db;
use hudi_db;
drop table if exists hudi_test_dijie;
CREATE TABLE hudi_test_dijie(
a varchar,
dt string,
PRIMARY KEY(a) NOT ENFORCED
)
PARTITIONED BY (`dt`)
WITH (
'connector'= 'hudi',
'path'= 'hdfs:///hudi/hudi_test_dijie',
'table.type'= 'MERGE_ON_READ',
'read.streaming.enabled'= 'true',
-- 'read.streaming.start-commit'= '20210401134557',
'read.streaming.check-interval'= '4',
'write.insert.drop.duplicates' = 'true'
);
当多条数据Key相同需要做预合并时,我们需要根据一个字段来确定哪条数据是新数据哪条是旧数据,
再重新建一次表,这次加上ts
字段
create database if not exists hudi_db;
use hudi_db;
drop table if exists hudi_test_dijie;
CREATE TABLE hudi_test_dijie(
a varchar,
dt string,
ts TIMESTAMP(3),
PRIMARY KEY(a) NOT ENFORCED
)
PARTITIONED BY (`dt`)
WITH (
'connector'= 'hudi',
'path'= 'hdfs:///hudi/hudi_test_dijie',
'table.type'= 'MERGE_ON_READ',
'read.streaming.enabled'= 'true',
-- 'read.streaming.start-commit'= '20210401134557',
'read.streaming.check-interval'= '4',
'write.insert.drop.duplicates' = 'true'
);
这次终于不报错了,那么我们进行下一步
可以看到数据能够正常的查出来,而且数据不存在重复的情况
那,如果我这个时候,将上面的插入语句再执行一次,查出的数据会多吗?
执行一下插入语句,同时再观察一下查询语句的输出结果
既然Hudi支持Upsert,那为何还是会有重复数据呢?
因为MOR表会在读取最新数据的时候,只会将这一次提交的数据给去重,以前的已经下发的数据,没有办法再去处理;如果是从历史提交位点进行读取数据,那么会将这个位点到最新位点中所有的数据做一次去重
那么COW表呢?很遗憾,目前Hudi Streaming Read只支持MOR表,COW表暂未支持
好了,写到这里,本文的内容已经全部讲完,更多的内容可以多多看看官网,或者等我更新~
canal-json
的Foramt,将数据以canal-json
的格式存入Hudi表中,读取的时候再以同样的格式读取,可以利用Flink的能力来进行数据回撤,这种方式比较推荐[1] 官方文档