史上最全干货!Flink SQL 成神之路(四)

crazeblue
发布于 2022-9-21 10:56
浏览
0收藏

作者 | antigeneral了呀

来源 | 大数据羊说(ID:young_say)

作者 | antigeneral了呀

4.SQL 能力扩展篇

4.1.SQL UDF 扩展 - Module

在介绍 Flink Module 具体能力之前,我们先来聊聊博主讲述的思路:

  1. ⭐ 背景及应用场景介绍
  2. ⭐ Flink Module 功能介绍
  3. ⭐ 应用案例:Flink SQL 支持 Hive UDF

4.1.1.Flink SQL Module 应用场景

兄弟们,想想其实大多数公司都是从离线数仓开始建设的。相信大家必然在自己的生产环境中开发了非常多的 Hive UDF。随着需求对于时效性要求的增高,越来越多的公司也开始建设起实时数仓。很多场景下实时数仓的建设都是随着离线数仓而建设的。实时数据使用 Flink 产出,离线数据使用 Hive/Spark 产出。

那么回到我们的问题:为什么需要给 Flink UDF 做扩展呢?可能这个问题比较大,那么博主分析的具体一些,如果 Flink 扩展支持 Hive UDF 对我们有哪些好处呢?

博主分析了下,结论如下:

站在数据需求的角度来说,一般会有以下两种情况:

  1. ⭐ 以前已经有了离线数据链路,需求方也想要实时数据。如果直接能用已经开发好的 hive udf,则不用将相同的逻辑迁移到 flink udf 中,并且后续无需费时费力维护两个 udf 的逻辑一致性。
  2. ⭐ 实时和离线的需求都是新的,需要新开发。如果只开发一套 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:

  1. ⭐ CoreModule:CoreModule 是 Flink 内置的 Module,其包含了目前 Flink 内置的所有 UDF,Flink 默认开启的 Module 就是 CoreModule,我们可以直接使用其中的 UDF
  2. ⭐ HiveModule:HiveModule 可以将 Hive 内置函数作为 Flink 的系统函数提供给 SQL\Table API 用户进行使用,比如 get_json_object 这类 Hive 内置函数(Flink 默认的 CoreModule 是没有的)
  3. ⭐ 用户自定义 Module:用户可以实现 Module 接口实现自己的 UDF 扩展 Module

在 Flink 中,Module 可以被 ​​加载​​​、​​启用​​​、​​禁用​​​、​​卸载​​ Module,当 TableEnvironment 加载(见 SQL 语法篇的 Load Module) Module 之后,默认就是开启的。

Flink 是同时支持多个 Module 的,并且根据加载 Module 的顺序去按顺序查找和解析 UDF,先查到的先解析使用。

此外,Flink 只会解析已经启用了的 Module。那么当两个 Module 中出现两个同名的函数时,会有以下三种情况:

  1. ⭐ 如果两个 Module 都启用的话,Flink 会根据加载 Module 的顺序进行解析,结果就是会使用顺序为第一个的 Module 的 UDF
  2. ⭐ 如果只有一个 Module 启用的话,Flink 就只会从启用的 Module 解析 UDF
  3. ⭐ 如果两个 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,数据库命名空间。

  1. ⭐ 使用 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 |
// +-------------+-------+
  1. ⭐ 使用 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 这件事分为两个部分。

  1. ⭐ Flink 扩展支持 hive 内置 UDF
  2. ⭐ 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。

史上最全干货!Flink SQL 成神之路(四)-鸿蒙开发者社区

error

第二部分:Flink 扩展支持用户自定义 Hive UDF。

内置函数解决不了用户的复杂需求,用户就需要自己写 Hive UDF,并且这部分自定义 UDF 也想在 flink sql 中使用。

下面看看怎么在 Flink SQL 中进行这两种扩展。

  1. ⭐ 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。

史上最全干货!Flink SQL 成神之路(四)-鸿蒙开发者社区

get_json_object

然后我们再去在 Flink SQL 中使用 get_json_object 这个 UDF,就没有报错,能正常输出结果了。

使用 Flink Hive connector 自带的 ​​HiveModule​​,已经能够解决很大一部分常见 UDF 使用的问题了。

  1. ⭐ 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。

史上最全干货!Flink SQL 成神之路(四)-鸿蒙开发者社区

ddl hive udf error

看了下源码,Flink 流任务模式下(未连接 Hive MetaStore 时)在创建 UDF 时会认为这个 UDF 是 Flink 生态体系中的 UDF。

所以在初始化我们引入的 ​​TestGenericUDF​​​ 时,默认会按照 Flink 的 ​​UserDefinedFunction​​ 强转,因此才会报强转错误。

那么我们就不能使用 Hive UDF 了吗?

错误,小伙伴萌岂敢有这种想法。博主都把这个标题列出来了(牛逼都吹出去了),还能给不出解决方案嘛。

思路见下一节。

  1. ⭐ Flink 扩展支持用户自定义 Hive UDF 的增强 module

其实思路很简单。

使用 Flink SQL 中的 ​​create temporary function​​ 虽然不能执行,但是 Flink 提供了插件化的自定义 module。

我们可以扩展一个支持用户自定义 Hive UDF 的 module,使用这个 module 来支持自定义的 Hive UDF。

实现的代码也非常简单。简单的把 Flink Hive connector 提供的 ​​HiveModule​​​ 做一个增强即可,即下图中的 ​​HiveModuleV2​​。使用方式如下图所示:

史上最全干货!Flink SQL 成神之路(四)-鸿蒙开发者社区


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:

  1. ⭐ GenericInMemoryCatalog:GenericInMemoryCatalog 是基于内存实现的 Catalog,所有元数据只在 session 的生命周期(即一个 Flink 任务一次运行生命周期内)内可用。
  2. ⭐ 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)
  1. ⭐ 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");
  1. ⭐ 用户自定义 Catalog:用户可以实现 Catalog 接口实现自定义 Catalog

下面看看 Flink Catalog 提供了什么 API,以及对应 API 的使用案例:

  1. ⭐ 使用 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 中的所有表
  1. ⭐ 使用 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 章节。

  1. ⭐ 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");
  1. ⭐ 数据库操作

// 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");
  1. ⭐ 表操作

// 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");
  1. ⭐ 视图操作

// 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");
  1. ⭐ 分区操作

// 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, ...));
  1. ⭐ 函数操作

// 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 类:

  1. ⭐ 运行时参数:优化 Flink SQL 任务在执行时的任务性能
  2. ⭐ 优化器参数:Flink SQL 任务在生成执行计划时,经过优化器优化生成更优的执行计划
  3. ⭐ 表参数:用于调整 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

其中上述参数中最常被用到为一下两种:

  1. ⭐ MiniBatch 聚合

table.exec.mini-batch.enabled: true
table.exec.mini-batch.allow-latency: 60 s
table.exec.mini-batch.size: 1000000000

具体使用场景如下链接:

​https://nightlies.apache.org/flink/flink-docs-release-1.13/docs/dev/table/tuning/#minibatch-aggregation​

  1. ⭐ 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

其中上述参数中最常被用到为以下两种:

  1. ⭐ 两阶段优化:

table.optimizer.agg-phase-strategy: AUTO

在计算 count(1),sum(col) 场景汇总提效很高,因为 count(1),sum(col) 在经过本地 localAggregate 之后,每个 group by 的 key 就一个结果值。

注意!!!:此优化在窗口聚合中会自动生效,但是在 unbounded agg 中需要与 minibatch 参数相结合使用才会生效。

  1. ⭐ 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 中的聚合算子的优化,在某些场景下应用这些优化后,性能提升会非常大。本小节主要包含以下四种优化:

  1. ⭐​​(常用)​​MiniBatch 聚合:unbounded group agg 中,可以使用 minibatch 聚合来做到微批计算、访问状态、输出结果,避免每来一条数据就计算、访问状态、输出一次结果,从而减少访问 state 的时长(尤其是 Rocksdb)提升性能。
  2. ⭐​​(常用)​​两阶段聚合:类似 MapReduce 中的 Combiner 的效果,可以先在 shuffle 数据之前先进行一次聚合,减少 shuffle 数据量
  3. ⭐​​(不常用)​​split 分桶:在 count distinct、sum distinct 的去重的场景中,如果出现数据倾斜,任务性能会非常差,所以如果先按照 distinct key 进行分桶,将数据打散到各个 TM 进行计算,然后将分桶的结果再进行聚合,性能就会提升很大
  4. ⭐​​(常用)​​去重 filter 子句:在 count distinct 中使用 filter 子句于 Hive SQL 中的 count(distinct if(xxx, user_id, null)) 子句,但是 state 中同一个 key 会按照 bit 位会进行复用,这对状态大小优化非常有用

上面简单介绍了聚合场景的四种优化,下面详细介绍一下其最终效果以及实现原理。

4.4.1.MiniBatch 聚合

  1. ⭐ 问题场景:默认情况下,unbounded agg 算子是逐条处理输入的记录,其处理流程如下:
  • ⭐ 从状态中读取 accumulator;
  • ⭐ 累加/撤回的数据记录至 accumulator;
  • ⭐ 将 accumulator 写回状态;
  • ⭐ 下一条记录将再次从流程 1 开始处理。

但是上述处理流程的问题在于会增加 StateBackend 的访问性能开销(尤其是对于 RocksDB StateBackend)。

  1. ⭐ MiniBatch 聚合如何解决上述问题:其核心思想是将一组输入的数据缓存在聚合算子内部的缓冲区中。当输入的数据被触发处理时,每个 key 只需要访问一次状态后端,这样可以大大减少访问状态的时间开销从而获得更好的吞吐量。但是,其会增加一些数据产出的延迟,因为它会缓冲一些数据再去处理。因此如果你要做这个优化,需要提前做一下吞吐量和延迟之间的权衡,但是大多数情况下,buffer 数据的延迟都是可以被接受的。所以非常建议在 unbounded agg 场景下使用这项优化。

下图说明了 MiniBatch 聚合如何减少状态访问的。

史上最全干货!Flink SQL 成神之路(四)-鸿蒙开发者社区

MiniBatch

上图展示了加 MiniBatch 和没加 MiniBatch 之前的执行区别。

  1. ⭐ 启用 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 最多的输入数据记录数目

注意!!!

  1. ⭐​​table.exec.mini-batch.allow-latency​​​ 和​​table.exec.mini-batch.size​​ 两者只要其中一项满足条件就会执行 batch 访问状态操作。
  2. ⭐ 上述 MiniBatch 配置不会对 Window TVF 生效,因为!!!Window TVF 默认就会启用小批量优化,Window TVF 会将 buffer 的输入记录记录在托管内存中,而不是 JVM 堆中,因此 Window TVF 不会有 GC 过高或者 OOM 的问题。

4.4.2.两阶段聚合

  1. ⭐ 问题场景:在聚合数据处理场景中,很可能会由于热点数据导致数据倾斜,如下 SQL 所示,当 color = RED 为 50000w 条,而 color = BLUE 为 5 条,就产生了数据倾斜,而器数据处理的算子产生性能瓶颈。

SELECT color, sum(id)
FROM T
GROUP BY color
  1. ⭐ 两阶段聚合如何解决上述问题:其核心思想类似于 MapReduce 中的 Combiner + Reduce,先将聚合操作在本地做一次 local 聚合,这样 shuffle 到下游的数据就会变少。

还是上面的 SQL 案例,如果在 50000w 条的 color = RED 的数据 shuffle 之前,在本地将 color = RED 的数据聚合成为 1 条结果,那么 shuffle 给下游的数据量就被极大地减少了。

下图说明了两阶段聚合是如何处理热点数据的:

史上最全干货!Flink SQL 成神之路(四)-鸿蒙开发者社区

两阶段聚合

  1. ⭐ 启用两阶段聚合的参数:

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"); // 打开两阶段聚合

注意!!!

  1. ⭐ 此优化在窗口聚合中会自动生效,大家在使用 Window TVF 时可以看到 localagg + globalagg 两部分
  2. ⭐ 但是在 unbounded agg 中需要与 MiniBatch 参数相结合使用才会生效。

4.4.3.split 分桶

  1. ⭐ 问题场景:使用两阶段聚合虽然能够很好的处理 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
  1. ⭐ 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 分桶的处理流程:

史上最全干货!Flink SQL 成神之路(四)-鸿蒙开发者社区

split 聚合

  1. ⭐ 启用 split 分桶的参数:

TableEnvironment tEnv = ...

tEnv.getConfig()
  .getConfiguration()
  .setString("table.optimizer.distinct-agg.split.enabled", "true");  // 打开 split 分桶

注意!!!

  1. ⭐ 如果有多个 distinct key,则多个 distinct key 都会被作为分桶 key。比如 count(distinct a),sum(distinct b) 这种多个 distinct key 也支持。
  2. ⭐ 小伙伴萌自己写的 UDAF 不支持!
  3. ⭐ 其实此种优化很少使用,因为大家直接自己按照分桶的写法自己就可以写了,而且最后生成的算子图和自己写的 SQL 的语法也能对应的上

4.4.4.去重 filter 子句

  1. ⭐ 问题场景:在一些场景下,用户可能需要从不同维度计算 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 子句。

  1. ⭐ 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 子句进行替换。

  1. ⭐ 优化前:

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 中主要包含了以下接口:

  1. ⭐ 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​
  1. ⭐ 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

  1. ⭐ 应用场景:将 where 中的一些过滤条件下推到 Source 中进行处理,这样不需要的数据就可以不往下游发送了,性能会有提升。
  2. ⭐ 优化前:如下图 web ui 算子图,过滤条件都在 Source 节点之后有单独的 filter 算子进行承接

史上最全干货!Flink SQL 成神之路(四)-鸿蒙开发者社区

filter 前

  1. ⭐ 优化方案及实现:在 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());
    }
}
  1. ⭐ 优化效果:如下图 web ui 算子图,过滤条件在 Source 节点执行

史上最全干货!Flink SQL 成神之路(四)-鸿蒙开发者社区

filter 后

4.5.4.Source:SupportsLimitPushDown

  1. ⭐ 应用场景:将 limit 子句下推到 Source 中,在批场景中可以过滤大部分不需要的数据
  2. ⭐ 优化前:如下图 web ui 算子图,limit 条件都在 Source 节点之后有单独的 Limit 算子进行承接

史上最全干货!Flink SQL 成神之路(四)-鸿蒙开发者社区

limit 前

  1. ⭐ 优化方案及实现:在 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;
    }
}
  1. ⭐ 优化效果:如下图 web ui 算子图,limit 条件在 Source 节点执行

史上最全干货!Flink SQL 成神之路(四)-鸿蒙开发者社区

limit 后

4.5.5.Source:SupportsProjectionPushDown

  1. ⭐ 应用场景:将下游用到的字段下推到 Source 中,然后 Source 中可以做到只取这些字段,不使用的字段就不往下游发
  2. ⭐ 优化前:如下图 web ui 算子图,limit 条件都在 Source 节点之后有单独的 Limit 算子进行承接

史上最全干货!Flink SQL 成神之路(四)-鸿蒙开发者社区

project 前

  1. ⭐ 优化方案及实现:在 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);
    }
}
  1. ⭐ 优化效果:如下图 web ui 算子图,下游没有用到的字段直接在 Source 节点过滤掉,不输出

史上最全干货!Flink SQL 成神之路(四)-鸿蒙开发者社区

project 后

4.5.6.Source:SupportsReadingMetadata

  1. ⭐ 应用场景:支持读取 Source 的 metadata,比如在 Kafka Source 中读取 Kafka 的 offset,写入时间戳等数据
  2. ⭐ 支持之前:比如想获取 Kafka 中的 offset 字段,在之前是不支持的
  3. ⭐ 支持方案及实现:在 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;
}
  1. ⭐ 支持之后的效果:

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

  1. ⭐ 应用场景:支持将 Watermark 的分配方式下推到 Source 中,比如 Kafka Source 中一个 Source Task 可以读取多个 Partition,Watermark 分配器下推到 Source 算子中后,就可以为每个 Partition 单独分配 Watermark Generator,这样 Watermark 的生成粒度就是 Kafka 的单 Partition,在事件时间下数据乱序会更小。
  2. ⭐ 支持之前:可以看到下图,Watermark 的分配是在 Source 节点之后的。

史上最全干货!Flink SQL 成神之路(四)-鸿蒙开发者社区

watermark 前

  1. ⭐ 支持方案及实现:在 DynamicTableSource 中实现 SupportsWatermarkPushDown 接口的方法,我们来看看 Flink Kafka Consumer 的具体实现方案:

// 方法输入参数:
// WatermarkStrategy<RowData> watermarkStrategy:将用户 DDL 中的 watermark 生成方式传入
@Override
public void applyWatermark(WatermarkStrategy<RowData> watermarkStrategy){
    this.watermarkStrategy = watermarkStrategy;
}
  1. ⭐ 支持之后的效果:

史上最全干货!Flink SQL 成神之路(四)-鸿蒙开发者社区

watermark 前

4.5.8.Sink:SupportsOverwrite

  1. ⭐ 应用场景:(常用于批处理场景)支持类似于 Hive SQL 的 insert overwrite table xxx 的能力,将已有分区内的数据进行覆盖。
  2. ⭐ 支持方案及实现:在 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;
}
  1. ⭐ 支持之后的效果:

支持在批任务中 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

  1. ⭐ 应用场景:(常用于批处理场景)支持类似于 Hive SQL 的 insert INTO xxx partition(key = 'A') 的能力,支持将结果数据写入某个静态分区。
  2. ⭐ 支持方案及实现:在 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));
        }
    }
}
  1. ⭐ 支持之后的效果:

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

  1. ⭐ 应用场景:支持将 metadata 写入到 Sink 中。举例:可以往 Kafka Sink 中写入 Kafka 的 timestamp、header 等。案例可见​​org.apache.flink.streaming.connectors.kafka.table.KafkaDynamicSink​
  2. ⭐ 支持方案及实现:在 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;
}
  1. ⭐ 支持之后的效果:

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


分类
标签
已于2022-9-21 10:56:51修改
收藏
回复
举报
回复
    相关推荐