从零构建Flink SQL计算平台 - 3实现校验和调试

上文分析了 SQL 作业提交流程和实现思路,即通过 SQL Client 管理和创建 TableEnv、设置各类信息、调用 sqlUpdate、最后复用 SQL Client 的提交作业逻辑。现在需要将该过程进行细化,并考虑如何实现 SQL 校验和调试功能。

一、作业提交步骤细化

首先,作业提交作为一个服务接口,我们先看参数处理部分,要对参数中的 SQL 部分进行预处理,具体是将其中注释和空行去掉、按分号将多个 SQL 语句提取出来并分类,主要是 DDL 和 DML,即 source、sink、维表的DDL和具体 insert into 语句,这里为了让用户可以复用之前创建的DDL,可以扩展出一个 source、sink、维表的管理功能,在提交接口只需要传入相应 DDL 记录的 id 即可。此外,参数中还应该包括 UDF 信息(同样思路的 UDF 管理功能)、并行度配置、Checkpoint配置、启动 Savepoint 地址等信息。

其次,正式开始后要通过 SQL Client 管理和创建 TableEnv,为了实现无状态的 Web 集群以及用户隔离,要在每次提交请求创建新的 ExecutionContext 对象,它的 dependencies 参数包括上面提到的 UDF 的 jar 包路径,在 ExecutionContext 对象默认构造方法中会将它们加载到自己新创建的 classloader 中,然后根据传入的 FunctionEntry 信息创建 UDF,此外 ExecutionContext 构造方法还包括了一系列重要配置,如 flinkConfig 目标集群信息、planner 指定等。最后我们调用它的 createEnvironmentInstance 创建 tableEnv,实际是被包装在 EnvironmentInstance 对象中,其中还包括 ExecutionEnvironment、StreamExecutionEnvironment 等原 Table Api 中的常见对象。

有了 table api 的那些 env 对象后,我们可以继续配置其他信息,比如状态配置、checkpoint 配置、并行度配置、重启策略配置等。现在我们终于到了 SQL 相关阶段,我们使用 LocalExecutor 中的 applyUpdate(实际调用 tableEnv 的 sqlUpdate),逐条执行之前拆分 DDL 和 DML 语句,注意要先执行 DDL,因为只有 blink planner 解析 DDL 并进行 register 后,DML 语句中的源表和结果表才可用。

最后一步是复用 SQL Client 中的提交作业逻辑,即参考 LocalExecutor 中的 executeUpdateInternal 方法,其中先创建了 JobGraph,这里可以通过 jobGraph 的 setSavepointRestoreSettings 方法设置启动恢复点配置,JobGraph 中还包括所有依赖的 Jar 包,包括使用到的 UDF 和 kafka connector 等不同数据源和数据汇所依赖的 Jar 包,最后使用 ProgramDeployer 进行作业远程提交。

二、实现校验功能

在将整个过程细化后,我们接下来看看如何实现校验功能,这里的校验主要是指 SQL 校验,Flink 中 SQL 处理部分使用 Calcite 框架,而 Calcite 解析 SQL 主要分为 Parser、Validate、Optimize、Execute 几个步骤,搜索到如下介绍信息:

  • Parser. 此步中 Calcite 通过 Java CC 将 SQL 解析成未经校验的AST。

  • Validate. 该步骤主要作用是校证 Parser 步骤中的AST是否合法,如验证 SQL scheme、字段、函数等是否存在; SQL语句是否合法等. 此步完成之后就生成了RelNode树。

  • Optimize. 该步骤主要的作用优化 RelNode 树, 并将其转化成物理执行计划。主要涉及SQL规则优化如: 基于规则优化(RBO)及基于代价(CBO)优化; Optimze 这一步原则上来说是可选的, 通过 Validate 后的 RelNode 树已经可以直接转化物理执行计划,但现代的SQL解析器基本上都包括有这一步,目的是优化SQL执行计划。此步得到的结果为物理执行计划。

  • Execute,即执行阶段。此阶段主要做的是: 将物理执行计划转化成可在特定的平台执行的程序。如 Hive 与 Flink 都在在此阶段将物理执行计划 CodeGen 生成相应的可执行代码。

这里我们关心前两步的实现,我们深入跟踪 tableEnv 的 sqlUpdate 方法源码,可以看到其中调用了 planner.parse(stmt),以及 flinkPlanner.validate(sqlNode) ,正是上面前两步的具体实现,因此我们的校验功能就是复用前面作业提交的前面部分,直到对各个 SQL 执行 applyUpdate(不再进行后面的提交逻辑),如果没有抛出异常则说明解析正常返回成功,如果有则解析该异常并返回具体错误信息,都不用再执行后续生成 JobGraph 以及远程提交逻辑。

三、实现调试功能

最后我们来看看调试功能,首先是需求方面,上面的校验功能只是关注 SQL 合法性,而调试功能要关注整个作业是否满足功能和业务逻辑,即测试需求。同时我们还要考虑如何能方便的进行调试,因为结果输入和输出都有多种形式,测试数据难以输入,结果数据难以观察。这里我们以输入和输出都是 kafka 消息为例,我们期望调试的数据源表(topic)应该和正式的隔离,这样用户输入的测试数据不会影响正式环境,同时也可以根据需要从生产环境复制一部分数据到测试 topic 中。而结果数据则应该直接返回给用户前端页面,不需要再去进行消息消费。用户在操作方面和之前正式提交流程一样准备参数配置和 SQL 语句,只是之后使用调试按钮,而非提交按钮。

我们在调试服务中首先复用正式提交的执行 update 语句之前部分,随后为用户创建带有 debug 后缀的源表,并使用 applyUpdate 创建和注册,随后将 DML 中源表增加 debug 后缀并去掉 DML 的 insert into 部分,保留 select 和之后部分,然后复用 LocalExecutor 中的 executeQueryInternal 进行提交,和前面正式提交流程复用的 executeUpdateInternal 不同,这个方面里面使用 Table Api 注册和写入了一个 CollectStreamResult,它本质上是一个 socket sink,即将结果写入本机 ip 的 socket 中(这里为了获得远程集群能访问到的本机 ip 进行了一系列连接检测),最后用 ProgramDeployer 远程提交时也使用了 blocking 模式(之前的 detached 模式立刻返回),这是为了确认作业在远程真正执行成功,同时本地的 CollectStreamResult 建立 ServerSocket 并开始接收和处理结果,我们可以根据任务ID从 resultStore 中不断获得结果,并返回给用户。

posted @ 2020-03-08 10:55  Jeff_p  阅读(3147)  评论(0编辑  收藏  举报