史上最全干货!Flink SQL 成神之路(四)
作者 | antigeneral了呀
来源 | 大数据羊说(ID:young_say)
作者 | antigeneral了呀
4.SQL 能力扩展篇
4.1.SQL UDF 扩展 - Module
在介绍 Flink Module 具体能力之前,我们先来聊聊博主讲述的思路:
- ⭐ 背景及应用场景介绍
- ⭐ Flink Module 功能介绍
- ⭐ 应用案例:Flink SQL 支持 Hive UDF
4.1.1.Flink SQL Module 应用场景
兄弟们,想想其实大多数公司都是从离线数仓开始建设的。相信大家必然在自己的生产环境中开发了非常多的 Hive UDF。随着需求对于时效性要求的增高,越来越多的公司也开始建设起实时数仓。很多场景下实时数仓的建设都是随着离线数仓而建设的。实时数据使用 Flink 产出,离线数据使用 Hive/Spark 产出。
那么回到我们的问题:为什么需要给 Flink UDF 做扩展呢?可能这个问题比较大,那么博主分析的具体一些,如果 Flink 扩展支持 Hive UDF 对我们有哪些好处呢?
博主分析了下,结论如下:
站在数据需求的角度来说,一般会有以下两种情况:
- ⭐ 以前已经有了离线数据链路,需求方也想要实时数据。如果直接能用已经开发好的 hive udf,则不用将相同的逻辑迁移到 flink udf 中,并且后续无需费时费力维护两个 udf 的逻辑一致性。
- ⭐ 实时和离线的需求都是新的,需要新开发。如果只开发一套 UDF,则事半功倍。
因此在 Flink 中支持 Hive UDF(也即扩展 Flink 的 UDF 能力)这件事对开发人员提效来说是非常有好处的。
4.1.2.Flink SQL Module 功能介绍
Module 允许 Flink 扩展函数能力。它是可插拔的,Flink 官方本身已经提供了一些 Module,用户也可以编写自己的 Module。
例如,用户可以定义自己的函数,并将其作为加载进入 Flink,以在 Flink SQL 和 Table API 中使用。
再举一个例子,用户可以加载官方已经提供的的 Hive Module,将 Hive 已有的内置函数作为 Flink 的内置函数。
目前 Flink 包含了以下三种 Module:
- ⭐ CoreModule:CoreModule 是 Flink 内置的 Module,其包含了目前 Flink 内置的所有 UDF,Flink 默认开启的 Module 就是 CoreModule,我们可以直接使用其中的 UDF
- ⭐ HiveModule:HiveModule 可以将 Hive 内置函数作为 Flink 的系统函数提供给 SQL\Table API 用户进行使用,比如 get_json_object 这类 Hive 内置函数(Flink 默认的 CoreModule 是没有的)
- ⭐ 用户自定义 Module:用户可以实现 Module 接口实现自己的 UDF 扩展 Module
在 Flink 中,Module 可以被 加载
、启用
、禁用
、卸载
Module,当 TableEnvironment 加载(见 SQL 语法篇的 Load Module) Module 之后,默认就是开启的。
Flink 是同时支持多个 Module 的,并且根据加载 Module 的顺序去按顺序查找和解析 UDF,先查到的先解析使用。
此外,Flink 只会解析已经启用了的 Module。那么当两个 Module 中出现两个同名的函数时,会有以下三种情况:
- ⭐ 如果两个 Module 都启用的话,Flink 会根据加载 Module 的顺序进行解析,结果就是会使用顺序为第一个的 Module 的 UDF
- ⭐ 如果只有一个 Module 启用的话,Flink 就只会从启用的 Module 解析 UDF
- ⭐ 如果两个 Module 都没有启用,Flink 就无法解析这个 UDF
当然如果出现第一种情况时,用户也可以改变使用 Module 的顺序。比如用户可以使用 USE MODULE hive, core
语句去将 Hive Module 设为第一个使用及解析的 Module。
另外,用户可以使用 USE MODULES hive
去禁用默认的 core Module,注意,禁用不是卸载 Module,用户之后还可以再次启用 Module,并且使用 USE MODULES core
去将 core Module 设置为启用的。如果使用未加载的 Module,则会直接抛出异常。
禁用和卸载 Module 的区别在于禁用依然会在 TableEnvironment 保留 Module,用户依然可以使用使用 list 命令看到禁用的 Module。
注意:
由于 Module 的 UDF 是被 Flink 认为是 Flink 系统内置的,它不和任何 Catalog,数据库绑定,所以这部分 UDF 没有对应的命名空间,即没有 Catalog,数据库命名空间。
- ⭐ 使用 SQL API 加载、卸载、使用、列出 Module
EnvironmentSettings settings = EnvironmentSettings.newInstance().useBlinkPlanner().build();
TableEnvironment tableEnv = TableEnvironment.create(settings);
// 展示加载和启用的 Module
tableEnv.executeSql("SHOW MODULES").print();
// +-------------+
// | module name |
// +-------------+
// | core |
// +-------------+
tableEnv.executeSql("SHOW FULL MODULES").print();
// +-------------+------+
// | module name | used |
// +-------------+------+
// | core | true |
// +-------------+------+
// 加载 hive module
tableEnv.executeSql("LOAD MODULE hive WITH ('hive-version' = '...')");
// 展示所有启用的 module
tableEnv.executeSql("SHOW MODULES").print();
// +-------------+
// | module name |
// +-------------+
// | core |
// | hive |
// +-------------+
// 展示所有加载的 module 以及它们的启用状态
tableEnv.executeSql("SHOW FULL MODULES").print();
// +-------------+------+
// | module name | used |
// +-------------+------+
// | core | true |
// | hive | true |
// +-------------+------+
// 改变 module 解析顺序
tableEnv.executeSql("USE MODULES hive, core");
tableEnv.executeSql("SHOW MODULES").print();
// +-------------+
// | module name |
// +-------------+
// | hive |
// | core |
// +-------------+
tableEnv.executeSql("SHOW FULL MODULES").print();
// +-------------+------+
// | module name | used |
// +-------------+------+
// | hive | true |
// | core | true |
// +-------------+------+
// 禁用 core module
tableEnv.executeSql("USE MODULES hive");
tableEnv.executeSql("SHOW MODULES").print();
// +-------------+
// | module name |
// +-------------+
// | hive |
// +-------------+
tableEnv.executeSql("SHOW FULL MODULES").print();
// +-------------+-------+
// | module name | used |
// +-------------+-------+
// | hive | true |
// | core | false |
// +-------------+-------+
// 卸载 hive module
tableEnv.executeSql("UNLOAD MODULE hive");
tableEnv.executeSql("SHOW MODULES").print();
// Empty set
tableEnv.executeSql("SHOW FULL MODULES").print();
// +-------------+-------+
// | module name | used |
// +-------------+-------+
// | hive | false |
// +-------------+-------+
- ⭐ 使用 Java API 加载、卸载、使用、列出 Module
EnvironmentSettings settings = EnvironmentSettings.newInstance().useBlinkPlanner().build();
TableEnvironment tableEnv = TableEnvironment.create(settings);
// Show initially loaded and enabled modules
tableEnv.listModules();
// +-------------+
// | module name |
// +-------------+
// | core |
// +-------------+
tableEnv.listFullModules();
// +-------------+------+
// | module name | used |
// +-------------+------+
// | core | true |
// +-------------+------+
// Load a hive module
tableEnv.loadModule("hive", new HiveModule());
// Show all enabled modules
tableEnv.listModules();
// +-------------+
// | module name |
// +-------------+
// | core |
// | hive |
// +-------------+
// Show all loaded modules with both name and use status
tableEnv.listFullModules();
// +-------------+------+
// | module name | used |
// +-------------+------+
// | core | true |
// | hive | true |
// +-------------+------+
// Change resolution order
tableEnv.useModules("hive", "core");
tableEnv.listModules();
// +-------------+
// | module name |
// +-------------+
// | hive |
// | core |
// +-------------+
tableEnv.listFullModules();
// +-------------+------+
// | module name | used |
// +-------------+------+
// | hive | true |
// | core | true |
// +-------------+------+
// Disable core module
tableEnv.useModules("hive");
tableEnv.listModules();
// +-------------+
// | module name |
// +-------------+
// | hive |
// +-------------+
tableEnv.listFullModules();
// +-------------+-------+
// | module name | used |
// +-------------+-------+
// | hive | true |
// | core | false |
// +-------------+-------+
// Unload hive module
tableEnv.unloadModule("hive");
tableEnv.listModules();
// Empty set
tableEnv.listFullModules();
// +-------------+-------+
// | module name | used |
// +-------------+-------+
// | hive | false |
// +-------------+-------+
4.1.3.应用案例:Flink SQL 支持 Hive UDF
Flink 支持 hive UDF 这件事分为两个部分。
- ⭐ Flink 扩展支持 hive 内置 UDF
- ⭐ Flink 扩展支持用户自定义 hive UDF
第一部分:Flink 扩展支持 Hive 内置 UDF,比如 get_json_object
,rlike
等等。
有同学问了,这么基本的 UDF,Flink 都没有吗?
确实没有。关于 Flink SQL 内置的 UDF 见如下链接,大家可以看看 Flink 支持了哪些 UDF:https://nightlies.apache.org/flink/flink-docs-release-1.13/docs/dev/table/functions/systemfunctions/
那么如果我如果强行使用 get_json_object 这个 UDF,会发生啥呢?结果如下图。
直接报错找不到 UDF。
error
第二部分:Flink 扩展支持用户自定义 Hive UDF。
内置函数解决不了用户的复杂需求,用户就需要自己写 Hive UDF,并且这部分自定义 UDF 也想在 flink sql 中使用。
下面看看怎么在 Flink SQL 中进行这两种扩展。
- ⭐ flink 扩展支持 hive 内置 udf
步骤如下:
- ⭐ 引入 hive 的 connector。其中包含了 flink 官方提供的一个
HiveModule
。在HiveModule
中包含了 hive 内置的 udf。
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-hive_${scala.binary.version}</artifactId>
<version>${flink.version}</version>
</dependency>
- ⭐ 在
StreamTableEnvironment
中加载HiveModule
。
String name = "default";
String version = "3.1.2";
tEnv.loadModule(name, new HiveModule(version));
然后在控制台打印一下目前有的 module。
String[] modules = tEnv.listModules();
Arrays.stream(modules).forEach(System.out::println);
然后可以看到除了 core
module,还有我们刚刚加载进去的 default
module。
default
core
- ⭐ 查看所有 module 的所有 udf。在控制台打印一下。
String[] functions = tEnv.listFunctions();
Arrays.stream(functions).forEach(System.out::println);
就会将 default 和 core module 中的所有包含的 udf 给列举出来,当然也就包含了 hive module 中的 get_json_object。
get_json_object
然后我们再去在 Flink SQL 中使用 get_json_object 这个 UDF,就没有报错,能正常输出结果了。
使用 Flink Hive connector 自带的 HiveModule
,已经能够解决很大一部分常见 UDF 使用的问题了。
- ⭐ Flink 扩展支持用户自定义 Hive UDF
原本博主是直接想要使用 Flink SQL 中的 create temporary function
去执行引入自定义 Hive UDF 的。
举例如下:
CREATE TEMPORARY FUNCTION test_hive_udf as 'flink.examples.sql._09.udf._02_stream_hive_udf.TestGenericUDF';
发现在执行这句 SQL 时,是可以执行成功,将 UDF 注册进去的。
但是在后续 UDF 初始化时就报错了。具体错误如下图。直接报错 ClassCastException。
ddl hive udf error
看了下源码,Flink 流任务模式下(未连接 Hive MetaStore 时)在创建 UDF 时会认为这个 UDF 是 Flink 生态体系中的 UDF。
所以在初始化我们引入的 TestGenericUDF
时,默认会按照 Flink 的 UserDefinedFunction
强转,因此才会报强转错误。
那么我们就不能使用 Hive UDF 了吗?
错误,小伙伴萌岂敢有这种想法。博主都把这个标题列出来了(牛逼都吹出去了),还能给不出解决方案嘛。
思路见下一节。
- ⭐ Flink 扩展支持用户自定义 Hive UDF 的增强 module
其实思路很简单。
使用 Flink SQL 中的 create temporary function
虽然不能执行,但是 Flink 提供了插件化的自定义 module。
我们可以扩展一个支持用户自定义 Hive UDF 的 module,使用这个 module 来支持自定义的 Hive UDF。
实现的代码也非常简单。简单的把 Flink Hive connector 提供的 HiveModule
做一个增强即可,即下图中的 HiveModuleV2
。使用方式如下图所示:
hive module enhance
然后程序就正常跑起来了。
肥肠滴好用!
4.2.SQL 元数据扩展 - Catalog
4.2.1.Flink Catalog 功能介绍
数据处理最关键的方面之一是管理元数据。元数据可以是临时的,例如临时表、UDF。元数据也可以是持久化的,例如 Hive MetaStore 中的元数据。
Flink SQL 中是由 Catalog 提供了元数据信息,例如数据库、表、分区、视图以及数据库或其他外部系统中存储的函数和信息。对标 Hive 去理解就是 Hive 的 MetaStore,都是用于存储计算引擎涉及到的元数据信息。
Catalog 允许用户引用其数据存储系统中现有的元数据,并自动将其映射到 Flink 的相应元数据。例如,Flink 可以直接使用 Hive MetaStore 中的表的元数据,也可以将 Flink SQL 中的元数据存储到 Hive MetaStore 中。Catalog 极大地简化了用户开始使用 Flink 的步骤,提升了用户体验。
目前 Flink 包含了以下四种 Catalog:
- ⭐ GenericInMemoryCatalog:GenericInMemoryCatalog 是基于内存实现的 Catalog,所有元数据只在 session 的生命周期(即一个 Flink 任务一次运行生命周期内)内可用。
- ⭐ JdbcCatalog:JdbcCatalog 使得用户可以将 Flink 通过 JDBC 协议连接到关系数据库。PostgresCatalog 是当前实现的唯一一种 JDBC Catalog,即可以将 Flink SQL 的预案数据存储在 Postgres 中。
// PostgresCatalog 方法支持的方法
PostgresCatalog.databaseExists(String databaseName)
PostgresCatalog.listDatabases()
PostgresCatalog.getDatabase(String databaseName)
PostgresCatalog.listTables(String databaseName)
PostgresCatalog.getTable(ObjectPath tablePath)
PostgresCatalog.tableExists(ObjectPath tablePath)
- ⭐ HiveCatalog:HiveCatalog 有两个用途,作为 Flink 元数据的持久化存储,以及作为读写现有 Hive 元数据的接口。注意:Hive MetaStore 以小写形式存储所有元数据对象名称。而 GenericInMemoryCatalog 会区分大小写。
TableEnvironment tableEnv = TableEnvironment.create(settings);
String name = "myhive";
String defaultDatabase = "mydatabase";
String hiveConfDir = "/opt/hive-conf";
HiveCatalog hive = new HiveCatalog(name, defaultDatabase, hiveConfDir);
tableEnv.registerCatalog("myhive", hive);
// set the HiveCatalog as the current catalog of the session
tableEnv.useCatalog("myhive");
- ⭐ 用户自定义 Catalog:用户可以实现 Catalog 接口实现自定义 Catalog
下面看看 Flink Catalog 提供了什么 API,以及对应 API 的使用案例:
- ⭐ 使用 SQL API 将表创建注册进 Catalog
TableEnvironment tableEnv = ...
// 创建 HiveCatalog
Catalog catalog = new HiveCatalog("myhive", null, "<path_of_hive_conf>");
// 注册 catalog
tableEnv.registerCatalog("myhive", catalog);
// 在 catalog 中创建 database
tableEnv.executeSql("CREATE DATABASE mydb WITH (...)");
// 在 catalog 中创建表
tableEnv.executeSql("CREATE TABLE mytable (name STRING, age INT) WITH (...)");
tableEnv.listTables(); // 列出当前 myhive.mydb 中的所有表
- ⭐ 使用 Java API 将表创建注册进 Catalog
import org.apache.flink.table.api.*;
import org.apache.flink.table.catalog.*;
import org.apache.flink.table.catalog.hive.HiveCatalog;
import org.apache.flink.table.descriptors.Kafka;
TableEnvironment tableEnv = TableEnvironment.create(EnvironmentSettings.newInstance().build());
// 创建 HiveCatalog
Catalog catalog = new HiveCatalog("myhive", null, "<path_of_hive_conf>");
// 注册 catalog
tableEnv.registerCatalog("myhive", catalog);
// 在 catalog 中创建 database
catalog.createDatabase("mydb", new CatalogDatabaseImpl(...));
// 在 catalog 中创建表
TableSchema schema = TableSchema.builder()
.field("name", DataTypes.STRING())
.field("age", DataTypes.INT())
.build();
catalog.createTable(
new ObjectPath("mydb", "mytable"),
new CatalogTableImpl(
schema,
new Kafka()
.version("0.11")
....
.startFromEarlist()
.toProperties(),
"my comment"
),
false
);
List<String> tables = catalog.listTables("mydb"); // 列出当前 myhive.mydb 中的所有表
4.2.2.操作 Catalog 的 API
这里只列出了 Java 的 Catalog API,用户也可以使用 SQL DDL API 实现相同的功能。关于 DDL 的详细信息请参考之前介绍到的 SQL CREATE DDL 章节。
- ⭐ Catalog 操作
// 注册 Catalog
tableEnv.registerCatalog(new CustomCatalog("myCatalog"));
// 切换 Catalog 和 Database
tableEnv.useCatalog("myCatalog");
tableEnv.useDatabase("myDb");
// 也可以通过以下方式访问对应的表
tableEnv.from("not_the_current_catalog.not_the_current_db.my_table");
- ⭐ 数据库操作
// create database
catalog.createDatabase("mydb", new CatalogDatabaseImpl(...), false);
// drop database
catalog.dropDatabase("mydb", false);
// alter database
catalog.alterDatabase("mydb", new CatalogDatabaseImpl(...), false);
// get databse
catalog.getDatabase("mydb");
// check if a database exist
catalog.databaseExists("mydb");
// list databases in a catalog
catalog.listDatabases("mycatalog");
- ⭐ 表操作
// create table
catalog.createTable(new ObjectPath("mydb", "mytable"), new CatalogTableImpl(...), false);
// drop table
catalog.dropTable(new ObjectPath("mydb", "mytable"), false);
// alter table
catalog.alterTable(new ObjectPath("mydb", "mytable"), new CatalogTableImpl(...), false);
// rename table
catalog.renameTable(new ObjectPath("mydb", "mytable"), "my_new_table");
// get table
catalog.getTable("mytable");
// check if a table exist or not
catalog.tableExists("mytable");
// list tables in a database
catalog.listTables("mydb");
- ⭐ 视图操作
// create view
catalog.createTable(new ObjectPath("mydb", "myview"), new CatalogViewImpl(...), false);
// drop view
catalog.dropTable(new ObjectPath("mydb", "myview"), false);
// alter view
catalog.alterTable(new ObjectPath("mydb", "mytable"), new CatalogViewImpl(...), false);
// rename view
catalog.renameTable(new ObjectPath("mydb", "myview"), "my_new_view", false);
// get view
catalog.getTable("myview");
// check if a view exist or not
catalog.tableExists("mytable");
// list views in a database
catalog.listViews("mydb");
- ⭐ 分区操作
// create view
catalog.createPartition(
new ObjectPath("mydb", "mytable"),
new CatalogPartitionSpec(...),
new CatalogPartitionImpl(...),
false);
// drop partition
catalog.dropPartition(new ObjectPath("mydb", "mytable"), new CatalogPartitionSpec(...), false);
// alter partition
catalog.alterPartition(
new ObjectPath("mydb", "mytable"),
new CatalogPartitionSpec(...),
new CatalogPartitionImpl(...),
false);
// get partition
catalog.getPartition(new ObjectPath("mydb", "mytable"), new CatalogPartitionSpec(...));
// check if a partition exist or not
catalog.partitionExists(new ObjectPath("mydb", "mytable"), new CatalogPartitionSpec(...));
// list partitions of a table
catalog.listPartitions(new ObjectPath("mydb", "mytable"));
// list partitions of a table under a give partition spec
catalog.listPartitions(new ObjectPath("mydb", "mytable"), new CatalogPartitionSpec(...));
// list partitions of a table by expression filter
catalog.listPartitionsByFilter(new ObjectPath("mydb", "mytable"), Arrays.asList(epr1, ...));
- ⭐ 函数操作
// create function
catalog.createFunction(new ObjectPath("mydb", "myfunc"), new CatalogFunctionImpl(...), false);
// drop function
catalog.dropFunction(new ObjectPath("mydb", "myfunc"), false);
// alter function
catalog.alterFunction(new ObjectPath("mydb", "myfunc"), new CatalogFunctionImpl(...), false);
// get function
catalog.getFunction("myfunc");
// check if a function exist or not
catalog.functionExists("myfunc");
// list functions in a database
catalog.listFunctions("mydb");
4.3.SQL 任务参数配置
关于 Flink SQL 详细的配置项及功能如下链接所示,详细内容大家可以点击链接去看,博主下面只介绍常用的性能优化参数及其功能:
https://nightlies.apache.org/flink/flink-docs-release-1.13/docs/dev/table/config/
4.3.1.参数设置方式
Flink SQL 相关参数需要在 TableEnvironment 中设置。如下案例:
// instantiate table environment
TableEnvironment tEnv = ...
// access flink configuration
Configuration configuration = tEnv.getConfig().getConfiguration();
// set low-level key-value options
configuration.setString("table.exec.mini-batch.enabled", "true");
configuration.setString("table.exec.mini-batch.allow-latency", "5 s");
configuration.setString("table.exec.mini-batch.size", "5000");
具体参数分为以下 3 类:
- ⭐ 运行时参数:优化 Flink SQL 任务在执行时的任务性能
- ⭐ 优化器参数:Flink SQL 任务在生成执行计划时,经过优化器优化生成更优的执行计划
- ⭐ 表参数:用于调整 Flink SQL table 的执行行为
4.3.2.运行时参数
用于优化 Flink SQL 任务在执行时的任务性能。
// 默认值:100
// 值类型:Integer
// 流批任务:流、批任务都支持
// 用处:异步 lookup join 中最大的异步 IO 执行数目
table.exec.async-lookup.buffer-capacity: 100
// 默认值:false
// 值类型:Boolean
// 流批任务:流任务支持
// 用处:MiniBatch 优化是一种专门针对 unbounded 流任务的优化(即非窗口类应用),其机制是在 `允许的延迟时间间隔内` 以及 `达到最大缓冲记录数` 时触发以减少 `状态访问` 的优化,从而节约处理时间。下面两个参数一个代表 `允许的延迟时间间隔`,另一个代表 `达到最大缓冲记录数`。
table.exec.mini-batch.enabled: false
// 默认值:0 ms
// 值类型:Duration
// 流批任务:流任务支持
// 用处:此参数设置为多少就代表 MiniBatch 机制最大允许的延迟时间。注意这个参数要配合 `table.exec.mini-batch.enabled` 为 true 时使用,而且必须大于 0 ms
table.exec.mini-batch.allow-latency: 0 ms
// 默认值:-1
// 值类型:Long
// 流批任务:流任务支持
// 用处:此参数设置为多少就代表 MiniBatch 机制最大缓冲记录数。注意这个参数要配合 `table.exec.mini-batch.enabled` 为 true 时使用,而且必须大于 0
table.exec.mini-batch.size: -1
// 默认值:-1
// 值类型:Integer
// 流批任务:流、批任务都支持
// 用处:可以用此参数设置 Flink SQL 中算子的并行度,这个参数的优先级 `高于` StreamExecutionEnvironment 中设置的并行度优先级,如果这个值设置为 -1,则代表没有设置,会默认使用 StreamExecutionEnvironment 设置的并行度
table.exec.resource.default-parallelism: -1
// 默认值:ERROR
// 值类型:Enum【ERROR, DROP】
// 流批任务:流、批任务都支持
// 用处:表上的 NOT NULL 列约束强制不能将 NULL 值插入表中。Flink 支持 `ERROR`(默认)和 `DROP` 配置。默认情况下,当 NULL 值写入 NOT NULL 列时,Flink 会产生运行时异常。用户可以将行为更改为 `DROP`,直接删除此类记录,而不会引发异常。
table.exec.sink.not-null-enforcer: ERROR
// 默认值:false
// 值类型:Boolean
// 流批任务:流任务
// 用处:接入了 CDC 的数据源,上游 CDC 如果产生重复的数据,可以使用此参数在 Flink 数据源算子进行去重操作,去重会引入状态开销
table.exec.source.cdc-events-duplicate: false
// 默认值:0 ms
// 值类型:Duration
// 流批任务:流任务
// 用处:如果此参数设置为 60 s,当 Source 算子在 60 s 内未收到任何元素时,这个 Source 将被标记为临时空闲,此时下游任务就不依赖此 Source 的 Watermark 来推进整体的 Watermark 了。
// 默认值为 0 时,代表未启用检测源空闲。
table.exec.source.idle-timeout: 0 ms
// 默认值:0 ms
// 值类型:Duration
// 流批任务:流任务
// 用处:指定空闲状态(即未更新的状态)将保留多长时间。尤其是在 unbounded 场景中很有用。默认 0 ms 为不清除空闲状态
table.exec.state.ttl: 0 ms
其中上述参数中最常被用到为一下两种:
- ⭐ MiniBatch 聚合
table.exec.mini-batch.enabled: true
table.exec.mini-batch.allow-latency: 60 s
table.exec.mini-batch.size: 1000000000
具体使用场景如下链接:
- ⭐ state ttl 状态过期
-- 状态清除如下流 SQL 案例场景很有用,随着实时任务的运行,前几天(即前几天的 p_date)的 state 不会被更新的情况下,就可以使用空闲状态删除机制把 state 给删除
select
p_date
, count(distinct user_id) as uv
from source_table
group
p_date
4.3.3.优化器参数
Flink SQL 任务在生成执行计划时,优化生成更优的执行计划
// 默认值:AUTO
// 值类型:String
// 流批任务:流、批任务都支持
// 用处:聚合阶段的策略。和 MapReduce 的 Combiner 功能类似,可以在数据 shuffle 前做一些提前的聚合,可以选择以下三种方式
// TWO_PHASE:强制使用具有 localAggregate 和 globalAggregate 的两阶段聚合。请注意,如果聚合函数不支持优化为两个阶段,Flink 仍将使用单阶段聚合。
// 两阶段优化在计算 count,sum 时很有用,但是在计算 count distinct 时需要注意,key 的稀疏程度,如果 key 不稀疏,那么很可能两阶段优化的效果会适得其反
// ONE_PHASE:强制使用只有 CompleteGlobalAggregate 的一个阶段聚合。
// AUTO:聚合阶段没有特殊的执行器。选择 TWO_PHASE 或者 ONE_PHASE 取决于优化器的成本。
//
// 注意!!!:此优化在窗口聚合中会自动生效,但是在 unbounded agg 中需要与 minibatch 参数相结合使用才会生效
table.optimizer.agg-phase-strategy: AUTO
// 默认值:false
// 值类型:Boolean
// 流批任务:流任务
// 用处:避免 group by 计算 count distinct\sum distinct 数据时的 group by 的 key 较少导致的数据倾斜,比如 group by 中一个 key 的 distinct 要去重 500w 数据,而另一个 key 只需要去重 3 个 key,那么就需要先需要按照 distinct 的 key 进行分桶。将此参数设置为 true 之后,下面的 table.optimizer.distinct-agg.split.bucket-num 可以用于决定分桶数是多少
// 后文会介绍具体的案例
table.optimizer.distinct-agg.split.enabled: false
// 默认值:1024
// 值类型:Integer
// 流批任务:流任务
// 用处:避免 group by 计算 count distinct 数据时的 group by 较少导致的数据倾斜。加了此参数之后,会先根据 group by key 结合 hash_code(distinct_key)进行分桶,然后再自动进行合桶。
// 后文会介绍具体的案例
table.optimizer.distinct-agg.split.bucket-num: 1024
// 默认值:true
// 值类型:Boolean
// 流批任务:流任务
// 用处:如果设置为 true,Flink 优化器将会尝试找出重复的自计划并重用。默认为 true 不需要改动
table.optimizer.reuse-sub-plan-enabled: true
// 默认值:true
// 值类型:Boolean
// 流批任务:流任务
// 用处:如果设置为 true,Flink 优化器会找出重复使用的 table source 并且重用。默认为 true 不需要改动
table.optimizer.reuse-source-enabled: true
// 默认值:true
// 值类型:Boolean
// 流批任务:流任务
// 用处:如果设置为 true,Flink 优化器将会做谓词下推到 FilterableTableSource 中,将一些过滤条件前置,提升性能。默认为 true 不需要改动
table.optimizer.source.predicate-pushdown-enabled: true
其中上述参数中最常被用到为以下两种:
- ⭐ 两阶段优化:
table.optimizer.agg-phase-strategy: AUTO
在计算 count(1),sum(col) 场景汇总提效很高,因为 count(1),sum(col) 在经过本地 localAggregate 之后,每个 group by 的 key 就一个结果值。
注意!!!:此优化在窗口聚合中会自动生效,但是在 unbounded agg 中需要与 minibatch 参数相结合使用才会生效。
- ⭐ split 分桶:
table.optimizer.distinct-agg.split.enabled: true
table.optimizer.distinct-agg.split.bucket-num: 1024
INSERT INTO sink_table
SELECT
count(distinct user_id) as uv,
max(cast(server_timestamp as bigint)) as server_timestamp
FROM source_table
-- 上述 SQL 打开了 split 分桶之后的效果等同于以下 SQL
INSERT INTO sink_table
SELECT
sum(bucket_uv) as uv
, max(server_timestamp) as server_timestamp
FROM (
SELECT
count(distinct user_id) as bucket_uv,
max(cast(server_timestamp as bigint)) as server_timestamp
FROM source_table
group by
mod(hash_code(user_id), 1024)
)
注意!!!:如果有多个 distinct key,则多个 distinct key 都会被作为分桶 key。
4.3.4.表参数
// 默认值:false
// 值类型:Boolean
// 流批任务:流、批任务都支持
// 用处:DML SQL(即执行 insert into 操作)是异步执行还是同步执行。默认为异步(false),即可以同时提交多个 DML SQL 作业,如果设置为 true,则为同步,第二个 DML 将会等待第一个 DML 操作执行结束之后再执行
table.dml-sync: false
// 默认值:64000
// 值类型:Integer
// 流批任务:流、批任务都支持
// 用处:Flink SQL 会通过生产 java 代码来执行具体的 SQL 逻辑,但是 jvm 限制了一个 java 方法的最大长度不能超过 64KB,但是某些场景下 Flink SQL 生产的 java 代码会超过 64KB,这时 jvm 就会直接报错。因此此参数可以用于限制生产的 java 代码的长度来避免超过 64KB,从而避免 jvm 报错。
table.generated-code.max-length: 64000
// 默认值:default
// 值类型:String
// 流批任务:流、批任务都支持
// 用处:在使用天级别的窗口时,通常会遇到时区问题。举个例子,Flink 开一天的窗口,默认是按照 UTC 零时区进行划分,那么在北京时区划分出来的一天的窗口是第一天的早上 8:00 到第二天的早上 8:00,但是实际场景中想要的效果是第一天的早上 0:00 到第二天的早上 0:00 点。因此可以将此参数设置为 GMT+08:00 来解决这个问题。
table.local-time-zone: default
// 默认值:default
// 值类型:Enum【BLINK、OLD】
// 流批任务:流、批任务都支持
// 用处:Flink SQL planner,默认为 BLINK planner,也可以选择 old planner,但是推荐使用 BLINK planner
table.planner: BLINK
// 默认值:default
// 值类型:String
// 流批任务:流、批任务都支持
// 用处:Flink 解析一个 SQL 的解析器,目前有 Flink SQL 默认的解析器和 Hive SQL 解析器,其区别在于两种解析器支持的语法会有不同,比如 Hive SQL 解析器支持 between and、rlike 语法,Flink SQL 不支持
table.sql-dialect: default
4.4.SQL 性能调优
本小节主要介绍 Flink SQL 中的聚合算子的优化,在某些场景下应用这些优化后,性能提升会非常大。本小节主要包含以下四种优化:
- ⭐
(常用)
MiniBatch 聚合:unbounded group agg 中,可以使用 minibatch 聚合来做到微批计算、访问状态、输出结果,避免每来一条数据就计算、访问状态、输出一次结果,从而减少访问 state 的时长(尤其是 Rocksdb)提升性能。 - ⭐
(常用)
两阶段聚合:类似 MapReduce 中的 Combiner 的效果,可以先在 shuffle 数据之前先进行一次聚合,减少 shuffle 数据量 - ⭐
(不常用)
split 分桶:在 count distinct、sum distinct 的去重的场景中,如果出现数据倾斜,任务性能会非常差,所以如果先按照 distinct key 进行分桶,将数据打散到各个 TM 进行计算,然后将分桶的结果再进行聚合,性能就会提升很大 - ⭐
(常用)
去重 filter 子句:在 count distinct 中使用 filter 子句于 Hive SQL 中的 count(distinct if(xxx, user_id, null)) 子句,但是 state 中同一个 key 会按照 bit 位会进行复用,这对状态大小优化非常有用
上面简单介绍了聚合场景的四种优化,下面详细介绍一下其最终效果以及实现原理。
4.4.1.MiniBatch 聚合
- ⭐ 问题场景:默认情况下,unbounded agg 算子是逐条处理输入的记录,其处理流程如下:
- ⭐ 从状态中读取 accumulator;
- ⭐ 累加/撤回的数据记录至 accumulator;
- ⭐ 将 accumulator 写回状态;
- ⭐ 下一条记录将再次从流程 1 开始处理。
但是上述处理流程的问题在于会增加 StateBackend 的访问性能开销(尤其是对于 RocksDB StateBackend)。
- ⭐ MiniBatch 聚合如何解决上述问题:其核心思想是将一组输入的数据缓存在聚合算子内部的缓冲区中。当输入的数据被触发处理时,每个 key 只需要访问一次状态后端,这样可以大大减少访问状态的时间开销从而获得更好的吞吐量。但是,其会增加一些数据产出的延迟,因为它会缓冲一些数据再去处理。因此如果你要做这个优化,需要提前做一下吞吐量和延迟之间的权衡,但是大多数情况下,buffer 数据的延迟都是可以被接受的。所以非常建议在 unbounded agg 场景下使用这项优化。
下图说明了 MiniBatch 聚合如何减少状态访问的。
MiniBatch
上图展示了加 MiniBatch 和没加 MiniBatch 之前的执行区别。
- ⭐ 启用 MiniBatch 聚合的参数:
TableEnvironment tEnv = ...
Configuration configuration = tEnv.getConfig().getConfiguration();
configuration.setString("table.exec.mini-batch.enabled", "true"); // 启用 MiniBatch 聚合
configuration.setString("table.exec.mini-batch.allow-latency", "5 s"); // buffer 最多 5s 的输入数据记录
configuration.setString("table.exec.mini-batch.size", "5000"); // buffer 最多的输入数据记录数目
注意!!!
- ⭐
table.exec.mini-batch.allow-latency
和table.exec.mini-batch.size
两者只要其中一项满足条件就会执行 batch 访问状态操作。- ⭐ 上述 MiniBatch 配置不会对 Window TVF 生效,因为!!!Window TVF 默认就会启用小批量优化,Window TVF 会将 buffer 的输入记录记录在托管内存中,而不是 JVM 堆中,因此 Window TVF 不会有 GC 过高或者 OOM 的问题。
4.4.2.两阶段聚合
- ⭐ 问题场景:在聚合数据处理场景中,很可能会由于热点数据导致数据倾斜,如下 SQL 所示,当 color = RED 为 50000w 条,而 color = BLUE 为 5 条,就产生了数据倾斜,而器数据处理的算子产生性能瓶颈。
SELECT color, sum(id)
FROM T
GROUP BY color
- ⭐ 两阶段聚合如何解决上述问题:其核心思想类似于 MapReduce 中的 Combiner + Reduce,先将聚合操作在本地做一次 local 聚合,这样 shuffle 到下游的数据就会变少。
还是上面的 SQL 案例,如果在 50000w 条的 color = RED 的数据 shuffle 之前,在本地将 color = RED 的数据聚合成为 1 条结果,那么 shuffle 给下游的数据量就被极大地减少了。
下图说明了两阶段聚合是如何处理热点数据的:
两阶段聚合
- ⭐ 启用两阶段聚合的参数:
TableEnvironment tEnv = ...
Configuration configuration = tEnv.getConfig().getConfiguration();
configuration.setString("table.exec.mini-batch.enabled", "true"); // 打开 minibatch
configuration.setString("table.exec.mini-batch.allow-latency", "5 s");
configuration.setString("table.exec.mini-batch.size", "5000");
configuration.setString("table.optimizer.agg-phase-strategy", "TWO_PHASE"); // 打开两阶段聚合
注意!!!
- ⭐ 此优化在窗口聚合中会自动生效,大家在使用 Window TVF 时可以看到 localagg + globalagg 两部分
- ⭐ 但是在 unbounded agg 中需要与 MiniBatch 参数相结合使用才会生效。
4.4.3.split 分桶
- ⭐ 问题场景:使用两阶段聚合虽然能够很好的处理 count,sum 等常规聚合算子,但是在 count distinct,sum distinct 等算子的两阶段聚合效果在大多数场景下都不太满足预期。
因为 100w 条数据的 count 聚合能够在 local 算子聚合为 1 条数据,但是 count distinct 聚合 100w 条在 local 聚合之后的结果和可能是 90w 条,那么依然会有数据倾斜,如下 SQL 案例所示:
SELECT color, COUNT(DISTINCT user_id)
FROM T
GROUP BY color
- ⭐ split 分桶如何解决上述问题:其核心思想在于按照 distinct 的 key,即 user_id,先做数据的分桶,将数据打散,分散到 Flink 的多个 TM 上进行计算,然后再将数据合桶计算。打开 split 分桶之后的效果就等同于以下 SQL:
SELECT color, SUM(cnt)
FROM (
SELECT color, COUNT(DISTINCT user_id) as cnt
FROM T
GROUP BY color, MOD(HASH_CODE(user_id), 1024)
)
GROUP BY color
下图说明了 split 分桶的处理流程:
split 聚合
- ⭐ 启用 split 分桶的参数:
TableEnvironment tEnv = ...
tEnv.getConfig()
.getConfiguration()
.setString("table.optimizer.distinct-agg.split.enabled", "true"); // 打开 split 分桶
注意!!!
- ⭐ 如果有多个 distinct key,则多个 distinct key 都会被作为分桶 key。比如 count(distinct a),sum(distinct b) 这种多个 distinct key 也支持。
- ⭐ 小伙伴萌自己写的 UDAF 不支持!
- ⭐ 其实此种优化很少使用,因为大家直接自己按照分桶的写法自己就可以写了,而且最后生成的算子图和自己写的 SQL 的语法也能对应的上
4.4.4.去重 filter 子句
- ⭐ 问题场景:在一些场景下,用户可能需要从不同维度计算 UV,例如 Android 的 UV、iPhone 的 UV、Web 的 UV 和总 UV。许多用户会选择 CASE WHEN 支持此功能,如下 SQL 所示:
SELECT
day,
COUNT(DISTINCT user_id) AS total_uv,
COUNT(DISTINCT CASE WHEN flag IN ('android', 'iphone') THEN user_id ELSE NULL END) AS app_uv,
COUNT(DISTINCT CASE WHEN flag IN ('wap', 'other') THEN user_id ELSE NULL END) AS web_uv
FROM T
GROUP BY day
但是如果你想实现类似的效果,Flink SQL 提供了更好性能的写法,就是本小节的 filter 子句。
- ⭐ Filter 子句重写上述场景:
SELECT
day,
COUNT(DISTINCT user_id) AS total_uv,
COUNT(DISTINCT user_id) FILTER (WHERE flag IN ('android', 'iphone')) AS app_uv,
COUNT(DISTINCT user_id) FILTER (WHERE flag IN ('web', 'other')) AS web_uv
FROM T
GROUP BY day
Filter 子句的优化点在于,Flink 会识别出三个去重的 key 都是 user_id,因此会把三个去重的 key 存在一个共享的状态中。而不是上文 case when 中的三个状态中。其具体实现区别在于:
- ⭐ case when:total_uv、app_uv、web_uv 在去重时,state 是存在三个 MapState 中的,MapState key 为 user_id,value 为默认值,判断是否重复直接按照 key 是在 MapState 中的出现过进行判断。如果总 uv 为 1 亿,'android', 'iphone' uv 为 5kw,'wap', 'other' uv 为 5kw,则 3 个 state 要存储总共 2 亿条数据
- ⭐ filter:total_uv、app_uv、web_uv 在去重时,state 是存在一个 MapState 中的,MapState key 为 user_id,value 为 long,其中 long 的第一个 bit 位标识在计算总 uv 时此 user_id 是否来光顾哦,第二个标识 'android', 'iphone',第三个标识 'wap', 'other',因此在上述 case when 相同的数据量的情况下,总共只需要存储 1 亿条数据,state 容量减小了几乎 50%
或者下面的场景也可以使用 filter 子句进行替换。
- ⭐ 优化前:
select
day
, app_typp
, count(distinct user_id) as uv
from source_table
group by
day
, app_type
如果能够确定 app_type 是可以枚举的,比如为 android、iphone、web 三种,则可以使用 filter 子句做性能优化:
select
day
, count(distinct user_id) filter (where app_type = 'android') as android_uv
, count(distinct user_id) filter (where app_type = 'iphone') as iphone_uv
, count(distinct user_id) filter (where app_type = 'web') as web_uv
from source_table
group by
day
经过上述优化之后,state 大小的优化效果也会是成倍提升的。
4.5.SQL Connector 扩展 - 自定义 Source\Sink
4.5.1.自定义 Source\Sink
4.5.2.自定义 Source\Sink 的扩展接口
Flink SQL 中除了自定义的 Source 的基础接口之外,还提供了一部分扩展接口用于性能的优化、能力扩展,接下来详细进行介绍。在 Source\Sink 中主要包含了以下接口:
- ⭐ Source 算子的接口:
- ⭐
SupportsFilterPushDown
:将过滤条件下推到 Source 中提前过滤,减少下游处理的数据量。案例可见org.apache.flink.table.filesystem.FileSystemTableSource
- ⭐
SupportsLimitPushDown
:将 limit 条目数下推到 Source 中提前限制处理的条目数。案例可见org.apache.flink.table.filesystem.FileSystemTableSource
- ⭐
SupportsPartitionPushDown
:(常用于批处理场景)将带有 Partition 属性的 Source,将所有的 Partition 数据获取到之后,然后在 Source 决定哪个 Source 读取哪些 Partition 的数据,而不必在 Source 后过滤。比如 Hive 表的 Partition,将所有 Partition 获取到之后,然后决定某个 Source 应该读取哪些 Partition,详细可见org.apache.flink.table.filesystem.FileSystemTableSource
。 - ⭐
SupportsProjectionPushDown
:将下游用到的字段下推到 Source 中,然后 Source 中只取这些字段,不使用的字段就不往下游发。案例可见org.apache.flink.connector.jdbc.table.JdbcDynamicTableSource
- ⭐
SupportsReadingMetadata
:支持读取 Source 的 metadata,比如在 Kafka Source 中读取 Kafka 的 offset,写入时间戳等数据。案例可见org.apache.flink.streaming.connectors.kafka.table.KafkaDynamicSource
- ⭐
SupportsWatermarkPushDown
:支持将 Watermark 的分配方式下推到 Source 中,比如 Kafka Source 中一个 Source Task 可以读取多个 Partition,然后为每个 Partition 单独分配 Watermark Generator,这样 Watermark 的生成粒度就是单 Partition,在事件时间下数据计算会更加准确。案例可见org.apache.flink.streaming.connectors.kafka.table.KafkaDynamicSource
- ⭐
SupportsSourceWatermark
:支持自定义的 Source Watermark 分配方式,比如目前已有的 Watermark 分配方式不满足需求,需要自定义 Source 的 Watermark 生成方式,则可以实现此接口 +在 DDL 中声明 SOURCE_WATERMARK()
来声明使用自定义 Source 的 Watermark 生成方式。案例可见org.apache.flink.table.planner.connectors.ExternalDynamicSource
- ⭐ Sink 算子的接口:
- ⭐
SupportsOverwrite
:(常用于批处理场景)支持类似于 Hive SQL 的 insert overwrite table xxx 的能力,将已有分区内的数据进行覆盖。案例可见org.apache.flink.connectors.hive.HiveTableSink
- ⭐
SupportsPartitioning
:(常用于批处理场景)支持类似于 Hive SQL 的 insert INTO xxx partition(key = 'A') xxx 的能力,支持将结果数据写入某个静态分区。案例可见org.apache.flink.connectors.hive.HiveTableSink
- ⭐
SupportsWritingMetadata
:支持将 metadata 写入到 Sink 中,比如可以往 Kafka Sink 中写入 Kafka 的 timestamp、header 等。案例可见org.apache.flink.streaming.connectors.kafka.table.KafkaDynamicSink
4.5.3.Source:SupportsFilterPushDown
- ⭐ 应用场景:将 where 中的一些过滤条件下推到 Source 中进行处理,这样不需要的数据就可以不往下游发送了,性能会有提升。
- ⭐ 优化前:如下图 web ui 算子图,过滤条件都在 Source 节点之后有单独的 filter 算子进行承接
filter 前
- ⭐ 优化方案及实现:在 DynamicTableSource 中实现 SupportsFilterPushDown 接口的方法,具体实现方案如下:
public class Abilities_TableSource implements ScanTableSource
, SupportsFilterPushDown // 过滤条件下推 {
private List<ResolvedExpression> filters;
// 方法输入参数:List<ResolvedExpression> filters:引擎下推过来的过滤条件,然后在此方法中来决定哪些条件需要被下推
// 方法输出参数:Result:Result 记录哪些过滤条件在 Source 中应用,哪些条件不能在 Source 中应用
@Override
public Result applyFilters(List<ResolvedExpression> filters){
this.filters = new LinkedList<>(filters);
// 1.不上推任何过滤条件
// Result.of(上推的 filter, 没有做上推的 filter)
// return Result.of(Lists.newLinkedList(), filters);
// 2.将所有的过滤条件都上推到 source
return Result.of(filters, Lists.newLinkedList());
}
}
- ⭐ 优化效果:如下图 web ui 算子图,过滤条件在 Source 节点执行
filter 后
4.5.4.Source:SupportsLimitPushDown
- ⭐ 应用场景:将 limit 子句下推到 Source 中,在批场景中可以过滤大部分不需要的数据
- ⭐ 优化前:如下图 web ui 算子图,limit 条件都在 Source 节点之后有单独的 Limit 算子进行承接
limit 前
- ⭐ 优化方案及实现:在 DynamicTableSource 中实现 SupportsLimitPushDown 接口的方法,具体实现方案如下:
public class Abilities_TableSource implements ScanTableSource
, SupportsLimitPushDown // limit 条件下推 {
private long limit = -1;
@Override
// 方法输入参数:long limit:引擎下推过来的 limit 条目数
public void applyLimit(long limit){
// 将 limit 数接收到之后,然后在 SourceFunction 中可以进行过滤
this.limit = limit;
}
}
- ⭐ 优化效果:如下图 web ui 算子图,limit 条件在 Source 节点执行
limit 后
4.5.5.Source:SupportsProjectionPushDown
- ⭐ 应用场景:将下游用到的字段下推到 Source 中,然后 Source 中可以做到只取这些字段,不使用的字段就不往下游发
- ⭐ 优化前:如下图 web ui 算子图,limit 条件都在 Source 节点之后有单独的 Limit 算子进行承接
project 前
- ⭐ 优化方案及实现:在 DynamicTableSource 中实现 SupportsProjectionPushDown 接口的方法,具体实现方案如下:
public class Abilities_TableSource implements ScanTableSource
, SupportsProjectionPushDown // select 字段下推 {
private TableSchema tableSchema;
@SneakyThrows
@Override
public ScanRuntimeProvider getScanRuntimeProvider(ScanContext runtimeProviderContext){
// create runtime classes that are shipped to the cluster
final DeserializationSchema<RowData> deserializer = decodingFormat.createRuntimeDecoder(
runtimeProviderContext,
getSchemaWithMetadata(this.tableSchema).toRowDataType());
...
}
@Override
// 方法输入参数:
// int[][] projectedFields:下游算子 `使用到的那些字段` 的下标,可以通过 projectSchemaWithMetadata 方法结合 table schema 信息生成 Source 新的需要写出 schema 信息
public void applyProjection(int[][] projectedFields){
this.tableSchema = projectSchemaWithMetadata(this.tableSchema, projectedFields);
}
}
- ⭐ 优化效果:如下图 web ui 算子图,下游没有用到的字段直接在 Source 节点过滤掉,不输出
project 后
4.5.6.Source:SupportsReadingMetadata
- ⭐ 应用场景:支持读取 Source 的 metadata,比如在 Kafka Source 中读取 Kafka 的 offset,写入时间戳等数据
- ⭐ 支持之前:比如想获取 Kafka 中的 offset 字段,在之前是不支持的
- ⭐ 支持方案及实现:在 DynamicTableSource 中实现 SupportsReadingMetadata 接口的方法,我们来看看 Flink Kafka Consumer 的具体实现方案:
// 注意!!!先执行 listReadableMetadata(),然后执行 applyReadableMetadata(xxx, xxx) 方法
// 方法输出参数:列出 Kafka Source 可以从 Kafka 中读取的 metadata 数据
@Override
public Map<String, DataType> listReadableMetadata(){
final Map<String, DataType> metadataMap = new LinkedHashMap<>();
// add value format metadata with prefix
valueDecodingFormat
.listReadableMetadata()
.forEach((key, value) -> metadataMap.put(VALUE_METADATA_PREFIX + key, value));
// add connector metadata
Stream.of(ReadableMetadata.values())
.forEachOrdered(m -> metadataMap.putIfAbsent(m.key, m.dataType));
return metadataMap;
}
// 方法输入参数:
// List<String> metadataKeys:用户 SQL 中写入到 Sink 表的的 metadata 字段名称(metadataKeys)
// DataType producedDataType:将用户 SQL 写入到 Sink 表的所有字段的类型信息传进来,包括了 metadata 字段的类型信息
@Override
public void applyReadableMetadata(List<String> metadataKeys, DataType producedDataType){
final List<String> formatMetadataKeys =
metadataKeys.stream()
.filter(k -> k.startsWith(VALUE_METADATA_PREFIX))
.collect(Collectors.toList());
final List<String> connectorMetadataKeys = new ArrayList<>(metadataKeys);
connectorMetadataKeys.removeAll(formatMetadataKeys);
final Map<String, DataType> formatMetadata = valueDecodingFormat.listReadableMetadata();
if (formatMetadata.size() > 0) {
final List<String> requestedFormatMetadataKeys =
formatMetadataKeys.stream()
.map(k -> k.substring(VALUE_METADATA_PREFIX.length()))
.collect(Collectors.toList());
valueDecodingFormat.applyReadableMetadata(requestedFormatMetadataKeys);
}
this.metadataKeys = connectorMetadataKeys;
this.producedDataType = producedDataType;
}
- ⭐ 支持之后的效果:
CREATE TABLE KafkaTable (
// METADATA 字段用于声明可以从 Source 读取的 metadata
// 关于 Flink Kafka Source 可以读取的 metadata 见以下链接
// https://nightlies.apache.org/flink/flink-docs-release-1.13/docs/connectors/table/kafka/#available-metadata
`event_time` TIMESTAMP(3) METADATA FROM 'timestamp',
`partition` BIGINT METADATA VIRTUAL,
`offset` BIGINT METADATA VIRTUAL,
`user_id` BIGINT,
`item_id` BIGINT,
`behavior` STRING
) WITH (
'connector' = 'kafka',
'topic' = 'user_behavior',
'properties.bootstrap.servers' = 'localhost:9092',
'properties.group.id' = 'testGroup',
'scan.startup.mode' = 'earliest-offset',
'format' = 'csv'
);
在后续的 DML SQL 语句中就可以正常使用这些 metadata 字段的数据了。
4.5.7.Source:SupportsWatermarkPushDown
- ⭐ 应用场景:支持将 Watermark 的分配方式下推到 Source 中,比如 Kafka Source 中一个 Source Task 可以读取多个 Partition,Watermark 分配器下推到 Source 算子中后,就可以为每个 Partition 单独分配 Watermark Generator,这样 Watermark 的生成粒度就是 Kafka 的单 Partition,在事件时间下数据乱序会更小。
- ⭐ 支持之前:可以看到下图,Watermark 的分配是在 Source 节点之后的。
watermark 前
- ⭐ 支持方案及实现:在 DynamicTableSource 中实现 SupportsWatermarkPushDown 接口的方法,我们来看看 Flink Kafka Consumer 的具体实现方案:
// 方法输入参数:
// WatermarkStrategy<RowData> watermarkStrategy:将用户 DDL 中的 watermark 生成方式传入
@Override
public void applyWatermark(WatermarkStrategy<RowData> watermarkStrategy){
this.watermarkStrategy = watermarkStrategy;
}
- ⭐ 支持之后的效果:
watermark 前
4.5.8.Sink:SupportsOverwrite
- ⭐ 应用场景:(常用于批处理场景)支持类似于 Hive SQL 的 insert overwrite table xxx 的能力,将已有分区内的数据进行覆盖。
- ⭐ 支持方案及实现:在 DynamicTableSink 中实现 SupportsOverwrite 接口的方法,我们来看看
HiveTableSink
的具体实现方案:
private DataStreamSink<Row> createBatchSink(
DataStream<RowData> dataStream,
DataStructureConverter converter,
StorageDescriptor sd,
HiveWriterFactory recordWriterFactory,
OutputFileConfig fileNaming,
final int parallelism)
throws IOException {
FileSystemOutputFormat.Builder<Row> builder = new FileSystemOutputFormat.Builder<>();
...
--- 2. 将 overwrite 字段设置到 FileSystemOutputFormat 中,在后续写入数据到 Hive 表时,如果 overwrite = true,则会覆盖直接覆盖已有数据
builder.setOverwrite(overwrite);
builder.setStaticPartitions(staticPartitionSpec);
...
return dataStream
.map((MapFunction<RowData, Row>) value -> (Row) converter.toExternal(value))
.writeUsingOutputFormat(builder.build())
.setParallelism(parallelism);
}
// 1. 方法输入参数:
// boolean overwrite:用户写的 SQL 中如果包含了 overwrite 关键字,则方法入参 overwrite = true
// 如果不包含 overwrite 关键字,则方法入参 overwrite = false
@Override
public void applyOverwrite(boolean overwrite){
this.overwrite = overwrite;
}
- ⭐ 支持之后的效果:
支持在批任务中 insert overwrite xxx。
insert overwrite hive_sink_table
select
user_id
, order_amount
, server_timestamp_bigint
, server_timestamp
from hive_source_table
4.5.9.Sink:SupportsPartitioning
- ⭐ 应用场景:(常用于批处理场景)支持类似于 Hive SQL 的 insert INTO xxx partition(key = 'A') 的能力,支持将结果数据写入某个静态分区。
- ⭐ 支持方案及实现:在 DynamicTableSink 中实现 SupportsPartitioning 接口的方法,我们来看看
HiveTableSink
的具体实现方案:
private DataStreamSink<Row> createBatchSink(
DataStream<RowData> dataStream,
DataStructureConverter converter,
StorageDescriptor sd,
HiveWriterFactory recordWriterFactory,
OutputFileConfig fileNaming,
final int parallelism)
throws IOException {
FileSystemOutputFormat.Builder<Row> builder = new FileSystemOutputFormat.Builder<>();
...
builder.setMetaStoreFactory(msFactory());
builder.setOverwrite(overwrite);
--- 2. 将 staticPartitionSpec 字段设置到 FileSystemOutputFormat 中,在后续写入数据到 Hive 表时,如果有静态分区,则会将数据写入到对应的静态分区中
builder.setStaticPartitions(staticPartitionSpec);
...
return dataStream
.map((MapFunction<RowData, Row>) value -> (Row) converter.toExternal(value))
.writeUsingOutputFormat(builder.build())
.setParallelism(parallelism);
}
// 1. 方法输入参数:
// Map<String, String> partitionMap:用户写的 SQL 中如果包含了 partition(partition_key = 'A') 关键字
// 则方法入参 Map<String, String> partitionMap 的输入值转为 JSON 后为:{"partition_key": "A"}
// 用户可以自己将方法入参的 partitionMap 保存到自定义变量中,后续写出到 Hive 表时进行使用
@Override
public void applyStaticPartition(Map<String, String> partitionMap){
staticPartitionSpec = new LinkedHashMap<>();
for (String partitionCol : getPartitionKeys()) {
if (partitionMap.containsKey(partitionCol)) {
staticPartitionSpec.put(partitionCol, partitionMap.get(partitionCol));
}
}
}
- ⭐ 支持之后的效果:
insert overwrite hive_sink_table partition(date = '2022-01-01')
select
user_id
, order_amount
, server_timestamp_bigint
, server_timestamp
from hive_source_table
4.5.9.Sink:SupportsWritingMetadata
- ⭐ 应用场景:支持将 metadata 写入到 Sink 中。举例:可以往 Kafka Sink 中写入 Kafka 的 timestamp、header 等。案例可见
org.apache.flink.streaming.connectors.kafka.table.KafkaDynamicSink
- ⭐ 支持方案及实现:在 DynamicTableSink 中实现 SupportsWritingMetadata 接口的方法,我们来看看
KafkaDynamicSink
的具体实现方案:
// 注意!!!先执行 listWritableMetadata(),然后执行 applyWritableMetadata(xxx, xxx) 方法
// 1. 方法返回参数 Map<String, DataType>:Flink 会获取到可以写入到 Kafka Sink 中的 metadata 都有哪些
@Override
public Map<String, DataType> listWritableMetadata(){
final Map<String, DataType> metadataMap = new LinkedHashMap<>();
Stream.of(WritableMetadata.values())
.forEachOrdered(m -> metadataMap.put(m.key, m.dataType));
return metadataMap;
}
// 2. 方法输入参数:
// List<String> metadataKeys:通过解析用户的 SQL 语句,得出用户写出到 Sink 的 metadata 列信息,是 listWritableMetadata() 返回结果的子集
// DataType consumedDataType:写出到 Sink 字段的 DataType 类型信息,包括了写出的 metadata 列的类型信息(注意!!!metadata 列会被添加到最后一列)。
// 用户可以将这两个信息获取到,然后传入构造的 SinkFunction 中实现将对应字段写入 metadata 流程。
@Override
public void applyWritableMetadata(List<String> metadataKeys, DataType consumedDataType){
this.metadataKeys = metadataKeys;
this.consumedDataType = consumedDataType;
}
- ⭐ 支持之后的效果:
CREATE TABLE KafkaSourceTable (
`user_id` BIGINT,
`item_id` BIGINT,
`behavior` STRING
) WITH (
'connector' = 'kafka',
'topic' = 'source_topic',
'properties.bootstrap.servers' = 'localhost:9092',
'properties.group.id' = 'testGroup',
'scan.startup.mode' = 'earliest-offset',
'value.format' = 'json'
);
CREATE TABLE KafkaSinkTable (
-- 1. 定义 kafka 中 metadata 的 timestamp 列
`timestamp` TIMESTAMP_LTZ(3) METADATA,
`user_id` BIGINT,
`item_id` BIGINT,
`behavior` STRING
) WITH (
'connector' = 'kafka',
'topic' = 'sink_topic',
'properties.bootstrap.servers' = 'localhost:9092',
'properties.group.id' = 'testGroup',
'scan.startup.mode' = 'earliest-offset',
'value.format' = 'json'
);
insert into KafkaSinkTable
select
-- 2. 写入到 kafka 的 metadata 中的 timestamp
cast(CURRENT_TIMESTAMP as TIMESTAMP_LTZ(3)) as `timestamp`
, user_id
, item_id
, behavior
from KafkaSourceTable