Flink优化器基础

文章详细阐述了Flink在批处理模式下的优化流程,包括将计划树拆分为RelNodeBlock,对每个块进行优化,添加和应用优化规则,以及FlinkHepRuleSetProgram的使用。优化过程中涉及到了规则匹配顺序、最大匹配次数和规则执行类型等关键概念。最后,文章提到了RelOptRule及其重要组成部分,如RelOptRuleOperand和onMatch方法在规则应用中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.流程

  优化调用到CommonSubGraphBasedOptimizer的optimize,之后是doOptimize,doOptimize是空实现,由两个子类各自实现:BatchCommonSubGraphBasedOptimizer、StreamCommonSubGraphBasedOptimizer,分别对应批和流的优化。
  之后以BatchCommonSubGraphBasedOptimizer为主。在doOptimize中,首先把计划树拆分成RelNodeBlock,然后对每个RelNodeBlock去进行优化。

1.1.RelNodeBlock

  这个是把计划树拆分成多个,一般的SQL应该是用不到的,针对的是多输出的情况,一般Table API会遇到这种情况。

* {{{-
*  val sourceTable = tEnv.scan("test_table").select('a, 'b, 'c)
*  val leftTable = sourceTable.filter('a > 0).select('a as 'a1, 'b as 'b1)
*  val rightTable = sourceTable.filter('c.isNotNull).select('b as 'b2, 'c as 'c2)
*  val joinTable = leftTable.join(rightTable, 'a1 === 'b2)
*  joinTable.where('a1 >= 70).select('a1, 'b1).writeToSink(sink1)
*  joinTable.where('a1 < 70 ).select('a1, 'c2).writeToSink(sink2)
* }}}
*
* the RelNode DAG is:
*
* {{{-
* Sink(sink1)     Sink(sink2)
*    |               |
* Project(a1,b1)  Project(a1,c2)
*    |               |
* Filter(a1>=70)  Filter(a1<70)
*       \          /
*        Join(a1=b2)
*       /           \
* Project(a1,b1)  Project(b2,c2)
*      |             |
* Filter(a>0)     Filter(c is not null)
*      \           /
*      Project(a,b,c)
*          |
*       TableScan
* }}}

* The [[RelNodeBlock]] plan is:
* {{{-
* RelNodeBlock2  RelNodeBlock3
*        \            /
*        RelNodeBlock1
* }}}

1.2.后续步骤

  后续核心是optimizeTree的方法,里面主要做两个事:1、添加优化规则; 2、基于优化规则做优化
  优化完成后有一个步骤,应该是做sink的封装的

optimizedTree match {
  case _: LegacySink | _: Sink => // ignore
  case _ =>
    val name = createUniqueIntermediateRelTableName
    val intermediateRelTable =
      new IntermediateRelTable(Collections.singletonList(name), optimizedTree)
    val newTableScan = wrapIntermediateRelTableToTableScan(intermediateRelTable, name)
    block.setNewOutputNode(newTableScan)
    block.setOutputTableName(name)
}
block.setOptimizedPlan(optimizedTree)

1.3.添加优化规则

  添加优化规则就是把规则放进一个列表,之后会遍历列表应用规则。

val programs = TableConfigUtils
  .getCalciteConfig(config)
  .getBatchProgram
  .getOrElse(FlinkBatchProgram.buildProgram(config))

  规则集在FlinkBatchProgram中,通过buildProgram会一个个加进去。完整的规则集为

val SUBQUERY_REWRITE = "subquery_rewrite"
val TEMPORAL_JOIN_REWRITE = "temporal_join_rewrite"
val DECORRELATE = "decorrelate"
val DEFAULT_REWRITE = "default_rewrite"
val PREDICATE_PUSHDOWN = "predicate_pushdown"
val JOIN_REORDER = "join_reorder"
val JOIN_REWRITE = "join_rewrite"
val PROJECT_REWRITE = "project_rewrite"
val WINDOW = "window"
val LOGICAL = "logical"
val LOGICAL_REWRITE = "logical_rewrite"
val TIME_INDICATOR = "time_indicator"
val PHYSICAL = "physical"
val PHYSICAL_REWRITE = "physical_rewrite"

  每一个规则里面都包含多个RuleSet,RuleSet里面集合了很多的rule,用来共同完成一个规则操作。rule有calcite的,也有flink自定义的。

// rewrite special temporal join plan
chainedProgram.addLast(
  TEMPORAL_JOIN_REWRITE,
  FlinkGroupProgramBuilder
    .newBuilder[BatchOptimizeContext]
    .addProgram(
      FlinkHepRuleSetProgramBuilder.newBuilder
        .setHepRulesExecutionType(HEP_RULES_EXECUTION_TYPE.RULE_SEQUENCE)
        .setHepMatchOrder(HepMatchOrder.BOTTOM_UP)
        .add(FlinkBatchRuleSets.EXPAND_PLAN_RULES)
        .build(),
      "convert correlate to temporal table join"
    )
    .addProgram(
      FlinkHepRuleSetProgramBuilder.newBuilder
        .setHepRulesExecutionType(HEP_RULES_EXECUTION_TYPE.RULE_SEQUENCE)
        .setHepMatchOrder(HepMatchOrder.BOTTOM_UP)
        .add(FlinkBatchRuleSets.POST_EXPAND_CLEAN_UP_RULES)
        .build(),
      "convert enumerable table scan"
    )
    .build()
)

1.4.优化

  优化就是遍历前面增加的规则,对计划树进行应用。
可以看到,每个规则的应用时间和效果都记录的日志,可以试试debug看看

programNames.foldLeft(root) {
  (input, name) =>
    val program = get(name).getOrElse(throw new TableException(s"This should not happen."))

    val start = System.currentTimeMillis()
    val result = program.optimize(input, context)
    val end = System.currentTimeMillis()

    if (LOG.isDebugEnabled) {
      LOG.debug(
        s"optimize $name cost ${end - start} ms.\n" +
          s"optimize result: \n${FlinkRelOptUtil.toString(result)}")
    }

    result
}

2.FlinkHepRuleSetProgram

  Flink里一个RuleSet的封装,包含一组操作。

2.1.HepMatchOrder

  查找规则匹配时的图遍历顺序,默认匹配顺序是任意的。
  顺序有以下几个选项:

  • ARBITRARY:任意顺序
  • BOTTOM_UP:从叶子节点向上,就是优先匹配子节点,再匹配父节点(相当于树的后续遍历?)
  • TOP_DOWN:父节点向下,与BOTTOM_UP相反
  • DEPTH_FIRST:深度优先,可以避免规则生成新节点后重复应用规则到节点。适用于大扇出情况,如union

2.2.matchLimit

  最大匹配尝试次数,规则可能会重复进行,这里是最大应用的次数。

2.3.HEP_RULES_EXECUTION_TYPE

  规则执行类型,有两种,默认是RULE_SEQUENCE

  • RULE_SEQUENCE:
    以RuleInstance(calcite的一个类)方式运行,规则集按顺序只执行一次,但是一个规则可以应用多次
  • RULE_COLLECTION:
    以RuleCollection方式运行,以任意顺序执行规则集中的规则,所以需要更多的控制

2.4.optimize

  方法,就是进行优化的入口,首先会进行上面这些属性的设置,然后创建FlinkHepProgram并调用它的optimize方法。
  FlinkHepProgram的optimize走的是Calcite的流程,首先创建HepPlanner,然后调用findBestExp进入Calcite的优化流程。

3.RelOptRule

  这是Calcite的类,是所有规则类的基类。FlinkBatchRuleSets里定义了各种规则集,所有的规则都继承实现了RelOptRule。

val SEMI_JOIN_RULES: RuleSet = RuleSets.ofList(
  SimplifyFilterConditionRule.EXTENDED,
  FlinkRewriteSubQueryRule.FILTER,
  FlinkSubQueryRemoveRule.FILTER,
  JoinConditionTypeCoerceRule.INSTANCE,
  FlinkJoinPushExpressionsRule.INSTANCE
)

  RelOptRule有三个重点内容:RelOptRuleOperand、matches、onMatch。
  onMatch是进行规则应用,就是实际的转换操作。
  RelOptRuleOperand是这个规则对应的模型,可以认为是SQL树的正则表达式,做优化时,优化器会去进行匹配,匹配才会应用规则。
  matches方法是对RelOptRuleOperand匹配的补充,RelOptRuleOperand匹配后会再调用matches进行匹配,默认返回true。按照注释的意思,这个可以方便规则引擎提前发现不匹配项,提升效率。
  个人理解,RelOptRuleOperand是做一个结构化的匹配,matches是附加结构内部更进一步的匹配。比如对limit 0的规则,RelOptRuleOperand匹配Sort这个结构,matches做更进一步的sort.fetch为0。

3.1.RelOptRuleOperand

  目前的很多扩展规则都是直接使用RelOptRule的operand方法传入的规则,但这个方法已经标记删除了,后续可能移除。新的方法是使用OperandBuilder中的operand方法。
  RelOptRuleOperand本质上也是一个节点数

protected <R extends RelNode> RelOptRuleOperand(
    Class<R> clazz,
    RelTrait trait,
    Predicate<? super R> predicate,
    RelOptRuleOperandChildren children) {
  this(clazz, trait, predicate, children.policy, children.operands);
}

  RelOptRuleOperandChildren后续会被删除,后续同样由OperandBuilder创建。最重要的就是两个成员Class和RelOptRuleOperandChildren
  Class即匹配模型树根节点的类型;RelOptRuleOperandChildren是子节点,本质上也是一个RelOptRuleOperand
  WindowGroupReorderRule的定义举例:

class WindowGroupReorderRule
  extends RelOptRule(
    operand(classOf[LogicalWindow], operand(classOf[RelNode], any)),
    "ExchangeWindowGroupRule") {

  RelOptRuleOperandChildren有一个重要的成员,RelOptRuleOperandChildPolicy,代表了子节点的规则。有四个类型:ANY、LEAF、SOME、UNORDERED。SOME是严格按顺序的意思,UNORDERED匹配任意子级的意思,默认SOME。
  RelOptRuleOperand的匹配校验三个东西,clazz、trait、predicate。clazz是必须项,核心内容。trait、predicate是属性和断言,大部分规则不配置,暂时不考虑。

public boolean matches(RelNode rel) {
  if (!clazz.isInstance(rel)) {
    return false;
  }
  if ((trait != null) && !rel.getTraitSet().contains(trait)) {
    return false;
  }
  return predicate.test(rel);
}

3.2.matches

  优化流程中,Operand匹配以后,还有一步matches调用。matches的功能应当是补充细化,以limit 0举例。
  Operand如下,匹配Sort节点,并不能完全满足规则的要求

class FlinkLimit0RemoveRule
  extends RelOptRule(operand(classOf[Sort], any()), "FlinkLimit0RemoveRule") {

  matches补充如下,在匹配Sort类型的基础上,进一步匹配fetch为0的

override def matches(call: RelOptRuleCall): Boolean = {
  val sort: Sort = call.rel(0)
  sort.fetch != null && RexLiteral.intValue(sort.fetch) == 0
}

  这里可以看到,matches方法的入参就是已经匹配到的项,方法里直接对匹配项操作即可,无需再从头获取。

3.3.onMatch

  onMatch就是规则的核心,完成sql树的转换。与matches一样,入参就是匹配到的子树结构,仍以limit 0举例。
  transformTo看说明是注册优化转换的意思,不确定是否此处就完成了转换,应该是在优化流程中最后统一组装的,这里是注册一个转换的对应。prune一般的规则用不到,就是从planner中删除一个节点,后续就不会在这个节点上应用规则,提升效率的手段。

override def onMatch(call: RelOptRuleCall): Unit = {
  val sort: Sort = call.rel(0)
  val emptyValues = call.builder().values(sort.getRowType).build()
  call.transformTo(emptyValues)

  // New plan is absolutely better than old plan.
  call.getPlanner.prune(sort)
}

4.applyRules

  前面说过,优化流程是遍历规则然后应用。应用进入FlinkHepProgram,最终调用Calcite的HepPlanner的findBestExp方法。
  执行过程中会基于HepInstruction进行不同的应用,包括设置一些matchLimit、matchOrder等。最核心的是对RuleInstance的应用,最终调用到HepPlanner的applyRules。
  方法中使用一个双层遍历的方式应用规则,实际的规则应用在下一层的applyRule调用

Iterator<HepRelVertex> iter = getGraphIterator(root);
fixedPoint = true;
while (iter.hasNext()) {
  HepRelVertex vertex = iter.next();
  for (RelOptRule rule : rules) {
    HepRelVertex newVertex =
        applyRule(rule, vertex, forceConversions);

  一个规则会有一个应用极限,每应用一次进行一次统计值的累加。如果规则应用没有产生变更,提前退出

if (newVertex == null || newVertex == vertex) {
  continue;
}
++nMatches;
if (nMatches >= currentProgram.matchLimit) {
  return;
}

  根据规则的匹配方式,规则应用完成后会有两种处理方式:从头开始遍历应用规则、从中断处继续应用规则。

if (fullRestartAfterTransformation) {
  iter = getGraphIterator(root);
} else {
  // To the extent possible, pick up where we left
  // off; have to create a new iterator because old
  // one was invalidated by transformation.
  iter = getGraphIterator(newVertex);

  对应的决定配置为,分别是随机匹配和深度优先匹配

boolean fullRestartAfterTransformation =
    currentProgram.matchOrder != HepMatchOrder.ARBITRARY
    && currentProgram.matchOrder != HepMatchOrder.DEPTH_FIRST;

  深度优先匹配方式有另一个方法预处理,但最终还是调用的applyRule

if (currentProgram.matchOrder == HepMatchOrder.DEPTH_FIRST) {
  nMatches =
      depthFirstApply(iter, rules, forceConversions, nMatches);
  if (nMatches >= currentProgram.matchLimit) {
    return;
  }
}

5.applyRule

  首先关注两种特殊类型的规则,需要特殊处理:ConverterRule、CommonRelSubExprRule
  后续主要四个步骤:1、Operand匹配;2、matches匹配;3、规则触发;4、新树生成

5.1.matchOperands

  做Operand匹配,首先是调用Operand的matches方法做匹配,匹配失败则失败

if (!operand.matches(rel)) {
  return false;
}

  之后是做子节点的匹配,这里子节点有一个特殊情况是会让整个规则都不应用的,暂时没理解具体结构

for (RelNode input : rel.getInputs()) {
  if (!(input instanceof HepRelVertex)) {
    // The graph could be partially optimized for materialized view. In that
    // case, the input would be a RelNode and shouldn't be matched again here.
    return false;
  }
}

  之后正常做子节点匹配,就是不断递归调用matchOperands方法进行匹配。根据子节点策略有三条处理分支:1、ANY,直接返回匹配;2、UNORDERED,遍历子节点,存在任意匹配的,递归调用;3、其他,按顺序匹配下去,存在不匹配项直接匹配失败

5.2.matches

  这个就是调用规则的matches方法进行匹配,注意它的入参在前面讲过,不是完整的计划树,而是跟Operand匹配的子树

5.3.fireRule

  这里进行规则的应用,核心就是调用规则的onMatch方法。
  这里有一个RelOptListener的结构,在规则应用前后都会调用一下ruleAttempted。从注释来看,是做规则尝试次数统计的。

5.4.applyTransformationResults

  使用规则优化后的子树替换原子树。
  应用规则的时候,规则产生子树会追加进graph列表。之后,会进行专门的操作,用新的子树替换掉旧的子树。核心操作在contractVertices中,做法应该是跟java链表操作类似,通过改变孩子指针实现。
  替换之后会有一步清理的操作:collectGarbage。collectGarbage使用可达算法,从根节点开始遍历,将获得的节点加入缓存列表,最后对比,清理不可达的节点。

if (graph.vertexSet().contains(root)) {
  BreadthFirstIterator.reachable(rootSet, graph, root);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值