Apache Flink源码解析 (六)执行计划生成之StreamGraph-上半篇: 通过DataStream API

概述

  • 根据上一篇文章 DataStream API 可以得知每一个方法生成的Transformation和实际运行中对Task的影响。事实上从Transformation到实际运行的Task中间还要经过StreamGraph,JobGraph,ExecutionGraph这三个转换

Prerequisites

  • DataStream API

    • 下图是不同类型的DataStream之间的转换关系。为了清晰的表达这种转换关系,在每两个流的转换中我选取了比较代表性的方法,此外还有红线表示这层转换对API的使用者是屏蔽的。关于API详细信息还是参考上一篇文章
    • DataStream

  • 执行计划生成

    • 下图就是官方给出的执行计划的生成过程例图
    • 在执行计划的生成过程中,会经历四个阶段,StreamGraph,JobGraph,ExecutionGraph,Execution。其中在客户端生成的是StreamGraph和JobGraph, 在JobManager中生成的是ExecutionGraph,最后实际运行在各个节点上的这张物理执行图事实上是一个抽象的图。
    • flink job graphs.jpg

StreamGraph

  • 如上图所示,StreamGraph是执行计划生成的第一张图,它包含两个重要元素,StreamNode和StreamEdge。
  • StreamGraph的生成发生在用户调用了env.execute() 方法之后,而在这之前,用户编写的应用程序会转换成一个包含了Transformation的集合,关于具体的Transformation接下来就会介绍。接着根据这个Transformation集合来生成相应的StreamNode并且用StreamEdge连接起来形成一张图。
  • Transformation

    • 每一个StreamTransformation都包含了一些标识信息(id和name等),还有输出的类型信息以及并行度和资源相关的信息。
    • 我个人将Transformation分为四大类:包含算子的Transformation,包含Partitioner的Transformation,连接型的Transformation,迭代型的Transformation。

    • 包含算子的Transformation
      • OneInputTransformation,TwoInputTransformation
        • 这两者顾名思义,分别包含了OneInputStreamOperator和TwoInputStreamOperator 算子(在StreamOperator 一文当中详细介绍),并且分别对应着一个输入流和两个输入流。
      • SourceTransformation,SinkTransformation
        • 这两者也很容易看出来包含了StreamSource和StreamSink算子。区别在于SourceTransformation没有输入。

    • 包含Partitioner的Transformation
      • PartitionTransformation
        • Partitioner决定了数据以什么样的方式发送到下游(例如轮询,hash),如果没有ParitionTransformation,那么就会默认使用ForwardPartitioner(类似于Spark中的窄依赖)。

    • 连接型Transformation
      • SplitTransformation,SelectTransformation
        • 这两者总是配对使用,SplitTransformation当中有用户注入的OutputSelector来决定数据会被发送到哪几个流中(命名的逻辑流),SelectTransformation中根据用户注入的selectedNames来连接到对应的上游。
      • SideOutputTransformation
        • 包含了用户指定的OutputTag,根据OutputTag连接到指定的下游。
      • UnionTransformation
        • 包含了一个输入流的集合,把它们一起连接到指定的下游。

    • 迭代型的Transformation不展开讨论

  • Attention:关于DataStream API如何转换成相应的Transformation在上一篇文章中有详细的例子。从这里开始就是如何将Transformation转换成StreamGraph。首先是StreamGraph组成的元素,之后是如何生成。


  • StreamNode

    • 就如StreamNode类的注释所说,它表示了一个算子以及它的属性。
    • 下图是StreamNode中的所有属性,最重要的当然是operator,从这里也可以看出最后StreamNode是和包含算子的Transformation一一对应的。
    • 除此之外,还有几个重要的属性是包含算子的Transformation所不具备的,那就是statePartitioner,outputSelectors,inEdges,outEdges。这些属性是如何被注入的就是生成过程中所要讲解的重要部分。
    • StreamNode fields

  • StreamEdge

    • StreamEdge相对来说要简单的多,它起到了连接两个StreamNode的作用。
    • 下图是StreamEdge的所有属性。比较重要的属性有sourceVertex(起点),targetVertex(终点),selectedNames(SelectTransformation当中用户注入的名字集合),outputTag(SideOutputTransformation当中用户指定的OutputTag), outputPartitioner(默认ForwardPartitioner,可由PartitionTransformation指定)。
    • StreamEdge fields

  • 生成过程

    • 在用户调用了env.execute() ,会调用StreamExecutionEnvironment中的getStreamGraph方法。
      @Override
      public JobExecutionResult execute(String jobName) throws ProgramInvocationException {
          StreamGraph streamGraph = getStreamGraph();
          streamGraph.setJobName(jobName);
          transformations.clear();
          return executeRemotely(streamGraph, jarFiles);
      }
    

    • 在getStreamGraph中,会将用户程序生成的Transformation集合作为生成StreamGraph的参数
      public StreamGraph getStreamGraph() {
          if (transformations.size() <= 0) {
              throw new IllegalStateException("No operators defined in streaming topology. Cannot execute.");
          }
          return StreamGraphGenerator.generate(this, transformations);
      }
    

    • 在StreamGraphGenerator中,会遍历Transformation集合并调用transform方法来完成Transformation向StreamGraph的转换。
      private StreamGraph generateInternal(List<StreamTransformation<?>> transformations) {
          for (StreamTransformation<?> transformation: transformations) {
              transform(transformation);
          }
          return streamGraph;
      }
    

    • 在transform方法中,会首先判断是否已经处理过该Transformation来防止重复处理,然后根据Transformation类型去掉用相应的子方法处理,子方法如下图。(迭代在这里不做介绍)
      • transform*

    • 就如之前介绍Transformation,先从transform包含算子的Transformation开始。首先递归调用input的transform方法(SourceTransformation除外),之后将算子加入到StreamGraph中,核心方法是addOperator(addCoOperator), addNode和addEdge。
      • 在addOperator中,根据StreamOperator类型调用addNode方法生成相应的StreamNode,并注入相应的输入和输出序列化器(上文中StreamNode中的属性)和输入输出类型。
      public <IN, OUT> void addOperator(
              Integer vertexID,
              String slotSharingGroup,
              @Nullable String coLocationGroup,
              StreamOperator<OUT> operatorObject,
              TypeInformation<IN> inTypeInfo,
              TypeInformation<OUT> outTypeInfo,
              String operatorName) {
      
          if (operatorObject instanceof StoppableStreamSource) {
              addNode(vertexID, slotSharingGroup, coLocationGroup, StoppableSourceStreamTask.class, operatorObject, operatorName);
          } else if (operatorObject instanceof StreamSource) {
              addNode(vertexID, slotSharingGroup, coLocationGroup, SourceStreamTask.class, operatorObject, operatorName);
          } else {
              addNode(vertexID, slotSharingGroup, coLocationGroup, OneInputStreamTask.class, operatorObject, operatorName);
          }
      
          TypeSerializer<IN> inSerializer = inTypeInfo != null && !(inTypeInfo instanceof MissingTypeInfo) ? inTypeInfo.createSerializer(executionConfig) : null;
      
          TypeSerializer<OUT> outSerializer = outTypeInfo != null && !(outTypeInfo instanceof MissingTypeInfo) ? outTypeInfo.createSerializer(executionConfig) : null;
      
          setSerializers(vertexID, inSerializer, null, outSerializer);
      
          if (operatorObject instanceof OutputTypeConfigurable && outTypeInfo != null) {
              @SuppressWarnings("unchecked")
              OutputTypeConfigurable<OUT> outputTypeConfigurable = (OutputTypeConfigurable<OUT>) operatorObject;
              // sets the output type which must be know at StreamGraph creation time
              outputTypeConfigurable.setOutputType(outTypeInfo, executionConfig);
          }
      
          if (operatorObject instanceof InputTypeConfigurable) {
              InputTypeConfigurable inputTypeConfigurable = (InputTypeConfigurable) operatorObject;
              inputTypeConfigurable.setInputType(inTypeInfo, executionConfig);
          }
      
          if (LOG.isDebugEnabled()) {
              LOG.debug("Vertex: {}", vertexID);
          }
      }
      
      • addNode方法具体执行了生成StreamNode的任务。
      protected StreamNode addNode(Integer vertexID,
          String slotSharingGroup,
          @Nullable String coLocationGroup,
          Class<? extends AbstractInvokable> vertexClass,
          StreamOperator<?> operatorObject,
          String operatorName) {
      
          if (streamNodes.containsKey(vertexID)) {
              throw new RuntimeException("Duplicate vertexID " + vertexID);
          }
      
          StreamNode vertex = new StreamNode(environment,
              vertexID,
              slotSharingGroup,
              coLocationGroup,
              operatorObject,
              operatorName,
              new ArrayList<OutputSelector<?>>(),
              vertexClass);
      
          streamNodes.put(vertexID, vertex);
      
          return vertex;
      }
      
      • 除此之外就是将Transformation中包含的信息(如并行度,资源)注入到生成好的StreamNode中。并且对每个input通过addEdge生成StreamEdge(在讲完接下来的Transformation之后会详细讲如何生成StreamEdge)。

    • 对于transform包含Partitioner的Transformation,首先获取所有的Input(调用tranform input最后只会返回所有包含StreamOperator的父Transformation Id),再将其遍历生成一个虚拟节点并将这个虚拟节点和(Input,partitioner)的映射加入到一个叫virtualPartitionNodes的Map中。
      private <T> Collection<Integer> transformPartition(PartitionTransformation<T> partition) {
          StreamTransformation<T> input = partition.getInput();
          List<Integer> resultIds = new ArrayList<>();
    
          Collection<Integer> transformedIds = transform(input);
          for (Integer transformedId: transformedIds) {
              int virtualId = StreamTransformation.getNewNodeId();
              streamGraph.addVirtualPartitionNode(transformedId, virtualId, partition.getPartitioner());
              resultIds.add(virtualId);
          }
    
          return resultIds;
      }
    
    

    • 对于连接型的Transformation
      • SplitTransformation的transform过程中会获取所有的Input(所有包含StreamOperator的父Transformation Id),将OutputSelector注入到Input的StreamNode中。
          for (int inputId : resultIds) {
              streamGraph.addOutputSelector(inputId, split.getOutputSelector());
          }
      
      • SelectTransformation同PartitionTransformation,只是将新建的虚拟节点和(Input, SelectedNames)的映射加入到了叫virtualSelectNodes的Map中
      • SideOutputTransformation同PartitionTransformation,只是将新建的虚拟节点和(Input,OutputTag)的映射加入到了叫virtualSideOutputNodes的Map中
      • UnionTransformation则简单的将所有的Input的id的集合返回,为下游节点准备好所有的Input

    • addEdge。在StreamNode生成之前,会调用所有上游的Transformation的transform方法,相应的Partitioner, SelectedNames,OutputTag都已经在上述的三个Map中。
      • 在addEdgeInternal方法中,会递归地处理OutputTag,SelectedNames,Partitioner(如果没有则生成ForwardPartitioner),最后生成StreamEdge,并加入到上游的outEdges和下游的为inEdges集合中。
      private void addEdgeInternal(Integer upStreamVertexID,
              Integer downStreamVertexID,
              int typeNumber,
              StreamPartitioner<?> partitioner,
              List<String> outputNames,
              OutputTag outputTag) {
      
          if (virtualSideOutputNodes.containsKey(upStreamVertexID)) {
              int virtualId = upStreamVertexID;
              upStreamVertexID = virtualSideOutputNodes.get(virtualId).f0;
              if (outputTag == null) {
                  outputTag = virtualSideOutputNodes.get(virtualId).f1;
              }
              addEdgeInternal(upStreamVertexID, downStreamVertexID, typeNumber, partitioner, null, outputTag);
          } else if (virtualSelectNodes.containsKey(upStreamVertexID)) {
              int virtualId = upStreamVertexID;
              upStreamVertexID = virtualSelectNodes.get(virtualId).f0;
              if (outputNames.isEmpty()) {
                  // selections that happen downstream override earlier selections
                  outputNames = virtualSelectNodes.get(virtualId).f1;
              }
              addEdgeInternal(upStreamVertexID, downStreamVertexID, typeNumber, partitioner, outputNames, outputTag);
          } else if (virtualPartitionNodes.containsKey(upStreamVertexID)) {
              int virtualId = upStreamVertexID;
              upStreamVertexID = virtualPartitionNodes.get(virtualId).f0;
              if (partitioner == null) {
                  partitioner = virtualPartitionNodes.get(virtualId).f1;
              }
              addEdgeInternal(upStreamVertexID, downStreamVertexID, typeNumber, partitioner, outputNames, outputTag);
          } else {
              StreamNode upstreamNode = getStreamNode(upStreamVertexID);
              StreamNode downstreamNode = getStreamNode(downStreamVertexID);
      
              // If no partitioner was specified and the parallelism of upstream and downstream
              // operator matches use forward partitioning, use rebalance otherwise.
              if (partitioner == null && upstreamNode.getParallelism() == downstreamNode.getParallelism()) {
                  partitioner = new ForwardPartitioner<Object>();
              } else if (partitioner == null) {
                  partitioner = new RebalancePartitioner<Object>();
              }
      
              if (partitioner instanceof ForwardPartitioner) {
                  if (upstreamNode.getParallelism() != downstreamNode.getParallelism()) {
                      throw new UnsupportedOperationException("Forward partitioning does not allow " +
                              "change of parallelism. Upstream operation: " + upstreamNode + " parallelism: " + upstreamNode.getParallelism() +
                              ", downstream operation: " + downstreamNode + " parallelism: " + downstreamNode.getParallelism() +
                              " You must use another partitioning strategy, such as broadcast, rebalance, shuffle or global.");
                  }
              }
      
              StreamEdge edge = new StreamEdge(upstreamNode, downstreamNode, typeNumber, outputNames, partitioner, outputTag);
      
              getStreamNode(edge.getSourceId()).addOutEdge(edge);
              getStreamNode(edge.getTargetId()).addInEdge(edge);
          }
      }
      

    • 当所有Transformation被遍历过后,完整的StreamGraph就生成了。

总结

  • 详细讲解了StreamGraph的构建过程,因为有很多的递归调用,所有逻辑相对来说比较复杂。
  • 本文与上两篇文章都有一定的关联性,还详细的讲解了Transformation,概念比较多,不好理解,建议多看两遍。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容