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);
}