文章目录
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保存,使它们二者之间产生关系。
文章详细介绍了ApacheFlink中JobGraph如何经过拓扑排序转换成ExecutionGraph的过程,包括JobVertex节点的排序、ExecutionJobVertex的生成、ExecutionVertex的创建以及ExecutionEdge的连接策略,特别是ALL_TO_ALL模式和点对点模式的执行边创建规则。
472

被折叠的 条评论
为什么被折叠?



