## sql部分不兼容的地方
### 时间属性
proctime/event_time类型必须定义为timestamp(3),否则会报错。
在1.11中,`ts timestamp`,相当于定义`timestamp(6)`。 而时间属性类型为timestamp(3)
在1.9中,time attr在传递的时候回自动将类型转换为timestamp类型。在1.11中,这不会发生,
传递的类型是LocalDateTime。如果需要,需要你自己定义类型绑定:
```
/**
* The original table schema may contain generated columns which shouldn't be produced/consumed
* by TableSource/TableSink. And the original TIMESTAMP/DATE/TIME types uses LocalDateTime/LocalDate/LocalTime
* as the conversion classes, however, JDBC connector uses Timestamp/Date/Time classes. So that
* we bridge them to the expected conversion classes.
*
* proc time, event time现在是LocalDateTime类型。
* 事实上大多数数据库系统都不支持。这里的作用是将其绑定到sql的timestamp类型。
*/
def transSqlTypesSchema(schema: TableSchema): TableSchema = {
val physicalSchemaBuilder = TableSchema.builder
schema.getTableColumns.foreach(c => {
if (!c.isGenerated) {
val `type` = DataTypeUtils.transform(c.getType, TypeTransformations.timeToSqlTypes)
physicalSchemaBuilder.field(c.getName, `type`)
}
})
physicalSchemaBuilder.build
}
```
为了解决这个问题,connector、format、udf可能需要适配LocalDateTime类型。
### connector 参数变更。
schema的type通过data-type参数传递,所以connector支持的参数必须添加schema.#.data-type。
在1.11版本中支持通过ddl定义time attr。
定义proctime :
```
create table test(
ts as proctime()
) with (...)
```
在source连接器中,必须添加支持:`schema.#.expr`参数。
定义 event time :
```
create table test(
user_action_time TIMESTAMP(3)
-- 定义user_action_time为事件时间,最大延迟为5秒钟。
watermark for user_action_time as user_action_time - interval '5' second
) with(..,)
```
支持这个需要需要支持如下属性:
```
//connector factory的supportedProperties方法
properties.add("schema.watermark.#.strategy.expr")
properties.add("schema.watermark.#.rowtime")
properties.add("schema.watermark.#.strategy.data-type")
```
### schema获取
schema获取必须通过 TableSchemaUtils.getPhysicalSchema获取,否则会报类型不匹配。
```
TableSchemaUtils.getPhysicalSchema(desc.getTableSchema(SCHEMA))
```
## filesystem
flink 1.11版本重写了filesystem连接器。
支持分区表,支持json、parquet、orc等格式。
parquet、orc是通过hive的库来支撑的,所以可以压缩。
但是其他格式是通过flink format支持,所以不可以压缩。
filesystem支持通过流来写入文件,如果是未分区表,那么直接写入;如果是分区表,分区需要进行配置才能完成分区提交。
### 文件滚动配置
配置 | 说明
--- | ---
sink.rolling-policy.file-size | part file最大大小。默认128mb。超过这个,会滚动新的文件。
sink.rolling-policy.rollover-interval | part file打开状态的最大时间(就是滚动时间)。默认30 m (30分钟)
sink.rolling-policy.check-interval | 检查文件是否滚动的频率。默认 1 m(1分钟)。就是检查文档打开时间是否超过了sink.rolling-policy.rollover-interval配置的值。
对应orc、parquet文件格式,滚动策略和checkpoint结合,也就是在checkpoint的时候文件会进行提交和滚动。
对于json文本格式(可以append),设置滚动策略可以让文件在checkpoint之前可见。 对于orc、parquet等格式,文件必须在checkpoint之后可见。
### 分区提交
数据写入分区表的分区后,需要通知下游程序新的分区创建或者更新hive metastore。
文件连接器允许配置分区提交策略,仅仅对动态分区插入有效。
分区提交触发:可以通过水印或者分区字段中提取时间来提交。
提交方式:支持写入_success文件、更新metastore。也可以自定义提交。
配置参数:
配置 | 说明
--- |---
sink.partition-commit.trigger | 触发方式,默认process-time。系统时间 > 分区创建时间 + 延迟时间,就提交分区;
如果设置为partition-time,从分区值中提取时间,需要生产watermark。watermark > 分区时间 + 延迟时间,就提交分区。
sink.partition-commit.delay | 提交的延迟时间,默认0,不提交。可以设置为比如“1 h”等时间间隔格式
sink.partition-commit.policy.kind | 无默认值。设置分区提交方式。metastore:提交到元数据;success-file,写入_success文件到
分区目录中。也可以同时设置2个:metastore,success-file。目前只有hive表支持metastore。
sink.partition-commit.policy.class | 分区提交类。自定义策略时候需要。
sink.partition-commit.success-file.name | 成功文件名称。默认_success。
基于process time的提交,不需要水印不需要提供分区时间,是非常通常的触发方式。如果数据延迟或者故障,会导致分区提前被提交,
但其实这并不是一个问题。
根据分区时间和水印提交分区,这需要流具有水印,分区是已经时间分区。
如果想提前看到分区的数据,无论数据是否完整,可以设置如下:
```
'sink.partition-commit.trigger'= 'process-time'
'sink.partition-commit.delay'= '0s'
```
这意味着,分区一旦有数据,就立即提交。这可能导致分区被多次提交。
如果要让下游仅在分区数据完成后才看到该分区,可以设置如下:
```
'sink.partition-commit.trigger'= '’partition-time'
'sink.partition-commit.delay'= '1h'
```
delay值应该和分区时间保持一直。这只能保存分区提交的时候,数据尽可能完整。对应steaming应用来说,这不是一个好的方式,不建议使用。
分区提交后,延迟数据到达,数据会写入到正确的分区中,会触发一次分区提交。
### 注意事项。
1. filesystem sink 通过2阶段提交保存恰好一次语义。
2. 任务异常退出,临时文件不会被删除。
3. 已经提交的文件是不会删除的。如果重新消费数据写入文件,会有重复数据,这需要你启动程序前删除历史数据。
4. 目前 filesystem sink只支持 append流(任然基于老的sink接口实现)
### 文件sink 实例
```
set checkpoint.checkpointInterval = 10000;
create table source (
id varchar,
name varchar
) with (
'connector.type' = 'sys-file',
'connector.path' = 'tmp/data.log',
'format.type' = 'json',
'format.derive-schema' = 'true'
);
print source;
create table sink (
dt varchar,
id varchar,
name varchar
) partitioned by (dt) with (
'connector' = 'filesystem',
'format' = 'orc',
'path' = 'hdfs://SZD-L0097851:8020/user/hadoop/flink-hive'
);
insert into sink select cast(sys_current_date() as varchar), * from source;
```
### 文件sink一致性语义保证
1. 文件创建的时候,生成“.”开始的文件,这个实时文件处于inprogress状态。“.”开始的文件,读取会被跳过。
2. 文件写入过程中,发生了文件滚动。上一个文件会执行close操作,刷新数据,加入到pendingFileRecoverablesForCurrentCheckpoint队列中。
新的文件保存在inProgressPart变量,表示当前正在处理的文件。
3. checkpoint发生的时候,inProgressPart文件执行close操作(shouldRollOnCheckpoint需要返回true)。
pendingFileRecoverablesPerCheckpoint会保存到state中。
4. checkpoint完成回调方法中,pendingFileRecoverablesPerCheckpoint中的所有文件都会执行comment操作(文件重命名)
上面的操作是针对buckets的,一个文件sink可能有对个buckets,也就是多个分区。
## hive stream
hive stream本质上是基于 flink filesystem,本质还是直接读写文件。
flink 通过配置文件初始化metastore,通过client获取到hive表的元数据信息,
读取表的路径和flink相关的属性信息,创建一个 flink filesystem sink。
主要file system的相关参数,可以在创建hive表的时候设置,flink会读取这些参数。
### sink完整案例
```
-- 在hive中创建hive表。
create table flink_sink (
ts timestamp,
id string ,
name string
) partitioned by (dt string) stored as orc tblproperties (
'sink.partition-commit.policy.kind'='metastore,success-file'
);
```
sql 案例
```
set checkpoint.checkpointInterval = 10000;
-- flink 1.11不支持ddl创建catalog,这里是平台封装后的。
create catalog hive with(
'hive-conf-dir' = 'src/test/resources'
);
create table source (
ts as proctime(),
id varchar,
name varchar
) with (
'connector.type' = 'sys-file',
'connector.path' = 'tmp/data.log',
'format.type' = 'json',
'format.derive-schema' = 'true'
);
insert into hive.`default`.flink_sink
select
ts,
id,
name,
cast(sys_current_date() as varchar)
from source;
```
sink 支持分区提交,具体查看filesystem的相关配置。
### hive read
和flume类,1.11版本支持hive文件流的方式读取。flink会监控文件的动态。
### hive 维表读取。
### 计算列
计算列方便了table的定义。在1.11中定义time attr依赖于计算列,其他字段使用计算列可能作用不大,
实例如下:
```
create table source (
ts as proctime() ,
id varchar,
name varchar,
xx as concat(id, name)
) with (
'connector.type' = 'sys-file',
'connector.path' = 'tmp/data.log',
'format.type' = 'json',
'format.derive-schema' = 'true'
);
print source;
```
主要:计算列是读取source的数据之后计算得到的。
### time attr
定义proctime :
```
create table test(
ts as proctime()
) with (...)
```
在source连接器中,必须添加支持:`schema.#.expr`参数。
定义 event time(watermark) :
```
create table test(
user_action_time TIMESTAMP(3)
-- 定义user_action_time为事件时间,最大延迟为5秒钟。
watermark for user_action_time as user_action_time - interval '5' second
) with(..,)
```
定义event time的ddl语法可以使用函数和计算(放回类型必须为timestamp(3)),这使得定义time attr更加方便,更好些。
一个非timestamp类也可以通过计算过程,将其定义为time attr。
该语法定义的watermark发送规则:当前记录返回的水印不为null并且大于上一次发出的水印,那边就发送水印。水印
的发送间隔有pipeline.auto-watermark-interval参数控制,默认0(生成的水印如果不为空且大于上一个发出的水印,立即发送)
watermark的策略:
```
# 时间严格增加,代表数据不会延迟。
WATERMARK FOR rowtime_column AS rowtime_column.
# 指定最大延迟。
WATERMARK FOR rowtime_column AS rowtime_column - INTERVAL 'string' timeUnit.
```
## 1.11 新增的语法
### 定义pk
```
create table test(
primary key (id, name) not enforced
) with()
```
flink的主键约束应该必须定义NOT ENFORCED(flink不包含数据,表示非强制性)。
flink的主键约束列不能为null。
在table schema中通过如下方法来获取主键:
```
schema.getPrimaryKey()
```
在flink定义主键只是一个语法糖,在sink端的主键约束需要connector来保证。flink本身
无法保证主键约束的正确性。 在我们平台是通过`connector.write.unique-key`属性来传递主键,
显示ddl定义主键更加简单、方便。
### 定义分区字段
通过PARTITIONED BY可以定义分区字段。
```
create table flink_sink (
ts timestamp,
id string ,
name string
) partitioned by (dt string)
with(...)
```
主要,如果partitioned 的字段没有在schema出现过,需要定义类型,他的顺序是在schema定义的自动后面。
在执行insert into的时候要注意。
主要:分区信息通常用于文件系统。 每个分区创建一个目录。
### like 语法
基于现有的表定义表。
like 语法将合并schema定义和with参数。
```
set checkpoint.checkpointInterval = 10000;
create table source (
ts as proctime() ,
id varchar,
name varchar,
xx as concat(id, name)
) with (
'connector.type' = 'sys-file',
'connector.path' = 'tmp/data.log',
'format.type' = 'json',
'format.derive-schema' = 'true'
);
create table sink
with (
'connector.path' = 'tmp/xx',
'connector.format' = 'text',
'connector.compress' = 'none'
) like source;
insert into sink select id, name from source;
```
注意: like 语法创建的表,schema不包含计算列表。如上案例中, ts,xx字段均不会复制到sink中。
可以使用如下选项来控制合并参数的类型:
选项 | 说明
--- | ---
constraints | 复制主键和唯一键
generated | 复制计算列
options | 复制连接器声明属性
partitions | 复制分区属性
watermarks | 复制水印声明
all | 全部属性。
在选项合并中,有3中策略:
策略 | 说明
--- | ---
including | 包含原表的选项。如果存在相同的定义,会报错。
excluding | 排除原表的选项。
overwriting | 覆盖原表的选项。存在相同的定义,使用当前的定义。
如果没有定义合并策略,将使用INCLUDING ALL OVERWRITING OPTIONS作用合并策略。但是不包含计算列。
like的选项有2行,第一部分声明schema合并方式,第二部分声明options合并方式(time attr、计算列属于options)
第一部分可选: { including | excluding } { all | constraints | partitions }
第二部分可选:{ including | excluding | overwriting } { generated | options | watermarks }
问题:
1. 字段必须全部合并。你不能选择合并那些字段。
2. 计算列和字段不能同时存在。
### 动态表选项
动态表选项允许在查询的适合动态指定参数。
比如:
```
-- 可以指定在查询的适合忽略csv解析错误。
/*+ OPTIONS('csv.ignore-parse-errors'='true') */
```
主要动态选项默认是禁止的。通过table.dynamic-table-options参数设置为true来开启(默认false)
举例:
```
CREATE TABLE kafka_table1 (id BIGINT, name STRING, age INT) WITH (...);
CREATE TABLE kafka_table2 (id BIGINT, name STRING, age INT) WITH (...);
-- override table options in query source
select id, name from kafka_table1 /*+ OPTIONS('scan.startup.mode'='earliest-offset') */;
-- override table options in join
select * from
kafka_table1 /*+ OPTIONS('scan.startup.mode'='earliest-offset') */ t1
join
kafka_table2 /*+ OPTIONS('scan.startup.mode'='earliest-offset') */ t2
on t1.id = t2.id;
-- override table options for INSERT target table
insert into kafka_table1 /*+ OPTIONS('sink.partitioner'='round-robin') */ select * from kafka_table2;
```
## 新的table 接口
老的table接口,在定义source、sink的时候比较麻烦。问题如下:
1. sink分为append、upsert、retract3中模式。其中append、upsert模式在sql生成查询计划的时候,
会进行校验,如果流的类型和sink类型不匹配,会报错。比如sink为upsert流,必须有group 字段。官方连接器都是
实现了append或者upsert流,多数业务场景下都无法使用,只能自己实现retract流。
2. 接口使用了builder模式,传递参数非常的麻烦,修改要同时修改多个地方。
3. 使用time attr比较麻烦
4. Row类型没有getter方法,必须强制类型转换。
5. retract消息使用tuple2标识,在row没有表示是否为回撤消息。导致处理不方便。
新的DynamicTable系列接口,解决了上面的一些问题。
查询类型产生的消息类型使用接口通知connector,不在是强制校验,由connector自己决定如何处理各种消息。
```
// requestedMode 包含可能产生的消息类型。由sink接口自己决定可以接受那些消息。
// flink planer不在强制校验。
@Override
public ChangelogMode getChangelogMode(ChangelogMode requestedMode) {
validatePrimaryKey(requestedMode);
return ChangelogMode.newBuilder()
.addContainedKind(RowKind.INSERT)
.addContainedKind(RowKind.DELETE)
.addContainedKind(RowKind.UPDATE_AFTER)
.build();
}
```
新的接口使用RowData传递数据,RowData包含了消息的类型,如下:
类型|标识符|说明
--- | --- |---
RowKind#INSERT | +I | insert消息
RowKind#UPDATE_BEFORE | -U | 更新前的消息
RowKind#UPDATE_AFTER | +U | 更新后的消息
RowKind#DELETE | -D | 删除消息
新的接口确实不错,但是很多高级特性没有实现,在生产任然建议使用老的接口。
基于新的接口连接器,任然有如下问题:
1. kafka sink只支持append
2. hive sink 只支持append,并且orc、parquet不支持复合数据类型。
## 后记
我们已经完成flink 1.11版本的代码合并。
但是flink 1.11新的table接口不完善,所以平台不会发布1.11版本。
我们会基于新的接口开发新的source、sink等待flink的下一个版本。