基于Flink1.8 深入理解Flink Sql执行流程 + Flink Sql语法扩展

本文主要内容如下:

  • 介绍 Flink SQL 引擎:Calcite
  • 简述 Flink Table/SQL 执行流程
  • 以 Flink SQL Demo 为切入,结合调试过程,深入理解 Flink Streaming SQL
  • CodeGen
  • flink 语法扩展

对比 Spark SQL 的执行流程:https://blog.csdn.net/super_wj0820/article/details/100981862


1. Flink SQL 引擎:Calcite

1.1 Calcite 必知概念

下面是 Calcite 概念梳理:
Calcite 概念梳理
Calcite 概念表格展示:

类型描述特点
RelOptRuletransforms an expression into another。对 expression 做等价转换根据传递给它的 RelOptRuleOperand 来对目标 RelNode 树进行规则匹配,匹配成功后,会再次调用 matches() 方法(默认返回真)进行进一步检查。如果 mathes() 结果为真,则调用 onMatch() 进行转换。
ConverterRuleAbstract base class for a rule which converts from one calling convention to another without changing semantics.它是 RelOptRule 的子类,专门用来做数据源之间的转换(Calling convention),ConverterRule 一般会调用对应的 Converter 来完成工作,比如说:JdbcToSparkConverterRule 调用 JdbcToSparkConverter 来完成对 JDBC Table 到 Spark RDD 的转换。
RelNoderelational expression,RelNode 会标识其 input RelNode 信息,这样就构成了一棵 RelNode 树代表了对数据的一个处理操作,常见的操作有 Sort、Join、Project、Filter、Scan 等。它蕴含的是对整个 Relation 的操作,而不是对具体数据的处理逻辑。
ConverterA relational expression implements the interface Converter to indicate that it converts a physical attribute, or RelTrait of a relational expression from one value to another.用来把一种 RelTrait 转换为另一种 RelTrait 的 RelNode。如 JdbcToSparkConverter 可以把 JDBC 里的 table 转换为 Spark RDD。如果需要在一个 RelNode 中处理来源于异构系统的逻辑表,Calcite 要求先用 Converter 把异构系统的逻辑表转换为同一种 Convention。
RexNodeRow-level expression行表达式(标量表达式),蕴含的是对一行数据的处理逻辑。每个行表达式都有数据的类型。这是因为在 Valdiation 的过程中,编译器会推导出表达式的结果类型。常见的行表达式包括字面量 RexLiteral, 变量 RexVariable, 函数或操作符调用 RexCall 等。 RexNode 通过 RexBuilder 进行构建。
RelTraitRelTrait represents the manifestation of a relational expression trait within a trait definition.用来定义逻辑表的物理相关属性(physical property),三种主要的 trait 类型是:Convention、RelCollation、RelDistribution;
ConventionCalling convention used to repressent a single data source, inputs must be in the same convention继承自 RelTrait,类型很少,代表一个单一的数据源,一个 relational expression 必须在同一个 convention 中;
RelTraitDef主要有三种:ConventionTraitDef:用来代表数据源 RelCollationTraitDef:用来定义参与排序的字段;RelDistributionTraitDef:用来定义数据在物理存储上的分布方式(比如:single、hash、range、random 等);
RelOptClusterAn environment for related relational expressions during the optimization of a query.palnner 运行时的环境,保存上下文信息;
RelOptPlannerA RelOptPlanner is a query optimizer: it transforms a relational expression into a semantically equivalent relational expression, according to a given set of rules and a cost model.也就是优化器,Calcite 支持RBO(Rule-Based Optimizer) 和 CBO(Cost-Based Optimizer)。Calcite 的 RBO (HepPlanner)称为启发式优化器(heuristic implementation ),它简单地按 AST 树结构匹配所有已知规则,直到没有规则能够匹配为止;Calcite 的 CBO 称为火山式优化器(VolcanoPlanner)成本优化器也会匹配并应用规则,当整棵树的成本降低趋于稳定后,优化完成,成本优化器依赖于比较准确的成本估算。RelOptCost 和 Statistic 与成本估算相关;
RelOptCostdefines an interface for optimizer cost in terms of number of rows processed, CPU cost, and I/O cost.优化器成本模型会依赖;

1.2 Calcite 处理流程

Sql 的执行过程一般可以分为下图中的四个阶段,Calcite 同样也是这样:
Calcite 处理流程
这里为了讲述方便,把 SQL 的执行分为下面五个阶段(跟上面比比又独立出了一个阶段):

1.2.1 SQL 解析阶段(SQL–>SqlNode)

Calcite 使用 JavaCC 做 SQL 解析,JavaCC 根据 Calcite 中定义的 Parser.jj 文件,生成一系列的 java 代码,生成的 Java 代码会把 SQL 转换成 AST 的数据结构(这里是 SqlNode 类型)。

Javacc 实现一个 SQL Parser,它的功能有以下两个,这里都是需要在 jj 文件中定义的。

  1. 设计词法和语义,定义 SQL 中具体的元素;
  2. 实现词法分析器(Lexer)和语法分析器(Parser),完成对 SQL 的解析,完成相应的转换。

即:把 SQL 转换成为 AST (抽象语法树),在 Calcite 中用 SqlNode 来表示;

1.2.2 SqlNode 验证(SqlNode–>SqlNode)

经过上面的第一步,会生成一个 SqlNode 对象,它是一个未经验证的抽象语法树,下面就进入了一个语法检查阶段,语法检查前需要知道元数据信息,这个检查会包括表名、字段名、函数名、数据类型的检查。

即:语法检查,根据元数据信息进行语法验证,验证之后还是用 SqlNode 表示 AST 语法树;

1.2.3 语义分析(SqlNode–>RelNode/RexNode)

经过第二步之后,这里的 SqlNode 就是经过语法校验的 SqlNode 树,接下来这一步就是将 SqlNode 转换成 RelNode/RexNode,也就是生成相应的逻辑计划(Logical Plan)

即:语义分析,根据 SqlNode及元信息构建 RelNode 树,也就是最初版本的逻辑计划(Logical Plan);

1.2.4 优化阶段(RelNode–>RelNode)

第四阶段,也就是 Calcite 的核心所在,优化器进行优化的地方,如过滤条件的下压(push down),在进行 join 操作前,先进行 filter 操作,这样的话就不需要在 join 时进行全量 join,减少参与 join 的数据量等。

在 Calcite 中,提供了两种 planner:HepPlanner 和 VolcanoPlanner,详细可参考下文。

即:逻辑计划优化,优化器的核心,根据前面生成的逻辑计划按照相应的规则(Rule)进行优化;

1.2.5 生成ExecutionPlan

针对不同的大数据组件,将优化后的plan映射到最终的大数据引擎,如折射成Flink图。

1.3 Calcite 优化器

优化器的作用:将解析器生成的关系代数表达式转换成执行计划,供执行引擎执行,在这个过程中,会应用一些规则优化,以帮助生成更高效的执行计划。

Calcite 中 RelOptPlanner 是 Calcite 中优化器的基类:
Calcite 中优化器
Calcite 中关于优化器提供了两种实现:

  1. HepPlanner:就是基于规则优化RBO 的实现,它是一个启发式的优化器,按照规则进行匹配,直到达到次数限制(match 次数限制)或者遍历一遍后不再出现 rule match 的情况才算完成;
  2. VolcanoPlanner:就是基于成本优化CBO 的实现,它会一直迭代 rules,直到找到 cost 最小的 paln。

Calcite 参考文章:
https://matt33.com/2019/03/07/apache-calcite-process-flow/
https://matt33.com/2019/03/17/apache-calcite-planner/


2. 简述 Flink Table/SQL 执行流程

Flink Table API&SQL 为流式数据和静态数据的关系查询保留统一的接口,而且利用了Calcite的查询优化框架和SQL parser。

该设计是基于Flink已构建好的API构建的,Flink的 core API 和引擎的所有改进都会自动应用到Table API和SQL上。

Flink Table/SQL 执行流程

2.1 Flink Sql 执行流程

一条stream sql从提交到calcite解析、优化最后到flink引擎执行,一般分为以下几个阶段:

  1. Sql Parser: 将sql语句通过java cc解析成AST(语法树),在calcite中用SqlNode表示AST;
  2. Sql Validator: 结合数字字典(catalog)去验证sql语法;
  3. 生成Logical Plan: 将sqlNode表示的AST转换成LogicalPlan, 用relNode表示;
  4. 生成 optimized LogicalPlan: 先基于calcite rules 去优化logical Plan,
    再基于flink定制的一些优化rules去优化logical Plan;
  5. 生成Flink PhysicalPlan: 这里也是基于flink里头的rules,将optimized LogicalPlan转成成Flink的物理执行计划;
  6. 将物理执行计划转成Flink ExecutionPlan: 就是调用相应的tanslateToPlan方法转换和利用CodeGen元编程成Flink的各种算子。

2.2 Flink Table Api 执行流程

而如果是通过table api来提交任务的话,也会经过calcite优化等阶段,基本流程和直接运行sql类似:

  1. table api parser: flink会把table api表达的计算逻辑也表示成一颗树,用treeNode去表式;
    在这棵树上的每个节点的计算逻辑用Expression来表示。
  2. Validate: 会结合数字字典(catalog)将树的每个节点的Unresolved Expression进行绑定,生成Resolved Expression;
  3. 生成Logical Plan: 依次遍历数的每个节点,调用construct方法将原先用treeNode表达的节点转成成用calcite 内部的数据结构relNode 来表达。即生成了LogicalPlan, 用relNode表示;
  4. 生成 optimized LogicalPlan: 先基于calcite rules 去优化logical Plan,
    再基于flink定制的一些优化rules去优化logical Plan;
  5. 生成Flink PhysicalPlan: 这里也是基于flink里头的rules,将optimized LogicalPlan转成成Flink的物理执行计划;
  6. 将物理执行计划转成Flink ExecutionPlan: 就是调用相应的tanslateToPlan方法转换和利用CodeGen元编程成Flink的各种算子。

2.3 Flink Table/SQL 执行流程 的 异同

可以看出来,Table API 与 SQL 在获取 RelNode 之后是一样的流程,只是获取 RelNode 的方式有所区别:

  • Table API :通过使用 RelBuilder来拿到RelNode(LogicalNode与Expression分别转换成RelNode与RexNode),具体实现这里就不展开了;
  • SQL :通过使用Planner。首先通过parse方法将用户使用的SQL文本转换成由SqlNode表示的parse tree。接着通过validate方法,使用元信息来resolve字段,确定类型,验证有效性等等。最后通过rel方法将SqlNode转换成RelNode;

在flink提供两种API进行关系型查询,Table API 和 SQL。这两种API的查询都会用包含注册过的Table的catalog进行验证,除了在开始阶段从计算逻辑转成logical plan有点差别以外,之后都差不多。同时在stream和batch的查询看起来也是完全一样。只不过flink会根据数据源的性质(流式和静态)使用不同的规则进行优化, 最终优化后的plan转传成常规的Flink DataSet 或 DataStream 程序。


3. 以 Flink SQL Demo 为切入,深入理解 Flink Streaming SQL

3.1 demo SQL 说明

参考官网 StreamSQLExample Demo,Demo SQL 如下:

SELECT
	*
FROM
	(
		(
			SELECT
				*
			FROM
				OrderA
			WHERE
				user < 3
		)
		UNION ALL
		(
			SELECT
				*
			FROM
				OrderB
			WHERE
				product <> 'rubber'
		)
	) OrderAll
WHERE
	amount > 2

表OrderA定义三个字段:user, product, amount,先分别做select查询,再将查询结果 union,最后做select,最外层加了一个Filter,以便触发Filter下推及合并。

3.2 测试代码及说明

以下代码修改自官网 StreamSQLExample Demo,可直接运行:

/**
 * Simple example for demonstrating the use of SQL on a Stream Table in Java.
 *
 * <p>This example shows how to:
 *  - Convert DataStreams to Tables
 *  - Register a Table under a name
 *  - Run a StreamSQL query on the registered Table
 *
 */
public class StreamSQLExample {

	// *************************************************************************
	//     PROGRAM
	// *************************************************************************

	public static void main(String[] args) throws Exception {

		// set up execution environment
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		env.setParallelism(1);
		StreamTableEnvironment tEnv = StreamTableEnvironment.getTableEnvironment(env);

		DataStream<Order> orderA = env.fromCollection(Arrays.asList(
			new Order(1L, "beer", 3),
			new Order(1L, "diaper", 4),
			new Order(3L, "rubber", 2)));

		DataStream<Order> orderB = env.fromCollection(Arrays.asList(
			new Order(2L, "pen", 3),
			new Order(2L, "rubber", 3),
			new Order(4L, "beer", 1)));

		// register DataStream as Table
		tEnv.registerDataStream("OrderA", orderA, "user, product, amount");
		tEnv.registerDataStream("OrderB", orderB, "user, product, amount");

		// union the two tables
		Table result = tEnv.sqlQuery("SELECT " +
				"* " +
				"FROM " +
				"( " +
				"SELECT " +
				"* " +
				"FROM " +
				"OrderA " +
				"WHERE " +
				"user < 3 " +
				"UNION ALL " +
				"SELECT " +
				"* " +
				"FROM " +
				"OrderB " +
				"WHERE " +
				"product <> 'rubber' " +
				") OrderAll " +
				"WHERE " +
				"amount > 2");

		System.out.println(tEnv.explain(result));

		tEnv.toAppendStream(result, Order.class).print();

		env.execute();
	}

	// *************************************************************************
	//     USER DATA TYPES
	// *************************************************************************

	/**
	 * Simple POJO.
	 */
	public static class Order {
		public Long user;
		public String product;
		public int amount;

		public Order() {
		}

		public Order(Long user, String product, int amount) {
			this.user = user;
			this.product = product;
			this.amount = amount;
		}

		@Override
		public String toString() {
			return "Order{" +
				"user=" + user +
				", product='" + product + '\'' +
				", amount=" + amount +
				'}';
		}
	}
}

运行结果如下:
结果

3.3 结合 Flink SQL 执行流程 及 调试 详细说明

3.3.1 预览 AST、Optimized Logical Plan、Physical Execution Plan

上述代码中,通过 System.out.println(tEnv.explain(result)); 方法可以打出待执行Sql的抽象语法树(Abstract Syntax Tree)、优化后的逻辑计划以及物理计划:

== Abstract Syntax Tree ==
LogicalProject(user=[$0], product=[$1], amount=[$2])
  LogicalFilter(condition=[>($2, 2)])
    LogicalUnion(all=[true])
      LogicalProject(user=[$0], product=[$1], amount=[$2])
        LogicalFilter(condition=[<($0, 3)])
          LogicalTableScan(table=[[OrderA]])
      LogicalProject(user=[$0], product=[$1], amount=[$2])
        LogicalFilter(condition=[<>($1, _UTF-16LE'rubber')])
          LogicalTableScan(table=[[OrderB]])

== Optimized Logical Plan ==
DataStreamUnion(all=[true], union all=[user, product, amount])
  DataStreamCalc(select=[user, product, amount], where=[AND(<(user, 3), >(amount, 2))])
    DataStreamScan(table=[[OrderA]])
  DataStreamCalc(select=[user, product, amount], where=[AND(<>(product, _UTF-16LE'rubber'), >(amount, 2))])
    DataStreamScan(table=[[OrderB]])

== Physical Execution Plan ==
Stage 1 : Data Source
	content : collect elements with CollectionInputFormat

Stage 2 : Data Source
	content : collect elements with CollectionInputFormat

	Stage 3 : Operator
		content : from: (user, product, amount)
		ship_strategy : FORWARD

		Stage 4 : Operator
			content : where: (AND(<(user, 3), >(amount, 2))), select: (user, product, amount)
			ship_strategy : FORWARD

			Stage 5 : Operator
				content : from: (user, product, amount)
				ship_strategy : FORWARD

				Stage 6 : Operator
					content : where: (AND(<>(product, _UTF-16LE'rubber'), >(amount, 2))), select: (user, product, amount)
					ship_strategy : FORWARD

3.3.2 SQL 解析阶段(SQL–>SqlNode)

和前面介绍的 Calcite 处理流程一致,此处Flink解析Flink SQL 的语法和词法解析 完全依赖Calcite提供的SqlParser。

tEnv.sqlQuery() 方法中,下面的 Step-1 即为SQL解析过程,入参为 待解析的SQL,返回解析后的 SqlNode 对象。

*TableEnvironment.scala*

def sqlQuery(query: String): Table = {

    val planner = new FlinkPlannerImpl(getFrameworkConfig, getPlanner, getTypeFactory)
    // Step-1: SQL 解析阶段(SQL–>SqlNode), 把 SQL 转换成为 AST (抽象语法树),在 Calcite 中用 SqlNode 来表示
    val parsed = planner.parse(query)

    if (null != parsed && parsed.getKind.belongsTo(SqlKind.QUERY)) {

      // Step-2: SqlNode 验证(SqlNode–>SqlNode),语法检查,根据元数据信息进行语法验证,验证之后还是用 SqlNode 表示 AST 语法树;
      val validated = planner.validate(parsed)

      // Step-3: 语义分析(SqlNode–>RelNode/RexNode),根据 SqlNode及元信息构建 RelNode 树,也就是最初版本的逻辑计划(Logical Plan)
      val relational = planner.rel(validated)

      new Table(this, LogicalRelNode(relational.rel))
    } else {
      ...
    }
  }

被解析后的SqlNode AST,每个SQL组成会翻译成一个节点:
SqlNode  AST

3.3.3 SqlNode 验证(SqlNode–>SqlNode)

SQL在被SqlParser解析后,得到SqlNode组成的 抽象语法树(AST),此后还要根据注册的Catalog对该 SqlNode AST 进行验证。

以下语句注册表OrderA和OrderB:
tEnv.registerDataStream(“OrderA”, orderA, “user, product, amount”);
tEnv.registerDataStream(“OrderB”, orderB, “user, product, amount”);

tEnv.sqlQuery() 方法中,下面的 Step-2 即为SQL解析过程,入参为 待验证的SqlNode AST,返回验证后的 SqlNode 对象。

**TableEnvironment.scala**

def sqlQuery(query: String): Table = {

    val planner = new FlinkPlannerImpl(getFrameworkConfig, getPlanner, getTypeFactory)
    // Step-1: SQL 解析阶段(SQL–>SqlNode), 把 SQL 转换成为 AST (抽象语法树),在 Calcite 中用 SqlNode 来表示
    val parsed = planner.parse(query)

    if (null != parsed && parsed.getKind.belongsTo(SqlKind.QUERY)) {

      // Step-2: SqlNode 验证(SqlNode–>SqlNode),语法检查,根据元数据信息进行语法验证,验证之后还是用 SqlNode 表示 AST 语法树;
      val validated = planner.validate(parsed)

      // Step-3: 语义分析(SqlNode–>RelNode/RexNode),根据 SqlNode及元信息构建 RelNode 树,也就是最初版本的逻辑计划(Logical Plan)
      val relational = planner.rel(validated)

      new Table(this, LogicalRelNode(relational.rel))
    } else {
      ...
    }
  }

相对于Calcite原生的SQL校验,Flink拓展了语法校验范围,如Flink支持自定义的FunctionCatalog,用于校验SQL Function的入参个数及类型的相关校验,具体用法和细节后续补充。

下面为SQL校验的过程:

**FlinkPlannerImpl.scala**

def validate(sqlNode: SqlNode): SqlNode = {
    validator = new FlinkCalciteSqlValidator(
      operatorTable,
      createCatalogReader(false),
      typeFactory)
    validator.setIdentifierExpansion(true)
    try {
      validator.validate(sqlNode)
    }
    catch {
      case e: RuntimeException =>
        throw new ValidationException(s"SQL validation failed. ${e.getMessage}", e)
    }
  }

至此,Flink引擎已将 用户业务 转化成 如下抽象语法树(AST),此AST并未应用任何优化策略,只是Sql节点的原生映射 :

== Abstract Syntax Tree ==
LogicalProject(user=[$0], product=[$1], amount=[$2])
  LogicalFilter(condition=[>($2, 2)])
    LogicalUnion(all=[true])
      LogicalProject(user=[$0], product=[$1], amount=[$2])
        LogicalFilter(condition=[<($0, 3)])
          LogicalTableScan(table=[[OrderA]])
      LogicalProject(user=[$0], product=[$1], amount=[$2])
        LogicalFilter(condition=[<>($1, _UTF-16LE'rubber')])
          LogicalTableScan(table=[[OrderB]])

3.3.4 语义分析(SqlNode–>RelNode/RexNode)

前面经过的SQL解析和SQL验证之后得到的SqlNode,仅仅是将SQL解析到java数据结构的固定节点上,并没有给出相关节点之间的关联关系以及每个节点的类型等信息,因此还需要将SqlNode转换为逻辑计划(RelNode)。

tEnv.sqlQuery() 方法中,下面的 Step-3 即为SQL解析过程,入参为 验证后的SqlNode,返回的是包含RelNode信息的RelRoot对象。

**TableEnvironment.scala**

def sqlQuery(query: String): Table = {

    val planner = new FlinkPlannerImpl(getFrameworkConfig, getPlanner, getTypeFactory)
    // Step-1: SQL 解析阶段(SQL–>SqlNode), 把 SQL 转换成为 AST (抽象语法树),在 Calcite 中用 SqlNode 来表示
    val parsed = planner.parse(query)

    if (null != parsed && parsed.getKind.belongsTo(SqlKind.QUERY)) {

      // Step-2: SqlNode 验证(SqlNode–>SqlNode),语法检查,根据元数据信息进行语法验证,验证之后还是用 SqlNode 表示 AST 语法树;
      val validated = planner.validate(parsed)

      // Step-3: 语义分析(SqlNode–>RelNode/RexNode),根据 SqlNode及元信息构建 RelNode 树,也就是最初版本的逻辑计划(Logical Plan)
      val relational = planner.rel(validated)

      new Table(this, LogicalRelNode(relational.rel))
    } else {
      ...
    }
  }

下面为构建逻辑计划的过程:

**FlinkPlannerImpl.scala**

def rel(validatedSqlNode: SqlNode): RelRoot = {
    try {
      assert(validatedSqlNode != null)
      val rexBuilder: RexBuilder = createRexBuilder
      val cluster: RelOptCluster = FlinkRelOptClusterFactory.create(planner, rexBuilder)
      val sqlToRelConverter: SqlToRelConverter = new SqlToRelConverter(
        new ViewExpanderImpl,
        validator,
        createCatalogReader(false),
        cluster,
        convertletTable,
        sqlToRelConverterConfig)
      root = sqlToRelConverter.convertQuery(validatedSqlNode, false, true)
      root
    } catch {
      case e: RelConversionException => throw new TableException(e.getMessage)
    }
  }

至此,用户通过 StreamTableEnvironment 对象 注册的Calatlog信息 和 业务Sql 都 转化成了 逻辑计划(Logical Plan),同时,TableApi和SqlApi 也在 Logical Plan 这里达成一致,后续进行的优化阶段、生成物理计划和生成DataStream,都是相同的过程。

3.3.5 优化阶段(Logical RelNode–>FlinkLogicalRel)

tEnv.sqlQuery() 返回 Table 对象,在Flink中,Table对象既可通过TableApi生成,也可以通过SqlApi生成,TableApi和SqlApi至此达成一致。

在业务代码中,toAppendStream方法会进行 Logical Plan 的优化、生成物理计划以及生成DataStream的过程:

tEnv.toAppendStream(result, Order.class).print();

跟踪代码,会进入 StreamTableEnvironment.scala 的 translate 方法:

**StreamTableEnvironment.scala**

protected def translate[A](
      table: Table,
      queryConfig: StreamQueryConfig,
      updatesAsRetraction: Boolean,
      withChangeFlag: Boolean)(implicit tpe: TypeInformation[A]): DataStream[A] = {
    // 获取 逻辑计划(Logical Plan)
    val relNode = table.getRelNode

    // Step-4: 优化阶段 + Step-5: 生成物理计划 
    val dataStreamPlan = optimize(relNode, updatesAsRetraction)

    val rowType = getResultType(relNode, dataStreamPlan)

    // Step-6: 转成DataStream
    translate(dataStreamPlan, rowType, queryConfig, withChangeFlag)
  }
3.3.5.1 FlinkRuleSets

Calcite框架允许我们使用规则来优化逻辑计划,Flink在Optimize过程中,使用 FlinkRuleSets 定义优化规则进行优化:
FlinkRuleSets 描述
此处,简单描述下各RuleSet的作用:

  • FlinkRuleSets.TABLE_SUBQUERY_RULES :子查询优化,应用HepPlanner规则优化
  • FlinkRuleSets.EXPAND_PLAN_RULES :扩展计划优化,应用HepPlanner规则优化
  • FlinkRuleSets.POST_EXPAND_CLEAN_UP_RULES :扩展计划优化,应用HepPlanner规则优化
  • FlinkRuleSets.LOGICAL_OPT_RULES :逻辑计划优化( Logical Plan),应用VolcanoPlanner规则优化
  • FlinkRuleSets.DATASET_NORM_RULES :正常化批处理,应用HepPlanner规则优化
  • FlinkRuleSets.DATASET_OPT_RULES :优化批处理,应用Volcano规则优化
  • FlinkRuleSets.DATASTREAM_NORM_RULES :正常化流式计算,应用HepPlanner规则优化
  • FlinkRuleSets.DATASTREAM_OPT_RULES :优化流式计算,应用Volcano规则优化
  • FlinkRuleSets.DATASTREAM_DECO_RULES :装饰流式计算,应用HepPlanner规则优化

针对批/流应用,采用不同的Rule进行优化,下面是各规则的优化过程:

**StreamTableEnvironment.scala**

private[flink] def optimize(relNode: RelNode, updatesAsRetraction: Boolean): RelNode = {
    // 优化子查询,根据 TABLE_SUBQUERY_RULES 应用 HepPlanner 规则优化
    val convSubQueryPlan = optimizeConvertSubQueries(relNode)

    // 扩展计划优化,根据 EXPAND_PLAN_RULES 和 POST_EXPAND_CLEAN_UP_RULES 应用 HepPlanner 规则优化
    val expandedPlan = optimizeExpandPlan(convSubQueryPlan)

    val decorPlan = RelDecorrelator.decorrelateQuery(expandedPlan)
    val planWithMaterializedTimeAttributes =
      RelTimeIndicatorConverter.convert(decorPlan, getRelBuilder.getRexBuilder)

    // 正常化流式计算,根据 DATASTREAM_NORM_RULES 应用 HepPlanner 规则优化
    val normalizedPlan = optimizeNormalizeLogicalPlan(planWithMaterializedTimeAttributes)

    // 逻辑计划优化,根据 LOGICAL_OPT_RULES 应用 VolcanoPlanner 规则优化
    val logicalPlan = optimizeLogicalPlan(normalizedPlan)

    // 优化流式计算,根据 DATASTREAM_OPT_RULES 应用 Volcano 规则优化
    val physicalPlan = optimizePhysicalPlan(logicalPlan, FlinkConventions.DATASTREAM)

    // 装饰流式计算,根据 DATASTREAM_DECO_RULES 应用 HepPlanner 规则优化
    optimizeDecoratePlan(physicalPlan, updatesAsRetraction)
  }

由上述过程也可以看出,Flink基于FlinkRuleSets的rule进行转换的过程中,既包含了 优化 logical Plan 的过程,也包括了生成 Flink PhysicalPlan 的过程。

3.3.5.2 Flink 逻辑计划优化

从 3.3.5.1 节的优化过程可看出,Flink在进行 logical Plan 优化之前,会应用 HepPlanner 针对 TABLE_SUBQUERY_RULES、EXPAND_PLAN_RULES、POST_EXPAND_CLEAN_UP_RULES、DATASTREAM_NORM_RULES 这些规则进行预处理,处理完之后 才会应用 VolcanoPlanner 针对 LOGICAL_OPT_RULES 中罗列的优化规则,尝试使用不同的规则优化,试图计算出最优的一种优化plan返回。

1. Logic RelNode :normalizedPlan

应用 HepPlanner 针对 预处理规则 进行预处理后,会得到 Logic RelNode :
Logic RelNode
对比 Sql解析之后得到的 SqlNode 发现, Logic RelNode 同样持有 Sql 各组成的 映射信息,除此之外,相比SqlNode,Logic RelNode 加入了各节点的 rowType 类型信息。

2. Optimized Logical RelNode :logicalPlan

VolcanoPlanner 根据 FlinkRuleSets.LOGICAL_OPT_RULES 找到最优的执行Planner,并转换为 Flink Logical RelNode 返回:
logicalPlan

3.3.6 生成物理计划(LogicalRelNode–>Physic Plan)

应用 VolcanoPlanner 针对 FlinkRuleSets.DATASTREAM_OPT_RULES,将 Optimized Logical RelNode 转换为 Flink Physic Plan (Flink Logical RelNode -> DataStream RelNode)。

在这里插入图片描述
此时,用户的执行计划已被优化为如下计划:

== Optimized Logical Plan ==
DataStreamUnion(all=[true], union all=[user, product, amount])
  DataStreamCalc(select=[user, product, amount], where=[AND(<(user, 3), >(amount, 2))])
    DataStreamScan(table=[[OrderA]])
  DataStreamCalc(select=[user, product, amount], where=[AND(<>(product, _UTF-16LE'rubber'), >(amount, 2))])
    DataStreamScan(table=[[OrderB]])

如果是 RetractStream 则还会使用 FlinkRuleSets.DATASTREAM_DECO_RULES 进行 Retract特征 的一个包装:
在这里插入图片描述
至此,Step-4: 优化阶段 + Step-5: 生成物理计划 已完成。

3.3.7 生成DataStream(Physic Plan–>DataStream)

StreamTableEnvironment.scala 的 translate 方法中最后一步,Step-6:转成DataStream,此处将用户的业务Sql最终转成 Stream Api 执行。

**StreamTableEnvironment.scala**

protected def translate[A](
      table: Table,
      queryConfig: StreamQueryConfig,
      updatesAsRetraction: Boolean,
      withChangeFlag: Boolean)(implicit tpe: TypeInformation[A]): DataStream[A] = {
    // 获取 逻辑计划(Logical Plan)
    val relNode = table.getRelNode

    // Step-4: 优化阶段 + Step-5: 生成物理计划 
    val dataStreamPlan = optimize(relNode, updatesAsRetraction)

    val rowType = getResultType(relNode, dataStreamPlan)

    // Step-6: 转成DataStream
    translate(dataStreamPlan, rowType, queryConfig, withChangeFlag)
  }

跟踪代码,查看 translate 方法的具体实现:

**StreamTableEnvironment.scala**

protected def translate[A](
      logicalPlan: RelNode,
      logicalType: RelDataType,
      queryConfig: StreamQueryConfig,
      withChangeFlag: Boolean)
      (implicit tpe: TypeInformation[A]): DataStream[A] = {

    // ...
	
    // get CRow plan :关键方法
    val plan: DataStream[CRow] = translateToCRow(logicalPlan, queryConfig)

    // ...
  }

protected def translateToCRow(
    logicalPlan: RelNode,
    queryConfig: StreamQueryConfig): DataStream[CRow] = {

    logicalPlan match {
      case node: DataStreamRel =>
        // 依次递归调用每个节点的 translateToPlan 方法,将 DataStreamRelNode 转化为 DataStream,最终生成 DataStreamGraph
        node.translateToPlan(this, queryConfig)
      case _ =>
        throw new TableException("Cannot generate DataStream due to an invalid logical plan. " +
          "This is a bug and should not happen. Please file an issue.")
    }
  }

针对优化后得到的逻辑计划(实际已转成物理计划 DataStreamRel),由外到内遍历各节点,将 DataStreamRel Node 转化为 DataStream,以下面物理计划为例:

== Optimized Logical Plan ==
DataStreamUnion(all=[true], union all=[user, product, amount])
  DataStreamCalc(select=[user, product, amount], where=[AND(<(user, 3), >(amount, 2))])
    DataStreamScan(table=[[OrderA]])
  DataStreamCalc(select=[user, product, amount], where=[AND(<>(product, _UTF-16LE'rubber'), >(amount, 2))])
    DataStreamScan(table=[[OrderB]])

依次递归调用 DataStreamUnion、DataStreamCalc、DataStreamScan 类中 重写的 translateToPlan 方法,将各节点的 DataStreamRel 实现 转化为 DataStream 执行计划的实现。

== Physical Execution Plan ==
Stage 1 : Data Source
	content : collect elements with CollectionInputFormat

Stage 2 : Data Source
	content : collect elements with CollectionInputFormat

	Stage 3 : Operator
		content : from: (user, product, amount)
		ship_strategy : FORWARD

		Stage 4 : Operator
			content : where: (AND(<(user, 3), >(amount, 2))), select: (user, product, amount)
			ship_strategy : FORWARD

			Stage 5 : Operator
				content : from: (user, product, amount)
				ship_strategy : FORWARD

				Stage 6 : Operator
					content : where: (AND(<>(product, _UTF-16LE'rubber'), >(amount, 2))), select: (user, product, amount)
					ship_strategy : FORWARD

备注:在生成 DataStream 的过程中,使用到CodeGen生成成Flink的各种算子。后面会详细说明

补充:
关于 DataStreamRel 的类继承关系如下图所示,RelNode 是 Calcite 定义的 Sql节点关系 数据结构,FlinkRelNode 继承自 RelNode,其有三个实现,分别是FlinkLogicalRel、DataStreamRel、DataSetRel,分别对应Flink内部 对 Sql 表达式的 逻辑计划的描述以及物理计划的描述。
在这里插入图片描述

在这里插入图片描述

3.4 总结Flink Sql执行流程

图示总结:
在这里插入图片描述


4. CodeGen

在递归调用各个节点 DataStreamRel 的 translateToPlan 方法时,会利用CodeGen元编程成Flink的各种算子,就相当于我们直接利用Flink的DataSet或DataStream API开发的程序。

== Optimized Logical Plan ==
DataStreamUnion(all=[true], union all=[user, product, amount])
  DataStreamCalc(select=[user, product, amount], where=[AND(<(user, 3), >(amount, 2))])
    DataStreamScan(table=[[OrderA]])
  DataStreamCalc(select=[user, product, amount], where=[AND(<>(product, _UTF-16LE'rubber'), >(amount, 2))])
    DataStreamScan(table=[[OrderB]])

还是以上面的Demo为例,跟踪进 DataStreamScan 的 translateToPlan 方法中,会发现相关逻辑:

  1. 首先生成 function 代码的字符串形式,并封装成 GeneratedFunction 对象;
  2. 然后使用 CodeGen 进行编译;
  3. 在需要使用 Function 的时候使用反射进行加载使用。

后续在 扩展 flink语法(如join维表)时,需要针对上述步骤,拼接生成 function 的字符串形式。
在这里插入图片描述
在 FunctionCodeGenerator.scala 中,可调试至图处,查看拼接成的 Function String形式,以方便调试。


5. flink 语法扩展

了解完 Flink Sql 的执行流程之后,就可以针对 Flink Sql 做语法、功能上的扩展。

在Flink老版本上,Flink不支持 COUNT(DISTINCT aaa) 语法,但是如果需要对 Flink 做此功能拓展,需要结合 前面说到的 Flink Sql 执行流程,做相应修改。

修改点:

  1. 在进行 Rule 规则匹配时,放开对 Distinct 的限制
  2. DataStreamRelNode 转为 DataStream 过程中,拼接CodeGen所需的 Function String

5.1 在进行 Rule 规则匹配时,放开对 Distinct 的限制

DATASTREAM_OPT_RULES.DataStreamGroupWindowAggregateRule 中放开对 Distinct 的限制:
在这里插入图片描述

5.2 拼接CodeGen所需的 Function String

内部实现…

  • 27
    点赞
  • 76
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值