首页 > 大数据 > Hadoop > 大数据开发之Flink SQL建设实时数仓实践
个推Flink SQL使用现状
在SQL模式下,个推通过jar+SQL文件+配置参数的方式使用Flink。其中jar是基于Flink封装的执行SQL文件的执行jar,提交命令示例如下:
SQL文件内容示例如下:
在将原有的Spark Streaming实时计算任务改造成SQL的过程中,我们发现了许多原生Flink SQL无法支持的需求,比如:
针对以上大部分场景,我们均结 合业务特色需求,对Flink SQL进行了拓展适配。本文从中间表注册入手,分享Flink SQL正确使用姿势。
Flink中SQL的处理流程
为了帮助大家更好地理解中间表注册问题,我们先整体梳理下Flink中SQL的执行逻辑,如下图:
整个流程可以大致拆解为以下几个步骤:
1、SqlParser解析阶段(SQL -> SqlNode)
Flink的Calcite使用JavaCC,根据Parser.jj生成SqlParser(实际类名为SqlParserImpl)。SqlParser负责将SQL解析为AST语法树,数据类型为SqlNode。
2、Validator验证阶段
第一阶段后生成的AST树中,对字段、函数等并没有进行验证。第二阶段会进行校验,校验内容包括表名、字段名、函数名、数据类型等。
3、逻辑计划(SqlNode -> RelNode/RexNode)
经过语法校验的AST树经过SqlToRelConverter.convertQuery调用,将SQL转换为RelNode,即生成逻辑计划LogicalPlan。需要注意的是,Flink为了统一“table api”和“sql执行”两种方式,会在这个阶段将RelNode封装成Operation。
4、优化器(RelNode -> LogicalNode -> ExecNode)
优化器的作用是将关系代数表达式(RelNode)转换为执行计划,用于执行引擎执行。优化器会使用过滤条件的下压、列裁剪等常见的优化规则进行优化,以生成更高效的执行计划。
Flink主要使用Calcite的优化器,采用HepPlanner和VolcanoPlanner这两种优化方式进行优化。
需要注意的是,在调用规则优化前,Flink会有一个内部的CommonSubGraphBasedOptimizer优化器用于提取多个执行计划 的共用逻辑。CommonSubGraphBasedOptimizer是Flink对于多流场景(常见为多个insert)的优化器,主要作用是提取共用的逻辑,生成有向无环图,避免对共用逻辑进行重复计算。
根据运行环境不同(批和流式),CommonSubGraphBasedOptimizer优化器有BatchCommonSubGraphBasedOptimizer和 StreamCommonSubGraphBasedOptimizer两种实现方式。从执行结果来看,CommonSubGraphBasedOptimizer优化类似于 Spark表的物化,最终目的都是避免数据重复计算。
源码中RelNodeBlock类注释很形象地描述了优化效果:
可以看到,在这段注释中,sink1和sink2这两个sink流有一段逻辑是共用的,即Join(a1=b2)。那么,优化器在优化阶段会将这段逻辑切分成3个block,其用的逻辑为单独的RelNodeBlock1,优化器将把这部分共用逻辑提取出来,避免重复计算。
中间表注册语法扩展
问题描述
值得注意的是,原生的Flink SQL只能通过调用table api来提取共用逻辑。在非table api的场景下,比如,数据经过计算后将根据字段条件被写入不同的kafka topic,SQL示例如下:
如果使用 StreamTableEnviroment.executeSql()去分别执行这两条insert sql,最终会异步生成两个任务,因此需要使用Flink提供的statementset先缓存多条insert sql,最后调用执行,在一个任务中完成多条数据流的处理。
可以发现,在这两条insert sql中存在复用逻辑,即select SBSTR(data, 0, 6) data、topic from source_table。预期的结果是Flink能够识别到这段共用逻辑并复用,但是实际情况并非预期中的,如下图:
问题分析
分析出现该问题的原因是:Flink在解析阶段将select SBSTR(data, 0, 6) data、topic from source_table解析成SqlNode(SqlSelect)并生成相应的RelNode。由于即便是相同逻辑的SQL,其解析为RelNode的摘要也是不同的。而Flink正是通过摘要来寻找复用的RelNode。因此,Flink也就不能识别到这段逻辑是可以共用的。
判断逻辑共用的源码如下:
解决思路
那么如何解决呢?首先想到的思路就是将这段共用逻辑注册成表,这样Flink就能知道这段逻辑是共用的。
目前有“注册视图”(create view as query)和“注册表”(registerTable)两种方式能够将共用逻辑注册成表。在Flink中,当执行‘create view as query' 创建视图或者调用registerTable注册表时,底层都会在catalog中创建临时表,区别在于create view创建表的实现类为CatalogViewImpl,而registerTable创建表的实现类为QueryOperationCatalogView。
前者CatalogViewImpl的查询逻辑使用字符串表示,而后者 QueryOperationCatalogView的查询逻辑已经被解析为QueryOperation。也就是说,执行创建视图的语句时,最终创建的临时表仅仅是缓存了查询部分的SQL语句,当其他命令使用这个临时表时还需要重新解析临时表中的查询语句,而【关注尚硅谷,轻松学IT】重新解析带来的问题就是创建新的RelNode,产生不同的摘要,这样Flink仍然不能够识别到这段共用逻辑并复用。
相反,regiterTable这样的方式就不需要对临时表中的查询语句进行重新解析。因此可以采用regiterTable将共用逻辑注册成表。示例代码如下:
但是,为了SQL化,就需要有语法去支持中间表注册,以屏蔽底层的api调用,实现用户无感知。新语法预期如下:
实现语法支持
如何实现新的语法,来支持中间表注册呢?目前有2种解决方案:
方案1:框架先使用正则匹配判断SQL类型,之后提取出临时表名和查询逻辑,比如上面的SQL经过正则匹配提取组之后可以得到表名为tmp,查询逻辑为'select SBSTR(data, 0, 6) data,topic from source_table',之后框架去调用table api进行注册。
方案2:修改flink-table模块源码扩展语法,实现对register table语法的支持。
从实现难度上来说,方案1的改动少,难度也较小,而方案2虽然改动较大,但是通用性更好。下面主要围绕方案2的实现展开。
register table语法扩展大致分为以下3个步骤:
Step1 SQL解析与校验
即修改Java CC相关文件,使得 SqlParser可以识别新的语法并解析为AST。
1.1 增加关键字“REGISTER”
首先需要让解析器识别新的关键字“REGISTER”,因此修改Parser.tdd,在keywords和nonReservedKeywords中分别增加“REGISTER”关键字。
1.2 创建SqlNode (SqlRegisterTable)
由于SqlParser解析SQL生成的AST数据类型为SqlNode,因此需要增加相应的SqlNode。在org.apache.flink.sql.parser.ddl下创建SqlRegisterTable:
修改Parser.tdd,在imports增加org.apache.flink.sql.parser.ddl.SqlRegisterTable,即刚才新建的 SqlRegisterTable的全类名路径。
1.3 增加语法解析模版
除了关键字和SqlNode,还需要相应的语法模版,让解析器能够把SQL解析为SqlRegisterTable。
修改parserImpls.ftl,增加语法解析模版:
Step2:SqlNode转为Operation
根据calcite在Flink中的执行流程,Flink会将SqlNode封装为Operation,因此需要创建相应的RegisterTableOperation,并修改相关的转换逻辑。
2.1 新建 RegisterTableOperation
2.2 增加解析逻辑
生成RegisterTableOperation之后,还需要让Flink能将 SqlNode转换成对应的Operation,因此我们要修改 SqlToOperationConverter.convert内部代码,增加解析逻辑,代码如下 :
Step3 Operation执行
即底层调用RegisterTable实现注册。由于流处理底层使用TableEnvironmentImpl进行相关SQL操作,比如常见的 executeSql(String statement) 操作 :
因此在executeOperation()方法中,需要识别 RegisterTableOperation进行额外操作,因此增加operation执行逻辑如下:
通过以上三步,完成源码修改,然后将flink-parser打包替换当前依赖,即可实现对register table语法的扩展。
Flink的SQL执行基于calcite,语法拓展的实现简要概括分为语法解析、转换、优化和执行4个阶段,其中会涉及到Java CC、Planner等知识,有兴趣的同学可以查阅相关内容做深入了解。
来源个推技术实践
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70010293/viewspace-2852986/ ,如需转载,请注明出处,否则将追究法律责任。