深入理解Flink基于StreamGraph构建JobGraph的流程


JobGraph是所有类型的作业与集群运行时之间的通信协议,是作业提交使用的统一数据结构。不管是流还是批,最终都得转换成Flink集群能够接受的JobGraph。之所以要搞这么多的图转换,就是为了更好的兼容更多类型的作业,让不同类型的作业都能运行在同一套集群Runtime中。

StreamGraph借助StreamingJobGraphGenerator转换成JobGraph的核心逻辑如下:

/**
 * 将StreamGraph转换为JobGraph的主要逻辑
 */
private JobGraph createJobGraph() {
    // 对StreamGraph进行“预检查”,例如在开启Checkpoint时,StreamGraph每个节点的StreamOperator是否实现了InputSelectable接口
    preValidate();

    // 用StreamGraph的调度模式,作为JobGraph的调度模式
    jobGraph.setScheduleMode(streamGraph.getScheduleMode());

    /**
	 * 广度优先遍历StreamGraph:将StreamGraph中的所有Source排序后,从Source开始为StreamGraph中的每个StreamNode生成hash值(根据StreamNode ID),用作JobVertex ID。
	 * 精髓:基于Queue实现,从Source节点出发,先遍历它的下游,再遍历下游的下游
	 */
    Map<Integer, byte[]> hashes = defaultStreamGraphHasher.traverseStreamGraphAndGenerateHashes(streamGraph);

    // 生成旧版Hash以向后兼容
    List<Map<Integer, byte[]>> legacyHashes = new ArrayList<>(legacyStreamGraphHashers.size());
    for (StreamGraphHasher hasher : legacyStreamGraphHashers) {
        legacyHashes.add(hasher.traverseStreamGraphAndGenerateHashes(streamGraph));
    }

    Map<Integer, List<Tuple2<byte[], byte[]>>> chainedOperatorHashes = new HashMap<>();

    /**
	 * 从Source节点开始递归创建JobVertex节点:这里会将多个“符合链化条件”的StreamNode链化在一起,HeadOperator作为JobVertex节点。
	 * 链化形成OperatorChain,就是要减少数据在TaskManager之间因为网络传输而造成的性能损耗
	 */
    setChaining(hashes, legacyHashes, chainedOperatorHashes);

    /**
	 * 将每个JobVertex节点的所有输入边,序列化到这个JobVertex节点的StreamConfig中
	 */
    setPhysicalEdges();

    // 设置Slot共享组和CoLocation策略
    setSlotSharingAndCoLocation();

    // 设置JobGraph的管理内存比例
    setManagedMemoryFraction(
        Collections.unmodifiableMap(jobVertices),
        Collections.unmodifiableMap(vertexConfigs),
        Collections.unmodifiableMap(chainedConfigs),
        id -> streamGraph.getStreamNode(id).getMinResources(),
        id -> streamGraph.getStreamNode(id).getManagedMemoryWeight());

    // 配置checkpoint
    configureCheckpointing();

    // 配置savepoint
    jobGraph.setSavepointRestoreSettings(streamGraph.getSavepointRestoreSettings());

    // Job中用到的自定义文件
    JobGraphGenerator.addUserArtifactEntries(streamGraph.getUserArtifacts(), jobGraph);

    try {
        // 最后,将StreamGraph中的执行参数ExecutionConfig,交给JobGraph
        jobGraph.setExecutionConfig(streamGraph.getExecutionConfig());
    }
    catch (IOException e) {
        throw new IllegalConfigurationException("Could not serialize the ExecutionConfig." +
                                                "This indicates that non-serializable types (like custom serializers) were registered");
    }

    return jobGraph;
}

1. StreamNode哈希化

在正式构建JobGraph之前,会对StreamGraph中的全部StreamNode(根据StreamNode ID)进行哈希化处理,生成的hash值将来会被当做JobVertex ID。

/**
 * (从Source节点开始)为StreamGraph中的每个StreamNode节点(根据StreamNode ID)生成一个hash值,作为JobVertex节点的ID
 */
@Override
public Map<Integer, byte[]> traverseStreamGraphAndGenerateHashes(StreamGraph streamGraph) {
   final HashFunction hashFunction = Hashing.murmur3_128(0);
   final Map<Integer, byte[]> hashes = new HashMap<>();

   Set<Integer> visited = new HashSet<>();
   Queue<StreamNode> remaining = new ArrayDeque<>();

   // 保存StreamGraph中的所有Source StreamNode
   List<Integer> sources = new ArrayList<>();
   /**取出StreamGraph中所有的Source节点,保存到List中*/
   for (Integer sourceNodeId : streamGraph.getSourceIDs()) {
      sources.add(sourceNodeId);
   }
   // 排序
   Collections.sort(sources);


   /**遍历StreamGraph中的Source节点,将其保存到Queue和Set集合中*/
   for (Integer sourceNodeId : sources) {
      // 把Source节点添加到Queue中
      remaining.add(streamGraph.getStreamNode(sourceNodeId));
      // 将Source节点添加到Set集合中
      visited.add(sourceNodeId);
   }

   StreamNode currentNode;
   // 从Queue的队头拉取StreamNode并remove(从Source节点开始),对其进行哈希化处理。
   while ((currentNode = remaining.poll()) != null) {
      // 如果对StreamNode进行哈希化成功,会返回true
      if (generateNodeHash(currentNode, hashFunction, hashes, streamGraph.isChainingEnabled(), streamGraph)) {
         /**遍历当前StreamNode的所有输出边,让它的下游StreamNode也参与到哈希化处理的过程中*/
         for (StreamEdge outEdge : currentNode.getOutEdges()) {
            // 当前StreamNode的“下游StreamNode”
            StreamNode child = streamGraph.getTargetVertex(outEdge);

            /**
			 * 如果Set中没有当前StreamNode的下游StreamNode,就将其添加到Queue和Set中。
			 * 这样一来,下轮while循环时就会将其poll出来,并继续哈希化。如此循环,所有StreamNode都会被哈希化。
			 */
            if (!visited.contains(child.getId())) {
               remaining.add(child);
               visited.add(child.getId());
            }
         }
      } else {
         visited.remove(currentNode.getId());
      }
   }

   return hashes;
}

(基于广度优先遍历)将StreamGraph中的所有Source StreamNode保存到Queue中,并从Source开始,沿着StreamEdge向下,逐个对下游的StreamNode进行哈希化。最终,StreamGraph中的所有StreamNode全都完成哈希化,且以“StreamNode ID:hash值”的映射关系,保存在Map集合中。

同时为了向后兼容,还会额外为每个StreamNode生成旧版hash值:

/**
 * 手动为StreamNode生产对应的hash值,将来会被当做JobVertex ID
 */
@Override
public Map<Integer, byte[]> traverseStreamGraphAndGenerateHashes(StreamGraph streamGraph) {
    HashMap<Integer, byte[]> hashResult = new HashMap<>();
    // 遍历StreamGraph内的每个StreamNode,为其生成对应的hash值
    for (StreamNode streamNode : streamGraph.getStreamNodes()) {

        String userHash = streamNode.getUserHash();

        if (null != userHash) {
            hashResult.put(streamNode.getId(), StringUtils.hexStringToByte(userHash));
        }
    }

    return hashResult;
}

至此就得到了StreamGraph内每个StreamNode的hash值,将来会将其作为JobVertex ID

2. 链化

从StreamGraph过渡到JobGraph的核心就是Chain,满足链化条件的StreamNode会进行链化,链化过程中会创建JobVertex及其相关的JobEdge、IntermediateDataSet,并使其建立关联关系。将来借助它们可以用来构建OperatorChain,同一个OperatorChain中的StreamOperator就能运行在同一个SubTask上,可以减少网络传输。

因此,是否满足链化条件的判定就是核心中的核心:

/**
 * 对StreamGraph中的StreamNode进行链化处理:从StreamGraph结构中的Source源开始(可能有多个Source),
 * 递归创建JobVertex节点(满足链化条件的StreamOperator会以List的形式保存到JobVertex节点中)
 */
private void setChaining(Map<Integer, byte[]> hashes, List<Map<Integer, byte[]>> legacyHashes, Map<Integer, List<Tuple2<byte[], byte[]>>> chainedOperatorHashes) {
    /**
	 * 遍历这个StreamGraph的Source列表(StreamGraph中可能会有多个Source源):从Source开始逐个遍历StreamGraph中的StreamNode,
	 * 创建JobVertex、JobEdge、IntermediateDataSet,并维护好它们之间的关系
	 */
    for (Integer sourceNodeId : streamGraph.getSourceIDs()) {
        createChain(sourceNodeId, sourceNodeId, hashes, legacyHashes, 0, chainedOperatorHashes);
    }
}

从StreamGraph结构中的每个Source节点开始,基于深度优先遍历,去执行整个链化过程。

/**
 * 通过DFS深度优先搜索,遍历所有的StreamNode(从Source开始,一条路走到黑,直至无路可走),
 * 不停的将满足“链化条件”的StreamNode放到同一个OperatorChain中,每个StreamNode的配置信息都会被序列化到对应的StreamConfig中。
 * 只有OperatorChain的HeadOperator会生成对应的JobVertex(startNodeId = currentNodeId)。
 * 方法返回值为这个OperatorChain的实际的“输出边”
 */
private List<StreamEdge> createChain(
    Integer startNodeId, // Source源
    Integer currentNodeId, // 从Source源开始逐个移动
    Map<Integer, byte[]> hashes, // 根据StreamNode ID进行哈希化后的字节数组
    List<Map<Integer, byte[]>> legacyHashes, // 为向后兼容,为StreamGraph中的每个StreamNode生成对应的hash值
    int chainIndex,
    Map<Integer, List<Tuple2<byte[], byte[]>>> chainedOperatorHashes) {

    // 避免递归过程中发生重复转换
    if (!builtVertices.contains(startNodeId)) {

        // 存储当前OperatorChain的“最终输出边”
        List<StreamEdge> transitiveOutEdges = new ArrayList<StreamEdge>();

        // 存储可以链化的StreamEdge
        List<StreamEdge> chainableOutputs = new ArrayList<StreamEdge>();
        // 存储不能链化的StreamEdge
        List<StreamEdge> nonChainableOutputs = new ArrayList<StreamEdge>();

        // 当前要处理的StreamNode:最初currentNode就是Source,但随着递归调用,currentNode逐步指向后面的StreamNode,最终完成DFS
        StreamNode currentNode = streamGraph.getStreamNode(currentNodeId);

        /**
		 * (按照当前StreamEdge连接的上下游StreamNode是否满足链化条件)将当前StreamNode的StreamEdge分成2组:chainable和nonChainable
		 */
        for (StreamEdge outEdge : currentNode.getOutEdges()) {
            // 判断当前StreamEdge连接的两个StreamNode能否Chain,也就是Source StreamNode和Target StreamNode
            if (isChainable(outEdge, streamGraph)) {
                // 能Chain,分到“chainable组”
                chainableOutputs.add(outEdge);
            } else {
                // 不能Chain,分到“nonChainable组”
                nonChainableOutputs.add(outEdge);
            }
        }

        /**
		 * 处理“chainable组”里的StreamEdge:递归调用,找到“最终输出边”,即这个OperatorChain的输出边,并存储到List集合中
		 */
        for (StreamEdge chainable : chainableOutputs) {
            // 回溯的时候,如果找到了2个JobVertex之间的StreamEdge,就会将其添加到List中,以便2个JobVertex能够顺利connect
            transitiveOutEdges.addAll(
                // 递归链化,startNodeId指针不动,将currentNodeId指针沿着StreamEdge向后移动
                createChain(startNodeId, chainable.getTargetId(), hashes, legacyHashes, chainIndex + 1, chainedOperatorHashes));
        }

        /**
		 * 处理“nonChainable组”里的StreamEdge:递归调用,不能链化的StreamEdge就是连接2个JobVertex节点的最终输出边
		 */
        for (StreamEdge nonChainable : nonChainableOutputs) {
            // 递归时突然遇到一个不能链化的StreamEdge,它必然是2个JobVertex相连的最终输出边,特将其保存到List中,以便2个JobVertex能够顺利connect
            transitiveOutEdges.add(nonChainable);
            // 递归调用:startNodeId和currentNodeId这2个指针均向后移动,判断能否和再下游StreamNode继续Chain
            createChain(nonChainable.getTargetId(), nonChainable.getTargetId(), hashes, legacyHashes, 0, chainedOperatorHashes);
        }

        /**保存当前OperatorChain内每个StreamOperator的hash值*/
        List<Tuple2<byte[], byte[]>> operatorHashes =
            // key存在就把value取出来,key不存在就new一个List然后put到Map中
            chainedOperatorHashes.computeIfAbsent(startNodeId, k -> new ArrayList<>());

        // 获取当前StreamNode对应的hash值
        byte[] primaryHashBytes = hashes.get(currentNodeId);
        // 基于StreamNode的hash值,构造OperatorID
        OperatorID currentOperatorId = new OperatorID(primaryHashBytes);

        /**
		 * 将当前StreamNode对应的hash值保存起来,经过递归之后,同属于一个OperatorChain的所有StreamNode,就会形成一个键值对。
		 * 其中OperatorChain的HeadOperator的StreamNode ID作为Key,其他StreamNode的hash值(包装成的Tuple2)作为Value。
		 * 例如:abc能链化在一起,那么a的StreamNode ID就是Key,abc它们三个的hash值所包装的Tuple2就是Value
		 */
        for (Map<Integer, byte[]> legacyHash : legacyHashes) {
            // Tuple2的类型为:当前StreamNode的新版hash值、向后兼容的hash值
            operatorHashes.add(new Tuple2<>(primaryHashBytes, legacyHash.get(currentNodeId)));
        }

        // 当前节点的name、资源要求等信息
        chainedNames.put(currentNodeId, createChainedName(currentNodeId, chainableOutputs));
        chainedMinResources.put(currentNodeId, createChainedMinResources(currentNodeId, chainableOutputs));
        chainedPreferredResources.put(currentNodeId, createChainedPreferredResources(currentNodeId, chainableOutputs));

        if (currentNode.getInputFormat() != null) {
            getOrCreateFormatContainer(startNodeId).addInputFormat(currentOperatorId, currentNode.getInputFormat());
        }

        if (currentNode.getOutputFormat() != null) {
            getOrCreateFormatContainer(startNodeId).addOutputFormat(currentOperatorId, currentNode.getOutputFormat());
        }

        /**
		 * 如果startNodeId和currentNodeId是同一个,那就创建JobVertex(说明当前StreamNode是OperatorChain的HeadOperator)
		 */
        StreamConfig config = currentNodeId.equals(startNodeId)
            // 创建JobVertex
            ? createJobVertex(startNodeId, hashes, legacyHashes, chainedOperatorHashes)
            // 一旦startNodeId != currentNodeId,说明已经走到了OperatorChain的内部(利用了双指针思想),那就为OperatorChain内的StreamOperator创建StreamConfig
            : new StreamConfig(new Configuration());

        // 将StreamNode中的配置(序列化器、StreamOperator、Checkpoint等)经过序列化后,放到StreamConfig中,并将StreamConfig保存到Map集合中
        setVertexConfig(currentNodeId, config, chainableOutputs, nonChainableOutputs);

        /**
		 * startNode和currentNode是同一个,那就说明当前StreamNode是OperatorChain中的HeadOperator,也就是JobVertex节点,
		 * 那就为其丰富配置
		 */
        if (currentNodeId.equals(startNodeId)) {

            // 将OperatorChain中的HeadOperator,(通过StreamConfig)标记为起始节点
            config.setChainStart();
            config.setChainIndex(0);
            config.setOperatorName(streamGraph.getStreamNode(currentNodeId).getOperatorName());

            /**
			 * OperatorChain中的HeadOperator,会作为JobVertex节点,并且它会和OperatorChain的所有“最终输出边”相连
			 */
            for (StreamEdge edge : transitiveOutEdges) {
                // 遍历OperatorChain对外的所有输出边,通过StreamEdge来构建JobEdge和IntermediateDataSet,用来连接JobVertex和JobEdge
                connect(startNodeId, edge);
            }

            // 将OperatorChain的最终输出边,写入到StreamConfig中,部署时会用到
            config.setOutEdgesInOrder(transitiveOutEdges);
            // 将OperatorChain内的所有StreamOperator的StreamConfig,写入到HeadOperator的CHAINED_TASK_CONFIG配置中
            config.setTransitiveChainedTaskConfigs(chainedConfigs.get(startNodeId));

        } else {
            /**
			 * startNodeId和currentNodeId不是同一个,说明已经走到了OperatorChain的内部(利用了双指针),
			 * 那就为当前StreamNode节点标记好它在OperatorChain内的Index索引,并将它的StreamConfig保存到OperatorChain的“StreamConfig集合”中
			 */

            chainedConfigs.computeIfAbsent(startNodeId, k -> new HashMap<Integer, StreamConfig>());

            // 为这个StreamOperator,添加它在OperatorChain内的Index索引位置
            config.setChainIndex(chainIndex);
            // 当前StreamNode
            StreamNode node = streamGraph.getStreamNode(currentNodeId);
            // 将StreamNode对应的StreamOperator name设置到StreamConfig中
            config.setOperatorName(node.getOperatorName());
            // 将当前节点的StreamConfig put到它所在的OperatorChain的Config集合中
            chainedConfigs.get(startNodeId).put(currentNodeId, config);
        }

        // 设置当前Operator的ID
        config.setOperatorID(currentOperatorId);

        if (chainableOutputs.isEmpty()) {
            config.setChainEnd();
        }
        return transitiveOutEdges;

    } else {
        return new ArrayList<>();
    }
}

step 1:对StreamEdge分组

准备2个集合,分别用来保存“可以Chain”和“不能Chain”的StreamEdge。

// 存储可以链化的StreamEdge
List<StreamEdge> chainableOutputs = new ArrayList<StreamEdge>();
// 存储不能链化的StreamEdge
List<StreamEdge> nonChainableOutputs = new ArrayList<StreamEdge>();


/**
 * (按照当前StreamEdge连接的上下游StreamNode是否满足链化条件)将当前StreamNode的StreamEdge分成2组:chainable和nonChainable
 */
for (StreamEdge outEdge : currentNode.getOutEdges()) {
    // 判断当前StreamEdge连接的两个StreamNode能否Chain,也就是Source StreamNode和Target StreamNode
    if (isChainable(outEdge, streamGraph)) {
        // 能Chain,分到“chainable组”
        chainableOutputs.add(outEdge);
    } else {
        // 不能Chain,分到“nonChainable组”
        nonChainableOutputs.add(outEdge);
    }
}

根据这个StreamEdge两头连接的StreamNode是否满足链化链化条件,将其分成2派。如何判断是否满足链化,自然就成了重中之重。

/**
 * 判断当前StreamEdge连接的上下游StreamNode能否Chain
 */
public static boolean isChainable(StreamEdge edge, StreamGraph streamGraph) {
    // 通过当前StreamEdge,拿到它的Source StreamNode
    StreamNode upStreamVertex = streamGraph.getSourceVertex(edge);
    // 拿到这个StreamEdge的Target StreamNode
    StreamNode downStreamVertex = streamGraph.getTargetVertex(edge);

    // 获取2个StreamNode各自对应的StreamOperator(由OperatorFactory“代言”)
    StreamOperatorFactory<?> headOperator = upStreamVertex.getOperatorFactory();
    StreamOperatorFactory<?> outOperator = downStreamVertex.getOperatorFactory();

    // 只要当前StreamEdge连接的上下游2个StreamNode满足以下条件,就可以链化到同一个OperatorChain中
    return downStreamVertex.getInEdges().size() == 1 // 下游的StreamNode的StreamEdge必须为1,即只有1个输入
        // 2个StreamOperator不为null
        && outOperator != null
        && headOperator != null
        // 上下游的StreamNode必须处在同一个Slot共享组中
        && upStreamVertex.isSameSlotSharingGroup(downStreamVertex)
        // 上下游两个StreamOperator的链化策略要允许链化(可以手动禁用链化)
        // 链化策略:
        // 		1.ALWAYS当前Transformation中的算子尽量和上游算子链化
        // 		2.NEVER:永远不链化
        // 		3.HEAD:当前Transformation中的算子为头部算子,不能和上游链化,只能和下游链化
        // 		4.HEAD_WITH_SOURCES:尝试将多个Source链化
        && outOperator.getChainingStrategy() == ChainingStrategy.ALWAYS
        && (headOperator.getChainingStrategy() == ChainingStrategy.HEAD ||
            headOperator.getChainingStrategy() == ChainingStrategy.ALWAYS)
        // 这个StreamEdge连接的2个StreamNode之间的数据分发策略必须为:ForwardPartitioner(已经在构建StreamGraph阶段确定)
        && (edge.getPartitioner() instanceof ForwardPartitioner)
        // Shuffle模式不为批处理模式
        && edge.getShuffleMode() != ShuffleMode.BATCH
        // 上、下游的2个StreamNode的并行度必须相同
        && upStreamVertex.getParallelism() == downStreamVertex.getParallelism()
        // 开发者没有禁用Chain
        && streamGraph.isChainingEnabled();
}

StreamEdge作为“扁担”,两头挑着StreamNode。只要2个StreamNode满足以上条件,就说明这个StreamEdge可以链化。而使用2个不同的List对StreamEdge进行分组的原因也很简单,就是为了将StreamGraph中的StreamNode按照链化条件来箍堆儿,将来构建OperatorChain的时候,一堆儿就代表一个OperatorChain。

step 2:分别处理2种StreamEdge

对于满足链化条件的StreamEdge,会将currentNodeId向后移动,startNodeId指针不动,判断能否继续和再下游的StreamNode链化在一起。

/**
 * 处理“chainable组”里的StreamEdge:递归调用,找到“最终输出边”,即这个OperatorChain的输出边,并存储到List集合中
 */
for (StreamEdge chainable : chainableOutputs) {
    // 回溯的时候,如果找到了2个JobVertex之间的StreamEdge,就会将其添加到List中,以便2个JobVertex能够顺利connect
    transitiveOutEdges.addAll(
        // 递归链化,startNodeId指针不动,将currentNodeId指针沿着StreamEdge向后移动
        createChain(startNodeId, chainable.getTargetId(), hashes, legacyHashes, chainIndex + 1, chainedOperatorHashes));
}

对于不满足链化条件的StreamEdge,将其特殊保存起来。因为这个StreamEdge是划分2个OperatorChain的重要依据,后期会根据它来对2个JobVertex进行连接。注意:startNodeId和currentNodeId这2个指针均会向后移,指向“不能链化的StreamEdge”的下游StreamNode。

/**
 * 处理“nonChainable组”里的StreamEdge:递归调用,不能链化的StreamEdge就是连接2个JobVertex节点的最终输出边
 */
for (StreamEdge nonChainable : nonChainableOutputs) {
    // 递归时突然遇到一个不能链化的StreamEdge,它必然是2个JobVertex相连的最终输出边,特将其保存到List中,以便2个JobVertex能够顺利connect
    transitiveOutEdges.add(nonChainable);
    // 递归调用:startNodeId和currentNodeId这2个指针均向后移动,判断能否和再下游StreamNode继续Chain
    createChain(nonChainable.getTargetId(), nonChainable.getTargetId(), hashes, legacyHashes, 0, chainedOperatorHashes);
}

整个递归过程采用了类似“双指针”的思想,只要满足链化条件,就单后移currentNodeId指针;不满足链化条件,就将startNodeId和currentNodeId指针均向后移动,这也就自然的“切出来一堆儿”的StreamNode,将来作为一个整体的OperatorChain;

最终经过一轮一轮的递归调用,所有的StreamEdge就已经“泾渭分明”了,而且又找到了每一堆儿StreamNode之间的“三八线”。

step 3:创建JobVertex节点

由于整体采用了类似“双指针”思想,因此只要当前节点和Source节点是同一个,就足以说明这个StreamNode就是将来OperatorChain的HeadOperator。

/**
 * 如果startNodeId和currentNodeId是同一个,那就创建JobVertex(说明它是OperatorChain的HeadOperator)
 */
StreamConfig config = currentNodeId.equals(startNodeId)
    // 创建JobVertex
    ? createJobVertex(startNodeId, hashes, legacyHashes, chainedOperatorHashes)
    // 一旦startNodeId != currentNodeId,说明已经走到了OperatorChain的内部(利用了双指针思想),那就为OperatorChain内的StreamOperator创建StreamConfig
    : new StreamConfig(new Configuration());

只要startNodeId和currentNodeId这2个指针重合,那就说明又切了一堆儿,当前StreamNode就是逻辑层面上“新OperatorChain”的HeadOperator,那就创建JobVertex节点。

/**
 * 创建JobVertex节点:OperatorChain中的HeadOperator会被用来创建JobVertex节点
 */
private StreamConfig createJobVertex(
    Integer streamNodeId, // 这个OperatorChain的HeadOperator,即第一个StreamNode
    Map<Integer, byte[]> hashes, // 基于广度优先遍历生成的所有StreamNode的hash值
    List<Map<Integer, byte[]>> legacyHashes, // 为了向后兼容而生成的所有StreamNode的hash值
    // 以“HeadOperator:HeadOperator所在的OperatorChain中的所有的StreamOperator”为映射关系的Map
    Map<Integer, List<Tuple2<byte[], byte[]>>> chainedOperatorHashes) {

    JobVertex jobVertex;
    // 当前StreamNode作为OperatorChain中的HeadOperator
    StreamNode streamNode = streamGraph.getStreamNode(streamNodeId);

    // 获取当前StreamNode对应的hash值
    byte[] hash = hashes.get(streamNodeId);

    if (hash == null) {
        throw new IllegalStateException("Cannot find node hash. " +
                                        "Did you generate them before calling this method?");
    }

    // 基于HeadOperator的hash值,生成的JobVertex的ID
    JobVertexID jobVertexId = new JobVertexID(hash);

    List<JobVertexID> legacyJobVertexIds = new ArrayList<>(legacyHashes.size());
    for (Map<Integer, byte[]> legacyHash : legacyHashes) {
        hash = legacyHash.get(streamNodeId);
        if (null != hash) {
            legacyJobVertexIds.add(new JobVertexID(hash));
        }
    }

    // 以HeadOperator作为Key,从Map中取出Value(这个HeadOperator所在的OperatorChain中的所有StreamOperator的hash值的集合)
    List<Tuple2<byte[], byte[]>> chainedOperators = chainedOperatorHashes.get(streamNodeId);
    // 保存当前HeadOperator(作为JobVertex)所在的OperatorChain内的所有StreamOperator
    List<OperatorID> chainedOperatorVertexIds = new ArrayList<>();
    List<OperatorID> userDefinedChainedOperatorVertexIds = new ArrayList<>();
    if (chainedOperators != null) {
        /**遍历当前HeadOperator所属的OperatorChain内的所有StreamNode的hash值集合,将它们单独保存起来*/
        for (Tuple2<byte[], byte[]> chainedOperator : chainedOperators) {
            // 将这个HeadOperator所属的OperatorChain中的所有StreamOperator的hash值,add到List中
            chainedOperatorVertexIds.add(new OperatorID(chainedOperator.f0));
            // 用户自定义的哈希化函数所产生的StreamNode的hash值
            userDefinedChainedOperatorVertexIds.add(chainedOperator.f1 != null ? new OperatorID(chainedOperator.f1) : null);
        }
    }

    if (chainedInputOutputFormats.containsKey(streamNodeId)) {
        // 构建JobVertex
        jobVertex = new InputOutputFormatVertex(
            chainedNames.get(streamNodeId),
            jobVertexId,
            legacyJobVertexIds,
            chainedOperatorVertexIds,
            userDefinedChainedOperatorVertexIds);

        chainedInputOutputFormats
            .get(streamNodeId)
            .write(new TaskConfig(jobVertex.getConfiguration()));
    } else {
        // 构建JobVertex
        jobVertex = new JobVertex(
            // JobVertex节点的name
            chainedNames.get(streamNodeId),
            // JobVertex节点ID
            jobVertexId,
            legacyJobVertexIds,
            // 保存当前HeadOperator(作为JobVertex)所属的OperatorChain中的所有StreamOperator hash值的List,会被用来构建JobVertex
            chainedOperatorVertexIds,
            userDefinedChainedOperatorVertexIds);
    }

    jobVertex.setResources(chainedMinResources.get(streamNodeId), chainedPreferredResources.get(streamNodeId));

    jobVertex.setInvokableClass(streamNode.getJobVertexClass());

    int parallelism = streamNode.getParallelism();

    if (parallelism > 0) {
        jobVertex.setParallelism(parallelism);
    } else {
        parallelism = jobVertex.getParallelism();
    }

    jobVertex.setMaxParallelism(streamNode.getMaxParallelism());

    if (LOG.isDebugEnabled()) {
        LOG.debug("Parallelism set: {} for {}", parallelism, streamNodeId);
    }

    // TODO: inherit InputDependencyConstraint from the head operator
    jobVertex.setInputDependencyConstraint(streamGraph.getExecutionConfig().getDefaultInputDependencyConstraint());

    // 按照“OperatorChain中的HeadOperator的StreamNodeId:JobVertex”的映射关系,将JobVertex节点保存到Map中
    jobVertices.put(streamNodeId, jobVertex);
    // HeadOperator已经构建好了JobVertex,保存到集合中
    builtVertices.add(streamNodeId);
    // 将构建好的JobVertex节点添加到JobGraph中
    jobGraph.addVertex(jobVertex);

    // 返回这个JobVertex节点的StreamConfig
    return new StreamConfig(jobVertex.getConfiguration());
}

经历过递归之后,隶属于同一个OperatorChain内的所有StreamOperator的hash值都会被单独保存下来。接着OperatorChain内的HeadOperator作为JobVertex,OperatorChain内的所有StreamNode的hash值都被保存到这个JobVertex内的成员变量中,作为OperatorID

step 4:建立连接

既然已经将所有StreamNode都箍好了堆儿,并且每堆儿里的“代表”(也就是JobVertex)都明确了,那么接下来就该让上下游JobVertex节点之间建立联系。

/**
 * startNode和currentNode是同一个,那就说明当前StreamNode是OperatorChain中的HeadOperator,也就是JobVertex节点,
 * 那就为其丰富配置
 */
if (currentNodeId.equals(startNodeId)) {

    // 将OperatorChain中的HeadOperator,(通过StreamConfig)标记为起始节点
    config.setChainStart();
    config.setChainIndex(0);
    config.setOperatorName(streamGraph.getStreamNode(currentNodeId).getOperatorName());

    /**
	 * OperatorChain中的HeadOperator,会作为JobVertex节点,并且它会和OperatorChain的所有“最终输出边”相连
	 */
    for (StreamEdge edge : transitiveOutEdges) {
        // 遍历OperatorChain对外的所有输出边,通过StreamEdge来构建JobEdge和IntermediateDataSet,用来连接JobVertex和JobEdge
        connect(startNodeId, edge);
    }

    // 将OperatorChain的最终输出边,写入到StreamConfig中,部署时会用到
    config.setOutEdgesInOrder(transitiveOutEdges);
    // 将OperatorChain内的所有StreamOperator的StreamConfig,写入到HeadOperator的CHAINED_TASK_CONFIG配置中
    config.setTransitiveChainedTaskConfigs(chainedConfigs.get(startNodeId));

} else {
    /**
	 * startNodeId和currentNodeId不是同一个,说明已经走到了OperatorChain的内部(利用了双指针),
	 * 那就为当前StreamNode节点标记好它在OperatorChain内的Index索引,并将它的StreamConfig保存到OperatorChain的“StreamConfig集合”中
	 */

    chainedConfigs.computeIfAbsent(startNodeId, k -> new HashMap<Integer, StreamConfig>());

    // 为这个StreamOperator,添加它在OperatorChain内的Index索引位置
    config.setChainIndex(chainIndex);
    // 当前StreamNode
    StreamNode node = streamGraph.getStreamNode(currentNodeId);
    // 将StreamNode对应的StreamOperator name设置到StreamConfig中
    config.setOperatorName(node.getOperatorName());
    // 将当前节点的StreamConfig put到它所在的OperatorChain的Config集合中
    chainedConfigs.get(startNodeId).put(currentNodeId, config);
}

只要startNodeId和currentNodeId不是同一个,那就说明此时已经移动到OperatorChain的内部,内部的所有StreamNode都是可以链化在一起的(让它们抱团);否则,就会发生“切堆儿现象”,那说明又切出来一个新JobVertex。接下来要做的就是让上、下游JobVertex之间建立连接关系。

上、下游JobVertex去connect,本质就是:由上游JobVertex负责创建IntermediateDataSet、下游JobVertex负责创建JobEdge,通过彼此之间“成员变量持有”的方式,相互之间建立连接关系。

/**
 * 每个OperatorChain都会为它所有的输出边(也就是不能链化的那个StreamEdge)创建对应的JobEdge,并以此和JobVertex相连。
 * 假设StreamGraph结构为:a->b->c->d->e。假设abc能够链化在一起,那么a就是HeadOperator,而OperatorChain的最终输出边就是c->d。
 * 当然,最终输出边可能会有N个。此处就是站在HeadOperator的角度,遍历所有的“最终输出边”,
 */
private void connect(Integer headOfChain, StreamEdge edge) {

    // 将JobVertex的“输出边”保存到集合中
    physicalEdgesInOrder.add(edge);

    // 当前“最终输出边”所指向的Target StreamNode,也就是下一个OperatorChain的HeadOperator。如果当前HeadOperator是a,那么这个节点就是d
    Integer downStreamvertexID = edge.getTargetId();

    // 取出当前OperatorChain的HeadOperator所对应的JobVertex节点(也就是a)
    JobVertex headVertex = jobVertices.get(headOfChain);
    // 取出下一个OperatorChain的HeadOperator所对应的JobVertex节点(也就是d)
    JobVertex downStreamVertex = jobVertices.get(downStreamvertexID);

    // 当前JobEdge指向的下游JobVertex节点所对应的StreamConfig
    StreamConfig downStreamConfig = new StreamConfig(downStreamVertex.getConfiguration());

    // 为下游JobVertex节点增加一个输入
    downStreamConfig.setNumberOfInputs(downStreamConfig.getNumberOfInputs() + 1);

    // 2个JobVertex节点之间的StreamEdge的数据分区策略
    StreamPartitioner<?> partitioner = edge.getPartitioner();

    ResultPartitionType resultPartitionType;
    // 根据StreamEdge的ShuffleMode,确定ResultPartition的类型。
    // 确定了ResultPartition的类型,就确定了上游JobVertex的输出--IntermediateDataSet的类型(也是当前JobEdge的输入)
    switch (edge.getShuffleMode()) {
        case PIPELINED:
            resultPartitionType = ResultPartitionType.PIPELINED_BOUNDED;
            break;
        case BATCH:
            resultPartitionType = ResultPartitionType.BLOCKING;
            break;
        case UNDEFINED:
            resultPartitionType = streamGraph.isBlockingConnectionsBetweenChains() ?
                ResultPartitionType.BLOCKING : ResultPartitionType.PIPELINED_BOUNDED;
            break;
        default:
            throw new UnsupportedOperationException("Data exchange mode " +
                                                    edge.getShuffleMode() + " is not supported yet.");
    }

    checkAndResetBufferTimeout(resultPartitionType, edge);

    // 创建JobEdge和IntermediateDataSet
    JobEdge jobEdge;
    /**
	 * 根据Partitioner类型,确定上下游各子节点之间的连接模式(哪些生产SubTask连接到哪些消费SubTask),
	 * (由下游JobVertex负责)以此创建IntermediateDataSet,并和JobEdge建立连接。
	 */
    if (partitioner instanceof ForwardPartitioner || partitioner instanceof RescalePartitioner) {
        // 由下游JobVertex负责创建JobEdge
        jobEdge = downStreamVertex.connectNewDataSetAsInput(
            // 当前OperatorChain的HeadOperator所对应的JobVertex
            headVertex,
            // 每个生产子任务,都连接到消费任务的1个或多个子任务
            DistributionPattern.POINTWISE,
            resultPartitionType);
    } else {
        // 根据JobVertex D,创建JobEdge(从下游JobVertex的角度看)
        jobEdge = downStreamVertex.connectNewDataSetAsInput(
            headVertex,
            // 每个生产子任务,都连接到消费任务的每个子任务
            DistributionPattern.ALL_TO_ALL,
            resultPartitionType);
    }
    // set strategy name so that web interface can show it.
    jobEdge.setShipStrategyName(partitioner.toString());

    if (LOG.isDebugEnabled()) {
        LOG.debug("CONNECTED: {} - {} -> {}", partitioner.getClass().getSimpleName(),
                  headOfChain, downStreamvertexID);
    }
}

创建IntermediateDataSet、JobEdge的逻辑如下:

/**
 * 构建JobEdge,会同步创建IntermediateDataSet。通过彼此之间“成员变量持有”的方式,使它们之间产生关联关系。
 * 									    -----------
 * 上游JobVertex-->IntermediateDataSet →|  JobEdge | →下游JobVertex
 * 									    -----------
 */
public JobEdge connectNewDataSetAsInput(
    JobVertex input, // 上游JobVertex
    DistributionPattern distPattern, // 上下游各SubTask之间的连接模式(点对点 or all_to_all)
    ResultPartitionType partitionType) {

    /**
	 * 根据确定好的ResultPartitionType,由上游JobVertex负责为自己构建IntermediateDataSet。
	 * 本质:将上游JobVertex自己创建好的IntermediateDataSet,保存到它自己的IntermediateDataSet集合中
	 */
    IntermediateDataSet dataSet = input.createAndAddResultDataSet(partitionType);

    /**根据IntermediateDataSet,构建JobEdge(将IntermediateDataSet作为source、下游JobVertex作为target)*/
    JobEdge edge = new JobEdge(dataSet, this, distPattern);
    /**
	 * 将JobEdge保存到下游JobVertex的JobEdge集合中,JobEdge将会作为下游JobVertex节点的输入
	 */
    this.inputs.add(edge);
    // 让这个JobEdge成为IntermediateDataSet的消费者,即将JobEdge保存到IntermediateDataSet内的“消费者集合”中
    dataSet.addConsumer(edge);
    // 返回创建好的JobEdge
    return edge;
}

上游JobVertex负责创建IntermediateDataSet,并将其保存到自己的IntermediateDataSet集合中,作为result;下游JobVertex负责创建JobEdge,并保存到自己的JobEdge集合中,作为input;IntermediateDataSet会将JobEdge保存到自己的JobEdge集合中,作为自己的Consumer;针对JobEdge,它会将IntermediateDataSet和下游JobVertex保存到自己的成员变量中,分别作为自己的source、target。

经过各个角色的这么一通折腾,终于在JobVertex、IntermediateDataSet、JobEdge之间建立好了连接关系。

3.递归回溯流程

假设StreamGraph的结构为:A—>B—>C—>D—>E,其中ABC、DE分别能各自链化到一起。为方便描述,我们以A作为StreamNode、以A-作为A节点后的StreamEdge。从Source节点开始,整个递归过程如下:

  • 1.startNodeId和currentNodeId均为A,A-可以链化,将A-add到可链化集合。currentNodeId指针后移至B
  • 2.startNodeId为A,currentNodeId为B,B-可以链化,将B-add到可链化集合,currentNodeId指针后移至C
  • 3.startNodeId为A,currentNodeId为C,C-不能链化,将C-add到不可链化集合,将C-特殊存放。startNodeId和currentNodeId指针后移至D
  • 4.startNodeId和currentNodeId均为D,D-可以链化,将D-add到可链化集合,currentNodeId指针后移至E
  • 5.startNodeId为D,currentNodeId为E,E没有E-,所有for循环都不走,return 空集合
  • 6.回溯至currentNodeId为D,特殊集合为空;由于D是Head Operator,且双指针重合,故创建JobVertex节点 D,特殊集合为空,并不会connect。return 空集合
  • 7.回溯至currentNodeId为C,此时特殊集合中有C-,最后return (包含C-)特殊集合
  • 8.回溯至currentNodeId为B,此时特殊集合中有C-,最后return (包含C-)特殊集合
  • 9.回溯至currentNodeId为A,此时特殊集合中有C-,由于A是Head Operator,且双指针重合,故创建JobVertex节点 A,并connect。
  • 10.递归结束。

最终,明确了JobVertex节点A和JobVertex节点D,以及连接它俩相关的C-,有了C-就能创建JobEdge。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值