【源码改造】Flink-jdbc-connector源码简析+改造支持谓词下推

一、需求背景分析

flink sql在维表关联时,会有一个场景:当右表的数据量比较大且有些数据虽然符合join条件,但其实对于下游来说数据可能没用,这样就浪费了flink的计算资源,且拉低了数据处理能力。如果在join前就把维表的数据进一步过滤,然后再join,这样就会使减轻“无用数据”对flink内存的占用,提高计算能力,进而优化数据处理的能力。

有两个思路可以解决这个问题,
一是将维表在join前先过滤不需要的数据,然后再注册为时态表函数,接着join的维表数据就是“下推后的数据”,但目前flink对于时态表函数join时并不完善,场景覆盖的不够全面。
二是在直接修改源码,给维表一个with参数去过滤数据,在join拉取维表数据时,直接过滤。说起来有点抽象,别急,这就是我们接下来要讨论内容。

 

二、flink-jdbc-connector源码分析与实现

1. 源码分析

1.1.官网解读

先了解下flink sql在代码层面从一个阶段到其他阶段的翻译过程。
在这里插入图片描述

阶段解释
元数据管理

执行CREATE TABLE时,会在目标catalog种更新元数据,比如上图黄色的create table语句。语句中with下的参数不会被校验和解释。DDL语句将会被解释成CatalogTable实例。

生成逻辑计划

接下来,当规划和优化一个job下的sql时,CatalogTable实例将会根据sql的语法具体解析为:当读select 语句时,会解析成DynamicTableSource;当读到insert into 语句时,解析成DynamicTableSink。

DynamicTableSource\SinkFactory将会提供具体的解析方法将CatalogTable解析为DynamicTableSource\Sink。具体的,比如说with下的port,Factory会从CatalogTable中校验port是否是连接器支持的参数,然后获取值,并创建参数实例,以便往下个阶段传输。

默认情况下,通过java的SPI机制发现工厂实例,工厂实例要定义一个能被校验的工厂标识符,例如上图:‘connector’ = ‘custom’。

DynamicTableSource\Sink 在运行时将会实际的读写数据。

运行时

当逻辑计划生成时,planner将会获取连接器的实现。运行时核心接口是,InputFormat和SourceFunction,按另一个抽象级别分组为ScanRuntimeProvider、 LookupRuntimeProvider和的子类SinkRuntimeProvider

通过对官网的解读,
我们知道对于一个flink sql,首先会被维护到catalogTable实例中,catalogTable根据sql类型找到对应的DynamicTable;

这里有一点:create table不会生成DynamicTable,而是select语句会生成DynamicTableSource代表从物理表中拉取数据,insert into语句生成DynamicTableSink代表写入数据到物理表。

在Planning期间,通过with下的connector参数获取对应的工厂,然后通过工厂校验、解析(with下的)参数值(比如’port’=‘5022’)最后封装成参数实例,并传递给runtime的实例;

最后Runtime期间,根据参数实例定义的数据消费规则,开始真正的从物理表中处理(拉取\写入)数据。

 

1.2 JDBC-connector的解读

通过上面的分析,现在我们通过一个场景分析下,一个job sql是如何在源码中被解析和运行的。

先看一个job下的sql:sql的逻辑是clickhouse join clickhouse 后将数据输出到flink的控制台

CREATE TABLE `in_table` (
   `id` BIGINT NOT NULL,
   `name` String NOT NULL,
   `cloud_wise_proc_time` AS `proctime`()
 ) WITH (
   'connector' = 'jdbc',
   'url' = 'jdbc:clickhouse://xxx/gaogao?socket_timeout=900000',
   'username' = 'default',
   'table-name' = 'user'
 );

 CREATE TABLE `in_table_1` (
   `id` BIGINT NOT NULL,
   `age` BIGINT NOT NULL,
   _cw_insert_time TIMESTAMP(3)
 ) WITH (
   'connector' = 'jdbc',
   'url' = 'jdbc:clickhouse://xxx/gaogao?socket_timeout=900000',
   'username' = 'default',
   'table-name' = 'info',
   'lookup.cache.max-rows' = '100000',
   'lookup.cache.ttl' = '10MINUTE'
 );

 create table out_table(
	`id` BIGINT,
    `age` BIGINT,
    _cw_insert_time TIMESTAMP(3)
 ) with (
 'connector' = 'print');

insert into out_table 
select a.id,b.age,b._cw_insert_time 
from in_table as a  left join in_table_1  FOR SYSTEM_TIME AS OF a.cloud_wise_proc_time as  b  on a.id=b.id;

下图展示了在维表关联时维表侧(in_table_1)拉取数据的源码逻辑:
在这里插入图片描述
Planning时
JdbcDynamicTableFactory拿到(in_table_1)CatalogTable实例的参数值(如下),

   'connector' = 'jdbc',
   'url' = 'jdbc:clickhouse://xxx:18100/gaogao?socket_timeout=900000',
   'username' = 'default',
   'table-name' = 'info',
   'lookup.cache.max-rows' = '100000',
   'lookup.cache.ttl' = '10MINUTE'

进行检查和解析,如必要的连接参数(url、username、password、driver、table-name)用于连接clickhouse的那张表;可选的配置(例查询缓存、分区查询),用于拉取数据时的策略,比如单次查询最多拉取10000条数据到缓存中,缓存每10分钟过期一次,以免没有及时查到更新后的数据。
然后生成参数实例,并将参数传递到runtime阶段。

runtime时
JdbcRowDataLookupFunction主要有两个逻辑,一个是通过planning传递的必要参数,去建立JDBC连接,二是根据查询条件去查询并获取数据。

关键来了:

查询条件是维表关联的关联字段,而值来源于左表。具体的说,当左表通过物理表来查询到一条数据时,这条数据中关联字段对应的数据作为右表查询时的查询条件!!!

当获取到这条消息之后,那谓词下推要实现的逻辑点就引刃而解了,即将谓词下推逻辑放到eval的查询sql中!!!

 

2. 源码改造

按照planning到runtime的顺序改造:

2.1. planning

添加with参数,即谓词下推参数

JdbcDynamicTableFactory{
...
    private static final ConfigOption<String> PRE_FILTER_CONDITION =
            ConfigOptions.key("lookup.data.filter")
                    .stringType()
                    .defaultValue("")
                    .withDescription("filter data before dimension table join.");
 
     /**
     * 用于可选参数的存储,和检查。
     * @return
     */
    @Override
    public Set<ConfigOption<?>> optionalOptions() {
        Set<ConfigOption<?>> optionalOptions = new HashSet<>();
        。。。
        optionalOptions.add(PRE_FILTER_CONDITION);
        return optionalOptions;
    }

2.2 Runtime

public class JdbcLookupFunction extends TableFunction<Row> {
。。。
    public JdbcLookupFunction(
            JdbcOptions options,
            JdbcLookupOptions lookupOptions,
            String[] fieldNames,
            TypeInformation[] fieldTypes,
            String[] keyNames) {
            。。。
       //嵌入下推条件
       this.query =
                FieldNamedPreparedStatementImpl.parseNamedStatement(
                        options.getDialect()
                                .getSelectFromStatement(
                                        options.getTableName(), fieldNames, keyNames, lookupOptions.getPreFilterCondition()), new HashMap<>());
@Internal
public interface JdbcDialect extends Serializable {
。。。
    /**
     * 维表join查询sql语句
     * " SELECT `user_id`, `age` FROM `w` WHERE `user_id` = ? and `age`>3"
     *
     * @param tableName          表名
     * @param selectFields       查询字段
     * @param conditionFields    关联字段
     * @param preFilterCondition 预过滤条件:一定程度减小查询缓存 例如:age>3,进一步过滤user_id等于某值的数据
     * @return
     */
    default String getSelectFromStatement(
            String tableName, String[] selectFields, String[] conditionFields, String preFilterCondition) {
        String selectExpressions =
                Arrays.stream(selectFields)
                        .map(this::quoteIdentifier)
                        .collect(Collectors.joining(", "));
        String fieldExpressions =
                Arrays.stream(conditionFields)
                        .map(f -> format("%s = :%s", quoteIdentifier(f), f))
                        .collect(Collectors.joining(" AND "));
        return "SELECT "
                + selectExpressions
                + " FROM "
                + quoteIdentifier(tableName)
                + (conditionFields.length > 0 ? " WHERE " + fieldExpressions : "")
                + (StringUtils.isNotBlank(preFilterCondition) ? " AND " + preFilterCondition : "");
    }

 

3. SQL验证

CREATE TABLE `in_table` (
   `id` BIGINT NOT NULL,
   `name` String NOT NULL,
   `cloud_wise_proc_time` AS `proctime`()
 ) WITH (
   'connector' = 'jdbc',
   'url' = 'jdbc:clickhouse://xxx/gaogao?socket_timeout=900000',
   'username' = 'default',
   'table-name' = 'user'
 );

 CREATE TABLE `in_table_1` (
   `id` BIGINT NOT NULL,
   `age` BIGINT NOT NULL,
   _cw_insert_time TIMESTAMP(3)
 ) WITH (
   'connector' = 'jdbc',
   'url' = 'jdbc:clickhouse://xxx/gaogao?socket_timeout=900000',
   'username' = 'default',
   'table-name' = 'info',
   'lookup.cache.max-rows' = '100000',
   'lookup.cache.ttl' = '10MINUTE'
    'lookup.data.filter'='toInt32(toYYYYMMDD(_partition_day))=20220707',
 );

 create table out_table(
	`id` BIGINT,
    `age` BIGINT,
    _cw_insert_time TIMESTAMP(3)
 ) with (
 'connector' = 'print');

insert into out_table 
select a.id,b.age,b._cw_insert_time 
from in_table as a  left join in_table_1  FOR SYSTEM_TIME AS OF a.cloud_wise_proc_time as  b  on a.id=b.id;

此时flink job 在join前,in_table_1会只拉取_partition_day=20220707的数据,实现谓词下推。

 

参考:
https://nightlies.apache.org/flink/flink-docs-release-1.12/zh/dev/table/sourceSinks.html

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Flink-connector-jdbc是Apache Flink的一个官方连接器,用于将Flink与关系型数据库(如MySQL、PostgreSQL等)进行集成。 要下载Flink-connector-jdbc,可以按照以下步骤进行操作: 1. 打开Apache Flink的官方网站(https://flink.apache.org/)。 2. 导航到"Downloads"(下载)页面,这里提供了Flink的各种版本。 3. 选择所需的Flink版本并下载对应的二进制文件。通常,您可以选择最新的稳定版本。 4. 在下载的Flink二进制文件中,找到一个名为"flink-connector-jdbc"的目录。该目录中包含了Flink与关系型数据库进行连接所需的所有库和文件。 5. 将"flink-connector-jdbc"目录复制到您的工作环境中,以便您可以在Flink应用程序中使用它。 6. 现在,您可以将"flink-connector-jdbc"添加到您的Flink应用程序的依赖中,并开始使用它。 如果您需要使用Maven进行构建,可以在您的Maven配置文件(pom.xml)中添加以下依赖: ``` <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-connector-jdbc_${scala.binary.version}</artifactId> <version>${flink.version}</version> </dependency> ``` 请注意替换`${scala.binary.version}`和`${flink.version}`为您要使用的具体版本。 下载Flink-connector-jdbc后,您就可以使用它来将Flink与关系型数据库进行集成,实现从数据库中读取数据或将数据写入到数据库中。您可以使用FlinkJDBC连接器来定义数据库表的结构,并使用SQL或DataStream API操作和处理数据库中的数据。通过Flink-connector-jdbc,您可以轻松地利用Flink强大的数据处理能力与关系型数据库进行交互。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

roman_日积跬步-终至千里

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值