1、环境准备
将编译好的jar包放到Flink的lib目录下。
cp hudi-flink1.13-bundle-0.12.0.jar /opt/module/flink-1.13.2/lib
2、sql-client方式
2.1、修改flink-conf.yaml配置
vim /opt/module/flink-1.13.2/conf/flink-conf.yaml
state.backend: rocksdb
execution.checkpointing.interval: 30000
state.checkpoints.dir: hdfs://hadoop1:9000/ckps
state.backend.incremental: true
2.2、yarn-session模式启动
1、启动
1、先启动hadoop集群,然后通过yarn-session启动flink:
/opt/module/flink-1.13.2/bin/yarn-session.sh -d
2、再启动sql-client
/opt/module/flink-1.13.2/bin/sql-client.sh embedded -s yarn-session
2、写入数据
表格模式(table mode)在内存中实体化结果,并将结果用规则的分页表格可视化展示出来。执行如下命令启用:
SET 'sql-client.execution.result-mode' = 'table'; --默认
变更日志模式(changelog mode)不会实体化和可视化结果,而是由插入(+)和撤销(-)组成的持续查询产生结果流。
SET 'sql-client.execution.result-mode' = 'changelog';
Tableau模式(tableau mode)更接近传统的数据库,会将执行的结果以制表的形式直接打在屏幕之上。具体显示的内容会取决于作业执行模式的不同(execution.type):
SET 'sql-client.execution.result-mode' = 'tableau';
-- 创建hudi表
CREATE TABLE t1(
uuid VARCHAR(20) PRIMARY KEY NOT ENFORCED,
name VARCHAR(10),
age INT,
ts TIMESTAMP(3),
`partition` VARCHAR(20)
)
PARTITIONED BY (`partition`)
WITH (
'connector' = 'hudi',
'path' = 'hdfs://hadoop1:9000/tmp/hudi_flink/t1', --hudi表的基本路径
'table.type' = 'MERGE_ON_READ' --默认是COW
);
-- 插入数据
INSERT INTO t1 VALUES
('id1','Danny',23,TIMESTAMP '1970-01-01 00:00:01','par1'),
('id2','Stephen',33,TIMESTAMP '1970-01-01 00:00:02','par1'),
('id3','Julian',53,TIMESTAMP '1970-01-01 00:00:03','par2'),
('id4','Fabian',31,TIMESTAMP '1970-01-01 00:00:04','par2'),
('id5','Sophia',18,TIMESTAMP '1970-01-01 00:00:05','par3'),
('id6','Emma',20,TIMESTAMP '1970-01-01 00:00:06','par3'),
('id7','Bob',44,TIMESTAMP '1970-01-01 00:00:07','par4'),
('id8','Han',56,TIMESTAMP '1970-01-01 00:00:08','par4');
3、IDEA编码方式
3.1、环境准备
1、手动install依赖
mvn install:install-file -DgroupId=org.apache.hudi -DartifactId=hudi-flink_2.11 -Dversion=0.12.0 -Dpackaging=jar -Dfile=./hudi-flink1.13-bundle-0.12.0.jar
2、编写代码
import org.apache.flink.contrib.streaming.state.{EmbeddedRocksDBStateBackend, PredefinedOptions}
import org.apache.flink.streaming.api.CheckpointingMode
import org.apache.flink.streaming.api.environment.{CheckpointConfig, StreamExecutionEnvironment}
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment
import java.util.concurrent.TimeUnit
object HudiExample {
def main(args: Array[String]): Unit = {
// val env = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI(new Configuration())
val env = StreamExecutionEnvironment.getExecutionEnvironment
// 设置状态后端RocksDB
val embeddedRocksDBStateBackend = new EmbeddedRocksDBStateBackend(true)
// embeddedRocksDBStateBackend.setDbStoragePath("file:///E:/rocksdb")
embeddedRocksDBStateBackend.setPredefinedOptions(PredefinedOptions.SPINNING_DISK_OPTIMIZED_HIGH_MEM)
env.setStateBackend(embeddedRocksDBStateBackend)
// checkpoint配置
env.enableCheckpointing(TimeUnit.SECONDS.toMillis(10), CheckpointingMode.EXACTLY_ONCE)
val checkpointConfig = env.getCheckpointConfig
checkpointConfig.setCheckpointStorage("hdfs://hadoop1:9000/ckps")
checkpointConfig.setMinPauseBetweenCheckpoints(TimeUnit.SECONDS.toMillis(10))
checkpointConfig.setTolerableCheckpointFailureNumber(5)
checkpointConfig.setCheckpointTimeout(TimeUnit.MINUTES.toMillis(1))
checkpointConfig.enableExternalizedCheckpoints(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION)
val sTableEnv = StreamTableEnvironment.create(env)
sTableEnv.executeSql("CREATE TABLE sourceT (\n" +
" uuid varchar(20),\n" +
" name varchar(10),\n" +
" age int,\n" + " ts timestamp(3),\n" +
" `partition` varchar(20)\n" +
") WITH (\n" +
" 'connector' = 'datagen',\n" +
" 'rows-per-second' = '1'\n" +
")")
sTableEnv.executeSql("create table t2(\n" +
" uuid varchar(20),\n" +
" name varchar(10),\n" +
" age int,\n" +
" ts timestamp(3),\n" +
" `partition` varchar(20)\n" +
")\n" +
"with (\n" +
" 'connector' = 'hudi',\n" +
" 'path' = 'hdfs://hadoop1:9000/tmp/hudi_flink/t2',\n" +
" 'table.type' = 'MERGE_ON_READ'\n" +
")")
sTableEnv.executeSql("insert into t2 select * from sourceT")
}
}
3、提交运行
bin/flink run -t yarn-per-job -c com.my.example.HudiExample ./myjars/HudiExample-1.0-SNAPSHOT-jar-with-dependencies.jar
4、核心参数设置
Flink可配参数:https://hudi.apache.org/docs/configurations#FLINK_SQL
4.1、去重参数
通过如下语法设置主键:
-- 设置单个主键
create table hoodie_table (
f0 int primary key not enforced,
f1 varchar(20),
...
) with (
'connector' = 'hudi',
...
)
-- 设置联合主键
create table hoodie_table (
f0 int,
f1 varchar(20),
...
primary key(f0, f1) not enforced
) with (
'connector' = 'hudi',
...
)
名称 | 说明 | 默认值 | 备注 |
hoodie.datasource.write.recordkey.field | 主键字段 | -- | 支持主键语法 PRIMARY KEY 设置,支持逗号分隔的多个字段 |
precombine.field (0.13.0 之前版本为 write.precombine.field) | 去重时间字段 | -- | record 合并的时候会按照该字段排序,选值较大的 record 为合并结果;不指定则为处理序:选择后到的 record |
4.2、并发参数
名称 | 说明 | 默认值 | 备注 |
write.tasks | writer 的并发,每个 writer 顺序写 1~N 个 buckets | 4 | 增加并发对小文件个数没影响 |
write.bucket_assign.tasks | bucket assigner 的并发 | Flink的并行度 | 增加并发同时增加了并发写的 bucekt 数,也就变相增加了小文件(小 bucket) 数 |
write.index_bootstrap.tasks | Index bootstrap 算子的并发,增加并发可以加快 bootstrap 阶段的效率,bootstrap 阶段会阻塞 checkpoint,因此需要设置多一些的 checkpoint 失败容忍次数 | Flink的并行度 | 只在 index.bootstrap.enabled 为 true 时生效 |
read.tasks | 读算子的并发(batch 和 stream) | 4 | |
compaction.tasks | online compaction 算子的并发 | writer 的并发 | online compaction 比较耗费资源,建议走 offline compaction |
案例演示
可以flink建表时在with中指定,或Hints临时指定参数的方式:在需要调整的表名后面加上 /*+ OPTIONS() */
CREATE TABLE sourceT (
uuid varchar(20),
name varchar(10),
age int,
ts timestamp(3),
`partition` varchar(20)
) WITH (
'connector' = 'datagen',
'rows-per-second' = '1'
);
create table t2(
uuid varchar(20),
name varchar(10),
age int,
ts timestamp(3),
`partition` varchar(20)
)
with (
'connector' = 'hudi',
'path' = '/tmp/hudi_flink/t2',
'table.type' = 'MERGE_ON_READ'
);
insert into t2 /*+ OPTIONS('write.tasks'='2','write.bucket_assign.tasks'='3','compaction.tasks'='4') */
select * from sourceT;
执行如下图所示:
4.3、压缩参数
1、参数说明
在线压缩的参数,通过设置 compaction.async.enabled =false关闭在线压缩执行,但是调度compaction.schedule.enabled 仍然建议开启,之后通过离线压缩直接执行 在线压缩任务 阶段性调度的压缩 plan。
名称 | 说明 | 默认值 | 备注 |
compaction.schedule.enabled | 是否阶段性生成压缩 plan | true | 建议开启,即使compaction.async.enabled 关闭的情况下 |
compaction.async.enabled | 是否开启异步压缩 | true | 通过关闭此参数关闭在线压缩 |
compaction.tasks | 压缩 task 并发 | 4 | |
compaction.trigger.strategy | 压缩策略 | num_commits | 支持四种策略:num_commits、time_elapsed、num_and_time、 num_or_time |
compaction.delta_commits | 默认策略,5 个 commits 压缩一次 | 5 | |
compaction.delta_seconds | 3600 | ||
compaction.max_memory | 压缩去重的 hash map 可用内存 | 100(MB) | 资源够用的话建议调整到 1GB |
compaction.target_io | 每个压缩 plan 的 IO 上限,默认 5GB | 500(GB) |
2、案例演示
CREATE TABLE t3(
uuid VARCHAR(20) PRIMARY KEY NOT ENFORCED,
name VARCHAR(10),
age INT,
ts TIMESTAMP(3),
`partition` VARCHAR(20)
)
WITH (
'connector' = 'hudi',
'path' = '/tmp/hudi_flink/t3',
'compaction.async.enabled' = 'true',
'compaction.tasks' = '1',
'compaction.schedule.enabled' = 'true',
'compaction.trigger.strategy' = 'num_commits',
'compaction.delta_commits' = '2',
'table.type' = 'MERGE_ON_READ'
);
set table.dynamic-table-options.enabled=true;
insert into t3
select * from sourceT/*+ OPTIONS('rows-per-second' = '5')*/;
4.4、文件大小
1、参数说明
Hudi会自管理文件大小,避免向查询引擎暴露小文件,其中自动处理文件大小起很大作用。在进行insert/upsert操作时,Hudi可以将文件大小维护在一个指定文件大小。
只有log文件的写入大小可以做到精确控制,parquet 文件大小按照估算值。
名称 | 说明 | 默认值 | 备注 |
hoodie.parquet.max.file.size | 最大可写入的 parquet 文件大小 | 120 * 1024 * 1024 默认 120MB (单位 byte) | 超过该大小切新的 file group |
hoodie.logfile.to.parquet.compression.ratio | log文件大小转 parquet 的比率 | 0.35 | hoodie统一依据 parquet 大小来评估小文件策略 |
hoodie.parquet.small.file.limit | 在写入时,hudi 会尝试先追加写已存小文件,该参数设置了小文件的大小阈值,小于该参数的文件被认为是小文件 | 104857600 默认 100MB (单位 byte) | 大于 100MB,小于 120MB 的文件会被忽略,避免写过度放大 |
hoodie.copyonwrite.record.size.estimate | 预估的record大小,hoodie 会依据历史的commits动态估算record的大小,但是前提是之前有单次写入超过 hoodie.parquet.small.file.limit大小,在未达到这个大小时会使用这个参数 | 1024 默认 1KB (单位 byte) | 如果作业流量比较小,可以设置下这个参数 |
hoodie.logfile.max.size | LogFile最大大小。这是在将Log滚转到下一个版本之前允许的最大大小。 | 1073741824 默认1GB (单位 byte) |
5、内存优化
5.1、内存参数
名称 | 说明 | 默认值 | 备注 |
write.task.max.size | 一个 write task 的最大可用内存 | 1024 | 当前预留给 write buffer 的内存为 write.task.max.size -compaction.max_memory 当 write task 的内存 buffer达到阈值后会将内存里最大的 buffer flush 出去 |
write.batch.size | Flink 的写 task 为了提高写数据效率,会按照写 bucket 提前 buffer 数据,每个 bucket 的数据在内存达到阈值之前会一直 cache 在内存中,当阈值达到会把数据 buffer 传递给 hoodie 的 writer 执行写操作 | 256 | 一般不用设置,保持默认值就好 |
write.log_block.size | hoodie 的 log writer 在收到 write task 的数据后不会马上 flush 数据,writer 是以 LogBlock 为单位往磁盘刷数据的,在 LogBlock 攒够之前 records 会以序列化字节的形式 buffer 在 writer 内部 | 128 | 一般不用设置,保持默认值就好 |
write.merge.max_memory | hoodie 在 COW 写操作的时候,会有增量数据和 base file 数据 merge 的过程,增量的数据会缓存在内存的 map 结构里,这个 map 是可 spill 的,这个参数控制了 map 可以使用的堆内存大小 | 100 | 一般不用设置,保持默认值就好 |
compaction.max_memory | 同 write.merge.max_memory: 100MB 类似,只是发生在压缩时。 | 100 | 如果是 online compaction,资源充足时可以开大些,比如 1GB |
5.2、MOR
(1)state backend 换成 rocksdb (默认的 in-memory state-backend 非常吃内存)
(2)内存够的话,compaction.max_memory 调大些 (默认是 100MB 可以调到 1GB)
(3)关注 TM 分配给每个 write task 的内存,保证每个 write task 能够分配到 write.task.max.size 所配置的大小,比如 TM 的内存是 4GB 跑了 2 个 StreamWriteFunction 那每个 write function 能分到 2GB,尽量预留一些 buffer,因为网络 buffer,TM 上其他类型 task (比如 BucketAssignFunction 也会吃些内存)
(4)需要关注 compaction 的内存变化,compaction.max_memory 控制了每个 compaction task 读 log 时可以利用的内存大小,compaction.tasks 控制了 compaction task 的并发
注意: write.task.max.size - compaction.max_memory 是预留给每个 write task 的内存 buffer
5.3、COW
(1)state backend 换成 rocksdb(默认的 in-memory state-backend 非常吃内存)。
(2)write.task.max.size 和 write.merge.max_memory 同时调大(默认是 1GB 和 100MB 可以调到 2GB 和 1GB)。
(3)关注 TM 分配给每个 write task 的内存,保证每个 write task 能够分配到 write.task.max.size 所配置的大小,比如 TM 的内存是 4GB 跑了 2 个 StreamWriteFunction 那每个 write function 能分到 2GB,尽量预留一些 buffer,因为网络 buffer,TM 上其他类型 task(比如 BucketAssignFunction 也会吃些内存)。
注意:write.task.max.size - write.merge.max_memory 是预留给每个 write task 的内存 buffer。