flink 1.11新特性


## 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的下一个版本。

你可能感兴趣的:(flink)