生成ExecutionGraph

文章详细介绍了ApacheFlink中JobGraph如何经过拓扑排序转换成ExecutionGraph的过程,包括JobVertex节点的排序、ExecutionJobVertex的生成、ExecutionVertex的创建以及ExecutionEdge的连接策略,特别是ALL_TO_ALL模式和点对点模式的执行边创建规则。
摘要由CSDN通过智能技术生成


JobGraph由JobVertex(顶点)和IntermediateDataSet(中间结果数据集)组成, ExecutionGraph是JobManager根据JobGraph生成的,可以将其理解为是JobGraph的“并行化”的产物。ExecutionGraph由ExecutionJobVertex(若干个ExecutionVertex子节点)和IntermediateResult(若干个IntermediateResultPartition)组成。 转换ExecutionGraph的核心就是“拆并行度”,将ExecutionJobVertex拆出若干个ExecutionVertex子节点,将IntermediateResult拆出若干个IntermediateResultPartition。

在构建JobMaster时会初始化调度器,调度器的构造方法中会去生成ExecutionGraph结构。构建ExecutionGraph的本质就是将每个JobVertex节点都转换成ExecutionJobVertex节点

/**
 * 初始化ExecutionGraph
 */
public static ExecutionGraph buildGraph(
    @Nullable ExecutionGraph prior,
    JobGraph jobGraph,
    Configuration jobManagerConfig,
    ScheduledExecutorService futureExecutor,
    Executor ioExecutor,
    SlotProvider slotProvider,
    ClassLoader classLoader,
    CheckpointRecoveryFactory recoveryFactory,
    Time rpcTimeout,
    RestartStrategy restartStrategy,
    MetricGroup metrics,
    BlobWriter blobWriter,
    Time allocationTimeout,
    Logger log,
    ShuffleMaster<?> shuffleMaster,
    JobMasterPartitionTracker partitionTracker,
    FailoverStrategy.Factory failoverStrategyFactory) throws JobExecutionException, JobException {

    // 省略部分代码...

    /**
	 * 核心:将JobGraph中的所有JobVertex节点,按照拓扑逻辑排序后,放到List集合中
	 */
    List<JobVertex> sortedTopology = jobGraph.getVerticesSortedTopologicallyFromSources();

    /**
	 * 核心:生成ExecutionJobVertex,并根据并行度来生成对应数量的ExecutionVertex
	 */
    executionGraph.attachJobGraph(sortedTopology);


    // 省略部分代码...
}

1.对JobGraph中的JobVertex节点进行拓扑排序

将JobGraph中的全部JobVertex节点,进行拓扑排序后,有序添加到List中

/**
 * 将JobGraph中的所有JobVertex节点,按照拓扑逻辑排序后,放到List集合中
 */
public List<JobVertex> getVerticesSortedTopologicallyFromSources() throws InvalidProgramException {
    // 如果映射关系为:“JobVertex ID:JobVertex”、保存了全部JobVertex节点的Map集合是空的,快速退出
    if (this.taskVertices.isEmpty()) {
        return Collections.emptyList();
    }

    // 用于装载拓扑排序后的JobVertex节点
    List<JobVertex> sorted = new ArrayList<JobVertex>(this.taskVertices.size());
    // 用Set集合对所有的JobVertex节点去重
    Set<JobVertex> remaining = new LinkedHashSet<JobVertex>(this.taskVertices.values());

    /**
	 * 局部代码块,代码执行完该作用域后就会释放资源
	 * 找出所有的Source JobVertex节点(没有输入),添加到List中、从Set中移除
	 */
    {
        // 迭代JobGraph结构中的全部JobVertex节点
        Iterator<JobVertex> iter = remaining.iterator();
        while (iter.hasNext()) {
            // 遍历到一个JobVertex
            JobVertex vertex = iter.next();

            // 由于当前JobVertex内保存的所有“输入JobEdge”,只要JobEdge的Source为null,
            // 就说明这个JobVertex是Source节点,将其添加到List中、从Set中移除
            if (vertex.hasNoConnectedInputs()) {
                // 例如:a-->b,当前节点为a,那么最终被add的就是a
                sorted.add(vertex);
                // 每添加1个Source JobVertex,就将其从(保存全部JobVertex节点的)迭代器中移除
                iter.remove();
            }
        }
    }

    // List集合开始遍历的起点位置,即从第一个开始
    int startNodePos = 0;

    // 只要Set还没空,那就继续对剩余的JobVertex进行拓扑排序
    while (!remaining.isEmpty()) {

        // 如果出现以下情况,说明JobGraph中形成了环形,那就得抛异常了
        if (startNodePos >= sorted.size()) {
            throw new InvalidProgramException("The job graph is cyclic.");
        }

        // 从容纳全部Source JobVertex节点的List中,取出指定位置上的JobVertex。
        // 指针自增加1,下次循环会取下一个索引位置的JobVertex
        JobVertex current = sorted.get(startNodePos++);
        // 核心:遍历这个Source JobVertex的下游节点,进行拓扑逻辑排序
        addNodesThatHaveNoNewPredecessors(current, sorted, remaining);
    }

    return sorted;
}

首先准备好2个容器,一个是用来容纳拓扑排序后的JobVertex的List,一个是对JobGraph的全部JobVertex节点去重的Set集合。然后就是将JobGraph中的所有Source JobVertex直接添加到List中,并从Set集合中对应删除。这样一来,到目前为止,List中装的就是全部的Source JobVertex,而Set里装的就是除Source JobVertex以外的全部JobVertex节点。

接下来就该处理JobGraph中除Source JobVertex以外的JobVertex节点了,对它们进行拓扑排序。开启while循环,只要Set还没空,那就继续搞:从Source开始沿着链路判断、处理它下游的JobVertex节点。当某个JobVertex节点的全部“输入JobVertex”均已添加到List后,它自己才能往List中添加、并从Set中对应移除。

/**
 * 对Source JobVertex节点所在的链路(对它下游JobVertex节点)进行拓扑排序:
 * 		从Source JobVertex节点开始,依次向下游遍历。当一个节点的全部“输入JobVertex”均已添加到List后,它才能被添加到List中,并从Set中remove
 *
 *
 * 		Source JobVertex_0 ---> IntermediateDataSet_0 ---> JobEdge_0 ---> JobVertex_00
 * 																				↑
 * 																				↑
 * 			    Source JobVertex_1 ---> IntermediateDataSet_1 ---> JobEdge_1 ---
 */
private void addNodesThatHaveNoNewPredecessors(JobVertex start, List<JobVertex> target, Set<JobVertex> remaining) {

    /**
	 * 正向寻找
	 */
    // 遍历当前Source JobVertex节点生产的全部的IntermediateDataSet,也就是 IntermediateDataSet_0
    for (IntermediateDataSet dataSet : start.getProducedDataSets()) {
        // 遍历当前IntermediateDataSet的所有消费者(即下游连接的“输出JobEdge”),也就是 JobEdge_0
        for (JobEdge edge : dataSet.getConsumers()) {

            // 这个JobEdge下游连接的JobVertex节点,也就是 JobVertex_00
            JobVertex v = edge.getTarget();
            // 如果Set中剩余保存的一堆,没有JobVertex节点,那就跳过本次循环
            if (!remaining.contains(v)) {
                continue;
            }

            // 一个JobVertex节点是否还有输入节点存在于Set集合中的标记
            boolean hasNewPredecessors = false;

            /**
			 * 反向寻找:
			 * 	如果节点v,它所有的输入节点均已不在Set集合,说明v的全部输入节点均已被添加到List集合,此时标记为false。
			 * 	否则标记为true,表示v还有输入节点仍然存在于Set集合中
			 */
            // 遍历这个JobVertex节点的所有输入JobEdge,也就是 JobEdge_0 和 JobEdge_1
            for (JobEdge e : v.getInputs()) {
                // skip the edge through which we came
                // 跳过我们来得那条路,即跳过 JobEdge_0
                if (e == edge) {
                    continue;
                }

                /**
				 * 只要节点v还有输入节点仍然存在于Set集合中,那就说明他还不能添加到List集合中,那就立刻结束这个内循环
				 */
                // 反向往前找,找到1个IntermediateDataSet,也就是 IntermediateDataSet_1
                IntermediateDataSet source = e.getSource();
                // 先取出这个IntermediateDataSet前面的“生产者JobVertex”,也就是 JobVertex_1。
                // 由于JobVertex_1也是Source,所以它肯定不在Set中,因此这个for循环会被直接break掉,且标记为false
                if (remaining.contains(source.getProducer())) {
                    // 节点v还有输入节点存在于Set中,说明这条链路还没彻底完成拓扑排序,那这个输入节点就不能被添加到Set,直接结束这个内循环
                    hasNewPredecessors = true;
                    break;
                }
            }

            /**
			 * 经过上面的for循环,已经彻底洞悉了v的全部“输入JobVertex”是否还有仍在Set中的。
			 * 如果Set集合中已经没有v的输入节点了,此时说明这条链路已经彻底完成了拓扑排序,
			 * 此时就可以将v添加到List中,并从Set中移除。然后递归遍历v的下游节点。
			 */
            if (!hasNewPredecessors) {
                // List<JobVertex>集合中添加这个JobVertex节点,也就是 JobVertex_00
                target.add(v);
                // Set<JobVertex>集合中移除这个JobVertex节点,也就是 JobVertex_00
                remaining.remove(v);
                // 递归遍历当前节点的下游节点
                addNodesThatHaveNoNewPredecessors(v, target, remaining);
            }
        }
    }
}

为了方便理解以上拓扑排序的代码逻辑,我们举个简单例子说明。假设JobGraph结构如下:

 * 		Source JobVertex_0 ---> IntermediateDataSet_0 ---> JobEdge_0 ---> JobVertex_00
 * 																				↑
 * 																				↑
 * 			    Source JobVertex_1 ---> IntermediateDataSet_1 ---> JobEdge_1 ---

因为JobVertex_0和JobVertex_1都是JobGraph结构中的Source JobVertex节点,因此它俩早就已经被添加到List中、且被从Set中对应remove了。

step 1:正向寻找Source JobVertex的下游JobVertex节点

当前JobVertex节点是 JobVertex_0 ,先找它的中间结果数据集 IntermediateDataSet_0 ,然后找中间结果数据集的消费者 JobEdge_0 。最后找这个JobEdge连接的下游节点,也就是 JobVertex_00 。

step 2:用1个标记证明某个JobVertex节点是否还有输入节点(也就是上游JobVertex)仍然存在于Set集合中

根据JobVertex_00 ,反向寻找负责连接它的JobEdge,也就是 JobEdge_0 和 JobEdge_1。我们需要跳过来时的路,只管 JobEdge_1 就行。再往前找到 IntermediateDataSet_1 ,并找到它的生产者,即 JobVertex_1节点。由于JobVertex_1是JobGraph中的Source,早就已经add List、remove Set了,因此Set中一定找不到 JobVertex_1。因此标记仍然还是false。

step 3:正式添加“新成员”

经过上面2个步骤的判断,对于JobVertex_00而言,它的全部输入节点,也就是 JobVertex_0 和 JobVertex_1,在Set中都找不到了。因此,它就能add List、remove Set了。

step 4:沿链路递归向下游继续判断

start从最初的 JobVertex_0 ,变成了 JobVertex_00,重复以上操作。

2.将JobVertex转换成ExecutionJobVertex节点

经历过拓扑排序,JobGraph中的全部JobVertex都已经按照顺序添加到了List中。现在就要将JobGraph结构中的JobVertex,转换成ExecutionGraph中的ExecutionJobVertex节点。

遍历每个JobVertex节点,每当处理一个JobVertex节点,就为其生成对应的ExecutionJobVertex节点,并进行“逻辑连接”

/**
 * 根据有序添加的List<JobVertex>集合,为每个JobVertex对应生成一个ExecutionJobVertex,并根据并行度来生成对应数量的ExecutionVertex
 */
public void attachJobGraph(List<JobVertex> topologiallySorted) throws JobException {

    // 省略部分代码...


    // 遍历(已经拓扑排序后的)所有的JobVertex节点
    for (JobVertex jobVertex : topologiallySorted) {

        // 对ExecutionGraph而言,只要有一个不能Stop的输入源JobVertex,那么ExecutionGraph就是不可Stop的
        if (jobVertex.isInputVertex() && !jobVertex.isStoppable()) {
            this.isStoppable = false;
        }

        /**
		 * 核心:为每个JobVertex节点,对应创建一个ExecutionJobVertex节点
		 * (内部会创建对应数量的ExecutionVertex子节点和一定数量的IntermediateResultPartition)
		 */
        ExecutionJobVertex ejv = new ExecutionJobVertex(
            this,
            jobVertex,
            1,
            maxPriorAttemptsHistoryLength,
            rpcTimeout,
            globalModVersion,
            createTimestamp);

        /**
		 * (在Flink 1.13中 ExecutionEdge的概念被优化,将由ConsumedPartitionGroup和ConsumedVertexGroup来代替)
		 * 	核心:这里就是让“上游IntermediateResult”的每个IntermediateResultPartition,遵从拓扑模式去跟ExecutionEdge相连,
		 * 	所谓相连,就是以全局变量“持有”的方式,使双方产生联系
		 */
        ejv.connectToPredecessors(this.intermediateResults);

        // 省略部分代码...
    }

    // 省略部分代码...
}

2.1 构建ExecutionJobVertex节点

每当遍历到JobGraph结构中的一个JobVertex节点,就创建对应的ExecutionJobVertex节点。

/**
 * 遍历JobVertex节点时,构建ExecutionJobVertex节点(和JobVertex节点一一对应)的同时,还会创建ExecutionVertex子节点
 * 关键词:“拆”并行度
 */
public ExecutionJobVertex(
    ExecutionGraph graph,
    JobVertex jobVertex,
    int defaultParallelism,
    int maxPriorAttemptsHistoryLength,
    Time timeout,
    long initialGlobalModVersion,
    long createTimestamp) throws JobException {

    if (graph == null || jobVertex == null) {
        throw new NullPointerException();
    }

    this.graph = graph;
    this.jobVertex = jobVertex;

    // JobVertex内的并行度
    int vertexParallelism = jobVertex.getParallelism();
    // 对JobVertex的并行度进行安全判断:如果JobVertex的并行度有异常,就取默认并行度 1
    int numTaskVertices = vertexParallelism > 0 ? vertexParallelism : defaultParallelism;

    // JobVertex的最大并行度
    final int configuredMaxParallelism = jobVertex.getMaxParallelism();

    this.maxParallelismConfigured = (VALUE_NOT_SET != configuredMaxParallelism);

    // 设置最大并行度
    setMaxParallelismInternal(maxParallelismConfigured ?
                              configuredMaxParallelism : KeyGroupRangeAssignment.computeDefaultMaxParallelism(numTaskVertices));

    // 如果JobVertex的并行度大于最大并行度,抛异常
    if (numTaskVertices > maxParallelism) {
        throw new JobException(
            String.format("Vertex %s's parallelism (%s) is higher than the max parallelism (%s). Please lower the parallelism or increase the max parallelism.",
                          jobVertex.getName(),
                          numTaskVertices,
                          maxParallelism));
    }

    // JobVertex最终确定下来的并行度
    this.parallelism = numTaskVertices;
    this.resourceProfile = ResourceProfile.fromResourceSpec(jobVertex.getMinResources(), MemorySize.ZERO);

    // ExecutionVertex[]数组:当前JobVertex节点的并行度是多少,对应生成的ExecutionJobVertex节点就有几个ExecutionVertex子节点
    this.taskVertices = new ExecutionVertex[numTaskVertices];
    // 当前JobVertex节点内所有(链化后OperatorChain中的所有StreamOperator)的OperatorID
    this.operatorIDs = Collections.unmodifiableList(jobVertex.getOperatorIDs());
    this.userDefinedOperatorIds = Collections.unmodifiableList(jobVertex.getUserDefinedOperatorIDs());

    // 用来保存当前ExecutionJobVertex节点的所有的上游IntermediateResult
    this.inputs = new ArrayList<>(jobVertex.getInputs().size());

    this.slotSharingGroup = jobVertex.getSlotSharingGroup();
    this.coLocationGroup = jobVertex.getCoLocationGroup();

    if (coLocationGroup != null && slotSharingGroup == null) {
        throw new JobException("Vertex uses a co-location constraint without using slot sharing");
    }

    // JobVertex生成的中间结果数据集--IntermediateDateaSet,和ExecutionJobVertex的IntermediateResult是一一对应的。
    // JobVertex有几个IntermediateDataSet,ExecutionJobVertex就有几个IntermediateResult。
    // 每个IntermediateResult都有“JobVertex并行度”个IntermediateResultPartition
    this.producedDataSets = new IntermediateResult[jobVertex.getNumberOfProducedIntermediateDataSets()];

    /**
	 * 为这个JobVertex节点(生产)的N个IntermediateDataSet(中间结果数据集),创建同等数量的IntermediateResult
	 */
    for (int i = 0; i < jobVertex.getProducedDataSets().size(); i++) {
        // 遍历到当前JobVertex节点所生产的1个IntermediateDataSet
        final IntermediateDataSet result = jobVertex.getProducedDataSets().get(i);

        // 创建IntermediateResult:将生成IntermediateResult,“填”到上面刚刚创建好的IntermediateResult数组的指定位置
        this.producedDataSets[i] = new IntermediateResult(
            result.getId(),
            this,
            // 当前JobVertex节点的并行度,它等于IntermediateResult内IntermediateResultPartition的个数
            numTaskVertices,
            result.getResultType());
    }

    /**
	 * 根据JobVertex节点的并行度,为ExecutionJobVertex创建同等数量的ExecutionVertex子节点
	 * (如果这个JobVertex的并行度=3,就创建3个ExecutionVertex)
	 */
    for (int i = 0; i < numTaskVertices; i++) {
        // 核心:创建ExecutionVertex
        ExecutionVertex vertex = new ExecutionVertex(
            this,
            // 当前ExecutionVertex子节点在ExecutionJobVertex节点内的Index索引位置
            i,
            // 已经准备好的IntermediateResult[]数组,JobVertex生产了几个IntermediateDataSet,数组里就有几个IntermediateResult
            producedDataSets,
            timeout,
            initialGlobalModVersion,
            createTimestamp,
            maxPriorAttemptsHistoryLength);

        // 将生成的ExecutionVertex子节点,“填充”到数组的指定index位置
        this.taskVertices[i] = vertex;
    }

    /**
	 * 遍历ExecutionJobVertex节点的IntermediateResult[]数组,看每个IntermediateResult内的IntermediateResultPartition的个数是否等于JobVertex并行度。
	 * 理论上JobVertex的并行度 = 1个IntermediateResult内的IntermediateResultPartition的个数。
	 * 一旦不相等,就得抛异常
	 */
    for (IntermediateResult ir : this.producedDataSets) {
        if (ir.getNumberOfAssignedPartitions() != parallelism) {
            throw new RuntimeException("The intermediate result's partitions were not correctly assigned.");
        }
    }

    try {
        @SuppressWarnings("unchecked")
        InputSplitSource<InputSplit> splitSource = (InputSplitSource<InputSplit>) jobVertex.getInputSplitSource();

        if (splitSource != null) {
            Thread currentThread = Thread.currentThread();
            ClassLoader oldContextClassLoader = currentThread.getContextClassLoader();
            currentThread.setContextClassLoader(graph.getUserClassLoader());
            try {
                // 根据JobVertex的并行度(即ExecutionVertex的数量),切分输入
                inputSplits = splitSource.createInputSplits(numTaskVertices);

                if (inputSplits != null) {
                    splitAssigner = splitSource.getInputSplitAssigner(inputSplits);
                }
            } finally {
                currentThread.setContextClassLoader(oldContextClassLoader);
            }
        }
        else {
            inputSplits = null;
        }
    }
    catch (Throwable t) {
        throw new JobException("Creating the input splits caused an error: " + t.getMessage(), t);
    }
}

构建ExecutionJobVertex的过程会伴随着创建一定数量的ExecutionVertex子节点(对应JobVertex并行度)、准备IntermediateResult(对应JobVertex生产的IntermediateDataSet的个数)及其所属的IntermediateResultPartition(个数 = 生产它的JobVertex的并行度)。

a. 准备IntermediateResult

遍历当前JobVertex节点所生产的所有中间结果数据集–IntermediateDataSet,为ExecutionJobVertex节点准备同等数量的IntermediateResult。JobVertex节点对应ExecutionJobVertex,IntermediateDataSet对应IntermediateResult。

/**
 * 为这个JobVertex节点(生产)的N个IntermediateDataSet(中间结果数据集),创建同等数量的IntermediateResult
 */
for (int i = 0; i < jobVertex.getProducedDataSets().size(); i++) {
    // 遍历到当前JobVertex节点所生产的1个IntermediateDataSet
    final IntermediateDataSet result = jobVertex.getProducedDataSets().get(i);

    // 创建IntermediateResult:将生成IntermediateResult,“填”到上面刚刚创建好的IntermediateResult数组的指定位置
    this.producedDataSets[i] = new IntermediateResult(
        result.getId(),
        this,
        // 当前JobVertex节点的并行度,它等于IntermediateResult内IntermediateResultPartition的个数
        numTaskVertices,
        result.getResultType());
}

假设JobVertex有3个并行度,生产了2个IntermediateDataSet。那么此时就会为ExecutionJobVertex节点准备2个IntermediateResult,且都会保存到IntermediateResult[]数组的对应位置上。

注意,IntermediateResult内保存了JobVertex节点的并行度,因为1个IntermediateResult会创建“JobVertex并行度数量”的IntermediateResultPartition。

b.构建ExecutionVertex子节点

对于1个ExecutionJobVertex节点而言,目前已经准备好了它所需数量的IntermediateResult。

/**
 * 根据JobVertex节点的并行度,为ExecutionJobVertex创建同等数量的ExecutionVertex子节点
 * (如果这个JobVertex的并行度=3,就创建3个ExecutionVertex)
 */
for (int i = 0; i < numTaskVertices; i++) {
    // 核心:创建ExecutionVertex
    ExecutionVertex vertex = new ExecutionVertex(
        this,
        // 当前ExecutionVertex子节点在ExecutionJobVertex节点内的Index索引位置
        i,
        // 已经准备好的IntermediateResult[]数组,JobVertex生产了几个IntermediateDataSet,数组里就有几个IntermediateResult
        producedDataSets,
        timeout,
        initialGlobalModVersion,
        createTimestamp,
        maxPriorAttemptsHistoryLength);

    // 将生成的ExecutionVertex子节点,“填充”到数组的指定index位置
    this.taskVertices[i] = vertex;
}

接下来就要根据JobVertex节点的并行度,为ExecutionJobVertex节点构建同等数量的ExecutionVertex子节点。

/**
 *  根据JobVertex节点的并行度,为ExecutionJobVertex创建对应数量的ExecutionVertex子节点:
 *  	1.对IntermediateResult分区:每当创建一个ExecutionVertex,就会为所有的IntermediateResult创建一个IntermediateResultPartition
 *  	2.包装Execution:将需要部署的Task的相关信息包装成Execution,方便发送给TaskExecutor(TaskExecutor会基于它来启动Task)
 */
public ExecutionVertex(
    // 当前ExecutionJobVertex节点
    ExecutionJobVertex jobVertex,
    // 当前ExecutionVertex子节点在ExecutionJobVertex节点内的Index索引位置
    int subTaskIndex,
    // 已经准备好的IntermediateResult[]数组,JobVertex生产了几个IntermediateDataSet,数组里就有几个IntermediateResult
    IntermediateResult[] producedDataSets,
    Time timeout,
    long initialGlobalModVersion,
    long createTimestamp,
    int maxPriorExecutionHistoryLength) {

    this.jobVertex = jobVertex;
    this.subTaskIndex = subTaskIndex;
    this.executionVertexId = new ExecutionVertexID(jobVertex.getJobVertexId(), subTaskIndex);
    this.taskNameWithSubtask = String.format("%s (%d/%d)",
                                             jobVertex.getJobVertex().getName(), subTaskIndex + 1, jobVertex.getParallelism());

    // 根据JobVertex生产的IntermediateDataSet个数(即ExecutionJobVertex的IntermediateResult数量),准备LinkedHashMap
    this.resultPartitions = new LinkedHashMap<>(producedDataSets.length, 1);

    /**
	 * 遍历ExecutionJobVertex节点的IntermediateResult的个数,也就是JobVertex生产的IntermediateDataSet的数量。
	 * JobVertex有3个并行度,生产了2个IntermediateDataSet,对应ExecutionJobVertex节点,就会创建3个ExecutionVertex子节点、
	 * 2个IntermediateResult,且每个IntermediateResult内有3个IntermediateResultPartition。
	 * 此时,每当创建1个ExecutionVertex,就会为每个IntermediateResult“安排上”1个IntermediateResultPartition。
	 * 等啥时候当前ExecutionJobVertex节点的全部ExecutionVertex子节点都创建完毕了,那么它对应的所有IntermediateResult也都“填满”了IntermediateResultPartition。
	 */
    for (IntermediateResult result : producedDataSets) {
        // JobVertex节点有几个并行度,ExecutionJobVertex就有几个ExecutionVertex子节点,这个for循环就会总共创建几个IntermediateResultPartition
        IntermediateResultPartition irp = new IntermediateResultPartition(result, this, subTaskIndex);
        // 将创建好的IntermediateResultPartition保存到IntermediateResult中,Index索引和ExecutionVertex子节点共用一套
        result.setPartition(subTaskIndex, irp);

        // 将创建好的IntermediateResultPartition,保存到LinkedHashMap中
        resultPartitions.put(irp.getPartitionId(), irp);
    }

    // 根据JobVertex节点的所有JobEdge(输入边)的数量,准备好ExecutionEdge二元数组,记录的是每个JobEdge对应的ExecutionEdge的数量
    this.inputEdges = new ExecutionEdge[jobVertex.getJobVertex().getInputs().size()][];

    this.priorExecutions = new EvictingBoundedList<>(maxPriorExecutionHistoryLength);

    /**
	 * JobMaster拿到对应TM节点的Slot资源后,会把部署Task所需的信息包装成Execution,
	 * 后续会调用Execution#deploy()方法执行部署:调用RPC请求,将Execution内包装的信息发送给TaskExecutor。
	 * TaskExecutor收到后,将其包装成Task对象,然后启动这个Task
	 */
    this.currentExecution = new Execution(
        getExecutionGraph().getFutureExecutor(),
        this,
        0,
        initialGlobalModVersion,
        createTimestamp,
        timeout);

    CoLocationGroup clg = jobVertex.getCoLocationGroup();
    if (clg != null) {
        this.locationConstraint = clg.getLocationConstraint(subTaskIndex);
    }
    else {
        this.locationConstraint = null;
    }

    getExecutionGraph().registerExecution(currentExecution);

    this.timeout = timeout;
    this.inputSplits = new ArrayList<>();
}

构建ExecutionVertex子节点,首先就是要为ExecutionJobVertex的每个IntermediateResult都“安排”1个IntermediateResultPartition

每当构建1个ExecutionVertex子节点,就会为IntermediateResult“安排”1个IntermediateResultPartition。假设JobVertex有3个并行度,生产了2个IntermediateDataSet。

构建第1个ExecutionVertex子节点:
											  *******************************
											  *	IntermediateResultPartition *
									  ------> * null                  		*
									  |		  * null                  		*
  				 					  |       *******************************
**************************			  |
*  ExecutionVertex子节点  *			|
*  null					 *-------------
*  null                  *			  |
**************************            |
									  |
									  |		  *******************************
									  ------> * IntermediateResultPartition *								
											  * null                  	    *
											  * null                  	    *
											  *******************************

构建第2个ExecutionVertex子节点:
											  *******************************
											  *	IntermediateResultPartition *
									  ------> *	IntermediateResultPartition *
									  |		  * null                  		*
  				 					  |       *******************************
**************************			  |
*  ExecutionVertex子节点  *			|
*  ExecutionVertex子节点  *-------------
*  null                  *			  |
**************************            |
									  |
									  |		  *******************************
									  ------> * IntermediateResultPartition *								
											  *	IntermediateResultPartition *
											  * null                  	    *
											  *******************************


构建第3个ExecutionVertex子节点:
											  *******************************
											  *	IntermediateResultPartition *
									  ------> *	IntermediateResultPartition *
									  |		  *	IntermediateResultPartition *
  				 					  |       *******************************
**************************			  |
*  ExecutionVertex子节点  *			|
*  ExecutionVertex子节点  *-------------
*  ExecutionVertex子节点  *			|
**************************            |
									  |
									  |		  *******************************
									  ------> * IntermediateResultPartition *								
											  *	IntermediateResultPartition *
											  *	IntermediateResultPartition *
											  *******************************

经过3轮循环后,ExecutionJobVertex节点内的全部ExecutionVertex子节点也准备好了,同时也为2个IntermediateResult分别准备好了各自内部(所需数量)的IntermediateResultPartition了。
在这里插入图片描述

另外,ExecutionVertex子节点在ExecutionJobVertex内的Index索引,和IntermediateResultPartition在IntermediateResult内的Index索引,是共用的。最后,将构建好的IntermediateResultPartition按照“IntermediateResultPartition ID :IntermediateResultPartition”的映射关系保存到Map集合中。

等这个ExecutionVertex子节点准备好IntermediateResultPartition后,接着就会构建Execution对象,它表示ExecutionVertex子节点的一次执行。

2.2 创建ExecutionEdge

到目前为止,ExecutionJobVertex节点也转换完毕了,ExecutionVertex子节点、IntermediateResult、IntermediateResultPartition也都准备好了。接下来要做的就是创建ExecutionEdge并建立连接了。

/**
 * JobVertex经过“拆分”并行度后,会创建一个对应的ExecutionJobVertex,内含N个ExecutionVertex(个数等于并行度)。
 * 自然IntermediateResult也就有对应数量的IntermediateResultPartition,该方法就是让这个IntermediateResult的每个IntermediateResultPartition,
 * 通过指定的数据分发模式,去跟ExecutionEdge相连
 * tips:该过程会经历3层for循环(ALL_TO_ALL模式)
 */
public void connectToPredecessors(Map<IntermediateDataSetID, IntermediateResult> intermediateDataSets) throws JobException {

    // 这个JobVertex节点的所有“输入JobEdge”
    List<JobEdge> inputs = jobVertex.getInputs();

    if (LOG.isDebugEnabled()) {
        LOG.debug(String.format("Connecting ExecutionJobVertex %s (%s) to %d predecessors.", jobVertex.getID(), jobVertex.getName(), inputs.size()));
    }

    // 遍历这个JobVertex节点的所有“输入JobEdge”
    for (int num = 0; num < inputs.size(); num++) {
        // 拿到一个JobEdge
        JobEdge edge = inputs.get(num);

        if (LOG.isDebugEnabled()) {
            if (edge.getSource() == null) {
                LOG.debug(String.format("Connecting input %d of vertex %s (%s) to intermediate result referenced via ID %s.",
                                        num, jobVertex.getID(), jobVertex.getName(), edge.getSourceId()));
            } else {
                LOG.debug(String.format("Connecting input %d of vertex %s (%s) to intermediate result referenced via predecessor %s (%s).",
                                        num, jobVertex.getID(), jobVertex.getName(), edge.getSource().getProducer().getID(), edge.getSource().getProducer().getName()));
            }
        }

        // 根据JobEdge,拿到它上游的IntermediateDataSet。并以此作为Key,
        // 从映射关系为“IntermediateDataSetID:IntermediateResult”的Map集合中取出对应的IntermediateResult
        IntermediateResult ires = intermediateDataSets.get(edge.getSourceId());
        // 安全检查:JobEdge必定会消费IntermediateDataSet,如果没有,那就抛异常
        if (ires == null) {
            throw new JobException("Cannot connect this job graph to the previous graph. No previous intermediate result found for ID "
                                   + edge.getSourceId());
        }

        // 将当前ExecutionJobVertex节点的上游IntermediateResult保存到List中
        this.inputs.add(ires);

        int consumerIndex = ires.registerConsumer();

        // 遍历这个JobVertex的并行度(创建ExecutionJobVertex时,会根据JobVertex并行度创建对应数量的ExecutionVertex,它们保存在数组中)
        for (int i = 0; i < parallelism; i++) {
            // 从数组中取出当前ExecutionJobVertex内的ExecutionVertex
            ExecutionVertex ev = taskVertices[i];
            /**
			 * 根据数据分发模式,让这个IntermediateResult的IntermediateResultPartition,去跟ExecutionEdge进行连接
			 * 参数分别为:
			 * 		当前ExecutionJobVertex节点对应的JobVertex节点的“输入JobEdge”的Index
			 * 		当前ExecutionJobVertex节点上游的IntermediateResult
			 * 		当前ExecutionJobVertex节点对应的JobVertex节点的某个“输入JobEdge”
			 * 		IntermediateResultPartition在IntermediateResult中的Index索引
			 */
            ev.connectSource(num, ires, edge, consumerIndex);
        }
    }
}

第1层for循环遍历当前JobVertex节点的所有输入JobEdge,每当拿到一个“上游JobEdge”,根据JobEdge拿到上游的IntermediateDataSet,根据IntermediateDataSet拿到对应的IntermediateResult。

接着第2层for循环遍历当前JobVertex节点的并行度,每个并行度对应一个ExecutionVertex子节点,为这个ExecutionVertex子节点“安排”一定数量的ExecutionEdge并建立连接。

/**
 * (针对JobVertex_0 ---> IntermediateDataSet_0 ---> JobEdge_0 ---> JobVertex_1这一条链路)遍历JobVertex_1的每个并行度,
 * 为每个ExecutionVertex子节点都“安排”上ExecutionEdge
 * @param inputNumber 当前ExecutionJobVertex节点对应的JobVertex节点的“输入JobEdge”的Index
 * @param source 当前ExecutionJobVertex节点上游的IntermediateResult
 * @param edge 当前ExecutionJobVertex节点对应的JobVertex节点的某个“输入JobEdge”
 * @param consumerNumber IntermediateResultPartition在IntermediateResult中的Index索引
 */
public void connectSource(int inputNumber, IntermediateResult source, JobEdge edge, int consumerNumber) {

    // 当前ExecutionVertex子节点所属的ExecutionJobVertex所对应的JobVertex节点的上游JobEdge的数据分发模式
    // 假设当前为JobVertex_1对应的ExecutionJobVertex_1节点,那这获取的就是JobEdge_0保存的分发模式
    final DistributionPattern pattern = edge.getDistributionPattern();
    // 获取当前ExecutionJobVertex节点的“上游IntermediateResult”下属的全部IntermediateResultPartition
    // 也就是IntermediateDateSet_0对应的IntermediateResult下的IntermediateResultPartition
    final IntermediateResultPartition[] sourcePartitions = source.getPartitions();

    // 容纳ExecutionEdge的数组
    ExecutionEdge[] edges;

    // 根据拓扑模式,创建ExecutionEdge
    switch (pattern) {
        case POINTWISE:
            // 1V1
            edges = connectPointwise(sourcePartitions, inputNumber);
            break;

        case ALL_TO_ALL:
            // 笛卡尔积:为每个IntermediateResult的每个IntermediateResultPartition,创建ExecutionEdge
            edges = connectAllToAll(sourcePartitions, inputNumber);
            break;

        default:
            throw new RuntimeException("Unrecognized distribution pattern.");

    }

    // 将创建好的ExecutionEdge,保存到二元数组中
    // 该数组在创建ExecutionVertex子节点时,就已经按照“JobVertex输入”准备好了
    inputEdges[inputNumber] = edges;

    // 为IntermediateResultPartition和ExecutionVertex子节点建立连接关系:本质就是将ExecutionEdge保存到IntermediateResultPartition
    for (ExecutionEdge ee : edges) {
        // Source就是IntermediateResultPartition,现在要给Source添加Consumer
        ee.getSource().addConsumer(ee, consumerNumber);
    }
}

ALL_TO_ALL模式

假设是ALL_TO_ALL模式,会为每一个ExecutionVertex子节点创建一定数量的ExecutionEdge,个数为:当前ExecutionJobVertex节点对应的JobVertex节点的前一个JobVertex的并行度

/**
 * 拓扑模式:ALL_TO_ALL
 * 针对当前ExecutionJobVertex节点的每一个并行度,开一个for循环去创建ExecutionEdge,
 * 循环次数为当前ExecutionJobVertex的上游IntermediateResult所属的IntermediateResultPartition个数(也就是它的“生产JobVertex”的并行度)
 */
private ExecutionEdge[] connectAllToAll(IntermediateResultPartition[] sourcePartitions, int inputNumber) {
    // 根据当前ExecutionJobVertex节点的“上游IntermediateResult”的IntermediateResultPartition数组的length,确定ExecutionEdge数组的长度
    ExecutionEdge[] edges = new ExecutionEdge[sourcePartitions.length];

    /**
	 * 遍历当前ExecutionJobVertex的“上游IntermediateResult”的全部IntermediateResultPartition,创建ExecutionEdge。
	 * 循环次数 = 当前ExecutionJobVertex上游IntermediateResult内的IntermediateResultPartition个数,即前一个JobVertex的并行度。
	 * 也就是说,对当前ExecutionJobVertex而言,它对应的JobVertex的并行度是多少,就会创建几个ExecutionVertex子节点。
	 * 这个JobVertex的上游JobVertex的并行度是多少,就会为每个ExecutionVertex子节点“准备”几个ExecutionEdge。
	 */
    for (int i = 0; i < sourcePartitions.length; i++) {
        // 取出1个IntermediateResultPartition
        IntermediateResultPartition irp = sourcePartitions[i];
        // 创建ExecutionEdge,并保存到ExecutionEdge[] 数组中
        edges[i] = new ExecutionEdge(irp, this, inputNumber);
    }

    return edges;
}

在这里插入图片描述

点对点模式

假如是点对点模式,分3种情况。
case 1:并行度相同,创建的ExecutionEdge个数 = 并行度

		// case 1:当前ExecutionJobVertex的并行度 = 上游IntermediateResultPartition数量
		if (numSources == parallelism) {
			// 取出在ExecutionJobVertex中指定index的ExecutionVertex,所对应的IntermediateResultPartition,用来构建ExecutionEdge
			// 简单理解:1个并行度,会为它对应的IntermediateResultPartition,创建1个ExecutionEdge
			return new ExecutionEdge[] { new ExecutionEdge(sourcePartitions[subTaskIndex], this, inputNumber) };
		}

case 2:当前ExecutionJobVertex的并行度 > 上游IntermediateResultPartition数量

			// 确定IntermediateResultPartition在IntermediateResult数组中的index
			int sourcePartition;

			// check if the pattern is regular or irregular
			// we use int arithmetics for regular, and floating point with rounding for irregular
			/**
			 * 并行度 % IntermediateResultPartition数量,结果是否为0,决定index到底该如何计算
			 */
			if (parallelism % numSources == 0) {
				// same number of targets per source
				int factor = parallelism / numSources;
				sourcePartition = subTaskIndex / factor;
			}
			else {
				// different number of targets per source
				float factor = ((float) parallelism) / numSources;
				sourcePartition = (int) (subTaskIndex / factor);
			}

			// 取出IntermediateResult数组中指定index的IntermediateResultPartition,用来构建ExecutionEdge
			return new ExecutionEdge[] { new ExecutionEdge(sourcePartitions[sourcePartition], this, inputNumber) };

假设当前ExecutionJobVertex的并行度为4, 上游IntermediateResultPartition数量为2。经过计算,index=0、1的IntermediateResultPartition路由到index=0的ExecutionVertex,index=2、3的IntermediateResultPartition路由到index=1的ExecutionVertex

case 3:当前ExecutionJobVertex的并行度 < 上游IntermediateResultPartition数量

			/**
			 * 并行度 % IntermediateResultPartition数量,结果是否为0,决定index到底该如何计算
			 */
			if (numSources % parallelism == 0) {
				// same number of targets per source
				// 计算要创建的ExecutionEdge个数
				int factor = numSources / parallelism;
				// 计算前面IntermediateResultPartition的路由对应索引
				int startIndex = subTaskIndex * factor;

				// 创建指定数量的ExecutionEdge
				ExecutionEdge[] edges = new ExecutionEdge[factor];
				for (int i = 0; i < factor; i++) {
					// 确定路由对应规则,让某一个或某几个IntermediateResultPartition借助1个ExecutionEdge对应1个ExecutionVertex子节点
					edges[i] = new ExecutionEdge(sourcePartitions[startIndex + i], this, inputNumber);
				}
				return edges;
			}
			else {
				float factor = ((float) numSources) / parallelism;

				// 确定start
				int start = (int) (subTaskIndex * factor);
				// 确定end
				int end = (subTaskIndex == getTotalNumberOfParallelSubtasks() - 1) ?
						sourcePartitions.length :
						(int) ((subTaskIndex + 1) * factor);

				// 每个ExecutionVertex创建的ExecutionEdge个数不一
				ExecutionEdge[] edges = new ExecutionEdge[end - start];
				for (int i = 0; i < edges.length; i++) {
					// 确定每个IntermediateResultPartition和ExecutionVertex的路由对应关系
					edges[i] = new ExecutionEdge(sourcePartitions[start + i], this, inputNumber);
				}

				return edges;
			}

如果成整数倍,ExecutionEdge个数 = 上游IntermediateResultPartition数量(假设4) / 当前ExecutionJobVertex的并行度(假设2)。index=0、1、2、3的IntermediateResultPartition通过1个ExecutionEdge路由对应index=0的ExecutionVertex,index=4、5、6、7的IntermediateResultPartition通过1个ExecutionEdge路由对应index=1的ExecutionVertex。
如果不成整数倍,ExecutionEdge个数 = 上游IntermediateResultPartition数量(假设5) / 当前ExecutionJobVertex的并行度(假设2)。index=0、1的IntermediateResultPartition各自通过1个ExecutionEdge路由对应index=0的ExecutionVertex,index=2、3、4的IntermediateResultPartition各自通过1个ExecutionEdge路由对应index=1的ExecutionVertex。

2.3 按照拓扑模式进行连接

截止到目前,1个ExecutionVertex子节点的上游ExecutionEdge也准备好了。接下来就是要为其建立连接关系了,所谓的建立连接,其实就是将ExecutionEdge交给IntermediateResultPartition持有,这样它们二者之间就产生了关系。

// 为IntermediateResultPartition和ExecutionVertex子节点建立连接关系:本质就是将ExecutionEdge保存到IntermediateResultPartition
for (ExecutionEdge ee : edges) {
    // Source就是IntermediateResultPartition,现在要给Source添加Consumer
    ee.getSource().addConsumer(ee, consumerNumber);
}

至此,ExecutionGraph结构就构建完成了。

现在我们举例简化以上转换流程,假设JobGraph结构如下:

JobVertex_0 ---> IntermediateDataSet_0 ---> JobEdge_0 ---> JobVertex_1 (p = 4)	
 (p = 2)													  ↑
               JobVertex_2 ---> IntermediateDataSet_2 ---> JobEdge_2 
                (p = 1)	

当前ExecutionJobVertex节点对应JobVertex_1,JobVertex_1有2个“输入JobEdge”,分别是 JobEdge_0 和 JobEdge_2 。现在要遍历这2个JobEdge。

第1层for循环,是处理是JobVertex_1的2条“输入链路”的,循环2次

循环次数取决于当前JobVertex节点的“输入JobEdge”的数量。

循环第1次是:获取 JobEdge_0、取出 IntermediateDataSet_0(以及对应的IntermediateResult)。

第2层for循环,是针对当前ExecutionJobVertex的ExecutionVertex子节点的,循环4次

循环次数取决于当前JobVertex的并行度,JobVertex的并行度 = ExecutionJobVertex的ExecutionVertex子节点个数。

迭代当前ExecutionJobVertex节点对应JobVertex_1的并行度(p = 4)。在4次循环中,每次会取出1个ExecutionVertex子节点,针对这个ExecutionVertex子节点,取出 JobEdge_0保存的DistributionPattern、IntermediateDataSet_0对应的IntermediateResult_0,拿着这俩玩意去创建ExecutionEdge。根据JobEdge保存的分发模式,为每个ExecutionVertex子节点“准备”一定数量的ExecutionEdge。

第3层for循环,是为ExecutionVertex子节点准备一定数量的ExecutionEdge,循环2次

第3层for循环的次数,取决于当前IntermediateDataSet_0对应的IntermediateResult所属的IntermediateResultPartition的个数(也就是JobVertex_0的并行度,p = 2)。

取出IntermediateDataSet_0对应的IntermediateResult_0的每个IntermediateResultPartition,为其构建ExecutionEdge。假设当前JobVertex_1节点的这个“输入JobEdge”保存记录的分发模式为ALL_TO_ALL,因此当前JobVertex_1的前一个JobVertex_0的并行度为2,所以本次for循环会循环2次,创建出2个ExecutionEdge。

简单来说就是,针对JobVertex_1,会循环4次,因为它的并行度为4。每次循环,都会为ExecutionVertex子节点“准备”2个ExecutionEdge(JobVertex_0的并行度)。不管是ALL_TO_ALL还是点对点模式,总会为每个ExecutionVertex子节点准备好所需数量的ExecutionEdge。之后的建立连接,就是把ExecutionEdge交给IntermediateResultPartition保存,使它们二者之间产生关系。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值