深入解析 Flink 的算子链机制

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"“为什么我的 Flink 作业 Web UI 中只显示出了一个框,并且 Records Sent 和Records Received 指标都是 0 ?是我的程序写得有问题吗?”"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Flink 算子链简介"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"笔者在 Flink 社区群里经常能看到类似这样的疑问。这种情况几乎都不是程序有问题,而是因为 Flink 的 operator chain ——即算子链机制导致的,即提交的作业的执行计划中,所有算子的并发实例(即 sub-task )都因为满足特定条件而串成了整体来执行,自然就观察不到算子之间的数据流量了。当然上述是一种特殊情况。我们更常见到的是只有部分算子得到了算子链机制的优化,如官方文档中出现过多次的下图所示,注意 Source 和 map() 算子。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/72/720b4e0e2709778ba105ae601308d74d.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"算子链机制的好处是显而易见的:所有 chain 在一起的 sub-task 都会在同一个线程(即 TaskManager 的 slot)中执行,能够减少不必要的数据交换、序列化和上下文切换,从而提高作业的执行效率。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/4b/4b9122f479e9ddeb5b03394bca2d367a.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"铺垫了这么多,接下来就通过源码简单看看算子链产生的条件,以及它是如何在 Flink Runtime 中实现的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"逻辑计划中的算子链"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"对 Flink Runtime 稍有了解的看官应该知道,Flink 作业的执行计划会用三层图结构来表示,即:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" StreamGraph —— 原始逻辑执行计划"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" JobGraph —— 优化的逻辑执行计划(Web UI 中看到的就是这个)"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ExecutionGraph —— 物理执行计划"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"算子链是在优化逻辑计划时加入的,也就是由 StreamGraph 生成 JobGraph 的过程中。那么我们来到负责生成 JobGraph 的 o.a.f.streaming.api.graph.StreamingJobGraphGenerator 类,查看其核心方法 createJobGraph() 的源码。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"private JobGraph createJobGraph() {\n // make sure that all vertices start immediately\n jobGraph.setScheduleMode(streamGraph.getScheduleMode());\n // Generate deterministic hashes for the nodes in order to identify them across\n // submission iff they didn't change.\n Map hashes = defaultStreamGraphHasher.traverseStreamGraphAndGenerateHashes(streamGraph);\n // Generate legacy version hashes for backwards compatibility\n List> legacyHashes = new ArrayList<>(legacyStreamGraphHashers.size());\n for (StreamGraphHasher hasher : legacyStreamGraphHashers) {\n legacyHashes.add(hasher.traverseStreamGraphAndGenerateHashes(streamGraph));\n }\n Map>> chainedOperatorHashes = new HashMap<>();\n setChaining(hashes, legacyHashes, chainedOperatorHashes);\n\n setPhysicalEdges();\n // 略......\n\n return jobGraph;\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可见,该方法会先计算出 StreamGraph 中各个节点的哈希码作为唯一标识,并创建一个空的 Map 结构保存即将被链在一起的算子的哈希码,然后调用 setChaining() 方法,如下源码所示。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"private void setChaining(Map hashes, List> legacyHashes, Map>> chainedOperatorHashes) {\n for (Integer sourceNodeId : streamGraph.getSourceIDs()) {\n createChain(sourceNodeId, sourceNodeId, hashes, legacyHashes, 0, chainedOperatorHashes);\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可见是逐个遍历 StreamGraph 中的 Source 节点,并调用 createChain() 方法。createChain() 是逻辑计划层创建算子链的核心方法,完整源码如下,有点长。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"private List createChain(\n Integer startNodeId,\n Integer currentNodeId,\n Map hashes,\n List> legacyHashes,\n int chainIndex,\n Map>> chainedOperatorHashes) {\n if (!builtVertices.contains(startNodeId)) {\n List transitiveOutEdges = new ArrayList();\n List chainableOutputs = new ArrayList();\n List nonChainableOutputs = new ArrayList();\n\n StreamNode currentNode = streamGraph.getStreamNode(currentNodeId);\n for (StreamEdge outEdge : currentNode.getOutEdges()) {\n if (isChainable(outEdge, streamGraph)) {\n chainableOutputs.add(outEdge);\n } else {\n nonChainableOutputs.add(outEdge);\n }\n }\n\n for (StreamEdge chainable : chainableOutputs) {\n transitiveOutEdges.addAll(\n createChain(startNodeId, chainable.getTargetId(), hashes, legacyHashes, chainIndex + 1, chainedOperatorHashes));\n }\n\n for (StreamEdge nonChainable : nonChainableOutputs) {\n transitiveOutEdges.add(nonChainable);\n createChain(nonChainable.getTargetId(), nonChainable.getTargetId(), hashes, legacyHashes, 0, chainedOperatorHashes);\n }\n\n List> operatorHashes =\n chainedOperatorHashes.computeIfAbsent(startNodeId, k -> new ArrayList<>());\n\n byte[] primaryHashBytes = hashes.get(currentNodeId);\n OperatorID currentOperatorId = new OperatorID(primaryHashBytes);\n\n for (Map legacyHash : legacyHashes) {\n operatorHashes.add(new Tuple2<>(primaryHashBytes, legacyHash.get(currentNodeId)));\n }\n\n chainedNames.put(currentNodeId, createChainedName(currentNodeId, chainableOutputs));\n chainedMinResources.put(currentNodeId, createChainedMinResources(currentNodeId, chainableOutputs));\n chainedPreferredResources.put(currentNodeId, createChainedPreferredResources(currentNodeId, chainableOutputs));\n\n if (currentNode.getInputFormat() != null) {\n getOrCreateFormatContainer(startNodeId).addInputFormat(currentOperatorId, currentNode.getInputFormat());\n }\n if (currentNode.getOutputFormat() != null) {\n getOrCreateFormatContainer(startNodeId).addOutputFormat(currentOperatorId, currentNode.getOutputFormat());\n }\n\n StreamConfig config = currentNodeId.equals(startNodeId)\n ? createJobVertex(startNodeId, hashes, legacyHashes, chainedOperatorHashes)\n : new StreamConfig(new Configuration());\n\n setVertexConfig(currentNodeId, config, chainableOutputs, nonChainableOutputs);\n\n if (currentNodeId.equals(startNodeId)) {\n config.setChainStart();\n config.setChainIndex(0);\n config.setOperatorName(streamGraph.getStreamNode(currentNodeId).getOperatorName());\n config.setOutEdgesInOrder(transitiveOutEdges);\n config.setOutEdges(streamGraph.getStreamNode(currentNodeId).getOutEdges());\n for (StreamEdge edge : transitiveOutEdges) {\n connect(startNodeId, edge);\n }\n config.setTransitiveChainedTaskConfigs(chainedConfigs.get(startNodeId));\n } else {\n chainedConfigs.computeIfAbsent(startNodeId, k -> new HashMap());\n config.setChainIndex(chainIndex);\n StreamNode node = streamGraph.getStreamNode(currentNodeId);\n config.setOperatorName(node.getOperatorName());\n chainedConfigs.get(startNodeId).put(currentNodeId, config);\n }\n\n config.setOperatorID(currentOperatorId);\n if (chainableOutputs.isEmpty()) {\n config.setChainEnd();\n }\n return transitiveOutEdges;\n } else {\n return new ArrayList<>();\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"先解释一下方法开头创建的 3 个 List 结构:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" transitiveOutEdges:当前算子链在 JobGraph 中的出边列表,同时也是 createChain() 方法的最终返回值;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" chainableOutputs:当前能够链在一起的 StreamGraph 边列表;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" nonChainableOutputs:当前不能够链在一起的 StreamGraph 边列表。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下来,从 Source 开始遍历 StreamGraph 中当前节点的所有出边,调用 isChainable() 方法判断是否可以被链在一起(这个判断逻辑稍后会讲到)。可以链接的出边被放入 chainableOutputs 列表,否则放入 nonChainableOutputs 列表。对于 chainableOutputs 中的边,就会以这些边的直接下游为起点,继续递归调用createChain() 方法延展算子链。对于 nonChainableOutputs 中的边,由于当前算子链的延展已经到头,就会以这些“断点”为起点,继续递归调用 createChain() 方法试图创建新的算子链。也就是说,逻辑计划中整个创建算子链的过程都是递归的,亦即实际返回时,是从 Sink 端开始返回的。然后要判断当前节点是不是算子链的起始节点。如果是,则调用 createJobVertex()方法为算子链创建一个 JobVertex( 即 JobGraph 中的节点),也就形成了我们在Web UI 中看到的 JobGraph 效果:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e6/e68f44b05c728c84a067425221bc196c.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最后,还需要将各个节点的算子链数据写入各自的 StreamConfig 中,算子链的起始节点要额外保存下 transitiveOutEdges。StreamConfig 在后文的物理执行阶段会再次用到。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"形成算子链的条件"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"来看看 isChainable() 方法的代码。 "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"public static boolean isChainable(StreamEdge edge, StreamGraph streamGraph) {\n StreamNode upStreamVertex = streamGraph.getSourceVertex(edge);\n StreamNode downStreamVertex = streamGraph.getTargetVertex(edge);\n\n StreamOperatorFactory> headOperator = upStreamVertex.getOperatorFactory();\n StreamOperatorFactory> outOperator = downStreamVertex.getOperatorFactory();\n\n return downStreamVertex.getInEdges().size() == 1\n && outOperator != null\n && headOperator != null\n && upStreamVertex.isSameSlotSharingGroup(downStreamVertex)\n && outOperator.getChainingStrategy() == ChainingStrategy.ALWAYS\n && (headOperator.getChainingStrategy() == ChainingStrategy.HEAD ||\n headOperator.getChainingStrategy() == ChainingStrategy.ALWAYS)\n && (edge.getPartitioner() instanceof ForwardPartitioner)\n && edge.getShuffleMode() != ShuffleMode.BATCH\n && upStreamVertex.getParallelism() == downStreamVertex.getParallelism()\n && streamGraph.isChainingEnabled();\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由此可得,上下游算子能够 chain 在一起的条件还是非常苛刻的(老生常谈了),列举如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 上下游算子实例处于同一个 SlotSharingGroup 中(之后再提);"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 下游算子的链接策略(ChainingStrategy)为 ALWAYS ——既可以与上游链接,也可以与下游链接。我们常见的 map()、filter() 等都属此类;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 上游算子的链接策略为 HEAD 或 ALWAYS。HEAD 策略表示只能与下游链接,这在正常情况下是 Source 算子的专属;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 两个算子间的物理分区逻辑是 ForwardPartitioner ,可参见之前写过的《聊聊Flink DataStream 的八种物理分区逻辑》;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 两个算子间的 shuffle 方式不是批处理模式;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 上下游算子实例的并行度相同;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 没有禁用算子链。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"禁用算子链"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用户可以在一个算子上调用 startNewChain() 方法强制开始一个新的算子链,或者调用 disableOperatorChaining() 方法指定它不参与算子链。代码位于 SingleOutputStreamOperator 类中,都是通过改变算子的链接策略实现的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"@PublicEvolving\npublic SingleOutputStreamOperator disableChaining() {\n return setChainingStrategy(ChainingStrategy.NEVER);\n}\n\n@PublicEvolving\npublic SingleOutputStreamOperator startNewChain() {\n return setChainingStrategy(ChainingStrategy.HEAD);\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果要在整个运行时环境中禁用算子链,调用 StreamExecutionEnvironment.disableOperatorChaining() 方法即可。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"物理计划中的算子链"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 JobGraph 转换成 ExecutionGraph 并交由 TaskManager 执行之后,会生成调度执行的基本任务单元 ——StreamTask,负责执行具体的 StreamOperator 逻辑。在StreamTask.invoke() 方法中,初始化了状态后端、checkpoint 存储和定时器服务之后,可以发现:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"operatorChain = new OperatorChain<>(this, recordWriters);\nheadOperator = operatorChain.getHeadOperator();"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"构造出了一个 OperatorChain 实例,这就是算子链在实际执行时的形态。解释一下OperatorChain 中的几个主要属性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"private final StreamOperator>[] allOperators;\nprivate final RecordWriterOutput>[] streamOutputs;\nprivate final WatermarkGaugeExposingOutput> chainEntryPoint;\nprivate final OP headOperator;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" headOperator:算子链的第一个算子,对应 JobGraph 中的算子链起始节点;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" allOperators:算子链中的所有算子,倒序排列,即 headOperator 位于该数组的末尾;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" streamOutputs:算子链的输出,可以有多个;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" chainEntryPoint:算子链的“入口点”,它的含义将在后文说明。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由上可知,所有 StreamTask 都会创建 OperatorChain。如果一个算子无法进入算子链,也会形成一个只有 headOperator 的单个算子的 OperatorChain。OperatorChain 构造方法中的核心代码如下。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"for (int i = 0; i < outEdgesInOrder.size(); i++) {\n StreamEdge outEdge = outEdgesInOrder.get(i);\n RecordWriterOutput> streamOutput = createStreamOutput(\n recordWriters.get(i),\n outEdge,\n chainedConfigs.get(outEdge.getSourceId()),\n containingTask.getEnvironment());\n this.streamOutputs[i] = streamOutput;\n streamOutputMap.put(outEdge, streamOutput);\n}\n\n// we create the chain of operators and grab the collector that leads into the chain\nList> allOps = new ArrayList<>(chainedConfigs.size());\nthis.chainEntryPoint = createOutputCollector(\n containingTask,\n configuration,\n chainedConfigs,\n userCodeClassloader,\n streamOutputMap,\n allOps);\n\nif (operatorFactory != null) {\n WatermarkGaugeExposingOutput> output = getChainEntryPoint();\n headOperator = operatorFactory.createStreamOperator(containingTask, configuration, output);\n headOperator.getMetricGroup().gauge(MetricNames.IO_CURRENT_OUTPUT_WATERMARK, output.getWatermarkGauge());\n} else {\n headOperator = null;\n}\n\n// add head operator to end of chain\nallOps.add(headOperator);\nthis.allOperators = allOps.toArray(new StreamOperator>[allOps.size()]);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先会遍历算子链整体的所有出边,并调用 createStreamOutput() 方法创建对应的下游输出 RecordWriterOutput。然后就会调用 createOutputCollector() 方法创建物理的算子链,并返回 chainEntryPoint,这个方法比较重要,部分代码如下。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"private WatermarkGaugeExposingOutput> createOutputCollector(\n StreamTask, ?> containingTask,\n StreamConfig operatorConfig,\n Map chainedConfigs,\n ClassLoader userCodeClassloader,\n Map> streamOutputs,\n List> allOperators) {\n List>, StreamEdge>> allOutputs = new ArrayList<>(4);\n\n // create collectors for the network outputs\n for (StreamEdge outputEdge : operatorConfig.getNonChainedOutputs(userCodeClassloader)) {\n @SuppressWarnings(\"unchecked\")\n RecordWriterOutput output = (RecordWriterOutput) streamOutputs.get(outputEdge);\n allOutputs.add(new Tuple2<>(output, outputEdge));\n }\n\n // Create collectors for the chained outputs\n for (StreamEdge outputEdge : operatorConfig.getChainedOutputs(userCodeClassloader)) {\n int outputId = outputEdge.getTargetId();\n StreamConfig chainedOpConfig = chainedConfigs.get(outputId);\n WatermarkGaugeExposingOutput> output = createChainedOperator(\n containingTask,\n chainedOpConfig,\n chainedConfigs,\n userCodeClassloader,\n streamOutputs,\n allOperators,\n outputEdge.getOutputTag());\n allOutputs.add(new Tuple2<>(output, outputEdge));\n }\n // 以下略......\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"该方法从上一节提到的 StreamConfig 中分别取出出边和链接边的数据,并创建各自的 Output。出边的 Output 就是将数据发往算子链之外下游的 RecordWriterOutput,而链接边的输出要靠 createChainedOperator() 方法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"private WatermarkGaugeExposingOutput> createChainedOperator(\n StreamTask, ?> containingTask,\n StreamConfig operatorConfig,\n Map chainedConfigs,\n ClassLoader userCodeClassloader,\n Map> streamOutputs,\n List> allOperators,\n OutputTag outputTag) {\n // create the output that the operator writes to first. this may recursively create more operators\n WatermarkGaugeExposingOutput> chainedOperatorOutput = createOutputCollector(\n containingTask,\n operatorConfig,\n chainedConfigs,\n userCodeClassloader,\n streamOutputs,\n allOperators);\n\n // now create the operator and give it the output collector to write its output to\n StreamOperatorFactory chainedOperatorFactory = operatorConfig.getStreamOperatorFactory(userCodeClassloader);\n OneInputStreamOperator chainedOperator = chainedOperatorFactory.createStreamOperator(\n containingTask, operatorConfig, chainedOperatorOutput);\n\n allOperators.add(chainedOperator);\n\n WatermarkGaugeExposingOutput> currentOperatorOutput;\n if (containingTask.getExecutionConfig().isObjectReuseEnabled()) {\n currentOperatorOutput = new ChainingOutput<>(chainedOperator, this, outputTag);\n }\n else {\n TypeSerializer inSerializer = operatorConfig.getTypeSerializerIn1(userCodeClassloader);\n currentOperatorOutput = new CopyingChainingOutput<>(chainedOperator, inSerializer, outputTag, this);\n }\n\n // wrap watermark gauges since registered metrics must be unique\n chainedOperator.getMetricGroup().gauge(MetricNames.IO_CURRENT_INPUT_WATERMARK, currentOperatorOutput.getWatermarkGauge()::getValue);\n chainedOperator.getMetricGroup().gauge(MetricNames.IO_CURRENT_OUTPUT_WATERMARK, chainedOperatorOutput.getWatermarkGauge()::getValue);\n return currentOperatorOutput;\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们一眼就可以看到,这个方法递归调用了上述 createOutputCollector() 方法,与逻辑计划阶段类似,通过不断延伸 Output 来产生 chainedOperator(即算子链中除了headOperator 之外的算子),并逆序返回,这也是 allOperators 数组中的算子顺序为倒序的原因。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"chainedOperator 产生之后,将它们通过 ChainingOutput 连接起来,形成如下图所示的结构。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/3c/3cd92803db185884b01cb455295dabbe.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"图片来自:http://wuchong.me/blog/2016/05/09/flink-internals-understanding-execution-resources/"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最后来看看 ChainingOutput.collect() 方法是如何输出数据流的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"@Override\npublic void collect(StreamRecord record) {\n if (this.outputTag != null) {\n // we are only responsible for emitting to the main input\n return;\n }\n pushToOperator(record);\n}\n\n@Override\npublic void collect(OutputTag outputTag, StreamRecord record) {\n if (this.outputTag == null || !this.outputTag.equals(outputTag)) {\n // we are only responsible for emitting to the side-output specified by our\n // OutputTag.\n return;\n }\n pushToOperator(record);\n}\n\nprotected void pushToOperator(StreamRecord record) {\n try {\n // we know that the given outputTag matches our OutputTag so the record\n // must be of the type that our operator expects.\n @SuppressWarnings(\"unchecked\")\n StreamRecord castRecord = (StreamRecord) record;\n numRecordsIn.inc();\n operator.setKeyContextElement1(castRecord);\n operator.processElement(castRecord);\n }\n catch (Exception e) {\n throw new ExceptionInChainedOperatorException(e);\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可见是通过调用链接算子的 processElement() 方法,直接将数据推给下游处理了。也就是说,OperatorChain 完全可以看做一个由 headOperator 和 streamOutputs组成的单个算子,其内部的 chainedOperator 和 ChainingOutput 都像是被黑盒遮蔽,同时没有引入任何 overhead。打通了算子链在执行层的逻辑,看官应该会明白 chainEntryPoint 的含义了。由于它位于递归返回的终点,所以它就是流入算子链的起始 Output,即上图中指向 headOperator 的 RecordWriterOutput。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文章转载自简书,作者:LittleMagic。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文链接:https://www.jianshu.com/p/799744e347c7"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章