文章目录
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。