文章目錄
本文主要圍繞 Flink 源碼中 flink-streaming-java
模塊。介紹下 StreamGraph 轉成 JobGraph 的過程等。
StreamGraph 和 JobGraph 都是在 Client 端生成的,也就是說我們可以在 IDE 中通過斷點調試觀察 StreamGraph 和 JobGraph 的生成過程。
StreamGraph 實際上只對應 Flink 作業在邏輯上的執行計劃圖,Flink 會進一步對 StreamGraph 進行轉換,得到另一個執行計劃圖,即 JobGraph。
1. 調用鏈路
使用 DataStream API 編寫好程序之後,就會調用到 StreamExecutionEnvironment.execute() 方法了,首先會調用 getStreamGraph 生成 StreamGraph,接着就會將 StreamGraph 轉成 JobGraph,調用鏈路如下:
- 首先,調用 StreamExecutionEnvironment 的 executeAsync() 方法,根據 Configuration 獲取 PipelineExecutorFactory 和 PipelineExecutor 。
@Public
public class StreamExecutionEnvironment {
/**
* 根據 execution.target 配置反射得到 PipelineExecutorFactory,拿出工廠類對應的 PipelineExecutor,執行其 execute() 方法
* execute的主要工作是將 StreamGraph 轉成了 JobGraph,並創建相應的 ClusterClient 完成提交任務的操作。
*/
@Internal
public JobClient executeAsync(StreamGraph streamGraph) throws Exception {
checkNotNull(streamGraph, "StreamGraph cannot be null.");
checkNotNull(configuration.get(DeploymentOptions.TARGET), "No execution.target specified in your configuration file.");
// SPI機制
// 根據flink Configuration中的"execution.target"加載 PipelineExecutorFactory
// PipelineExecutorFactory 的實現類在flink-clients包或者flink-yarn包裏,因此需要在pom.xml中添加對應的依賴
final PipelineExecutorFactory executorFactory =
executorServiceLoader.getExecutorFactory(configuration);
// 反射出的 PipelineExecutorFactory 類不能爲空
checkNotNull(
executorFactory,
"Cannot find compatible factory for specified execution.target (=%s)",
configuration.get(DeploymentOptions.TARGET));
// 根據加載到的 PipelineExecutorFactory 工廠類,獲取其對應的 PipelineExecutor,
// 並執行 PipelineExecutor 的 execute() 方法,將 StreamGraph 轉成 JobGraph
CompletableFuture<JobClient> jobClientFuture = executorFactory
.getExecutor(configuration)
.execute(streamGraph, configuration);
// 異步調用的返回結果
// ...
}
}
PipelineExecutorFactory 是通過 SPI ServiceLoader 加載的,我們看下 flink-clients
模塊的 META-INF.services
文件:
PipelineExecutorFactory 的實現子類,分別對應着 Flink 的不同部署模式,如 local、standalone、yarn、kubernets 等:
這裏我們只看下 LocalExecutorFactory 的實現:
@Internal
public class LocalExecutorFactory implements PipelineExecutorFactory {
/**
* execution.target 配置項對應的值爲 "local"
*/
@Override
public boolean isCompatibleWith(final Configuration configuration) {
return LocalExecutor.NAME.equalsIgnoreCase(configuration.get(DeploymentOptions.TARGET));
}
/**
* 直接 new 一個 LocalExecutor 返回
*/
@Override
public PipelineExecutor getExecutor(final Configuration configuration) {
return new LocalExecutor();
}
}
PipelineExecutor 的實現子類與 PipelineExecutorFactory 與工廠類一一對應,負責將 StreamGraph 轉成 JobGraph,並生成 ClusterClient 執行任務的提交:
- 接着,調用到 LocalExecutor 中的 getJobGraph() 方法,會反射出 StreamGraphTranslator 類,並調用它的 translateToJobGraph() 方法。
@Internal
public class LocalExecutor implements PipelineExecutor {
// ...
private JobGraph getJobGraph(Pipeline pipeline, Configuration configuration) {
// ...
// 這裏調用 FlinkPipelineTranslationUtil 的 getJobGraph() 方法
return FlinkPipelineTranslationUtil.getJobGraph(pipeline, configuration, 1);
}
}
FlinkPipelineTranslationUtil 中通過反射得到一個 FlinkPipelineTranslator ,即 StreamGraphTranslator:
public class FlinkPipelineTranslationUtil{
public static JobGraph getJobGraph(
Pipeline pipeline,
Configuration optimizerConfiguration,
int defaultParallelism) {
// 通過反射得到 FlinkPipelineTranslator
FlinkPipelineTranslator pipelineTranslator = getPipelineTranslator(pipeline);
return pipelineTranslator.translateToJobGraph(pipeline,
optimizerConfiguration,
defaultParallelism);
}
private static FlinkPipelineTranslator getPipelineTranslator(Pipeline pipeline) {
PlanTranslator planToJobGraphTransmogrifier = new PlanTranslator();
if (planToJobGraphTransmogrifier.canTranslate(pipeline)) {
return planToJobGraphTransmogrifier;
}
FlinkPipelineTranslator streamGraphTranslator = reflectStreamGraphTranslator();
// 其實就是判斷當前的 Pipeline 實例是不是 StreamGraph
if (!streamGraphTranslator.canTranslate(pipeline)) {
throw new RuntimeException("Translator " + streamGraphTranslator + " cannot translate "
+ "the given pipeline " + pipeline + ".");
}
return streamGraphTranslator;
}
private static FlinkPipelineTranslator reflectStreamGraphTranslator() {
Class<?> streamGraphTranslatorClass;
try {
streamGraphTranslatorClass = Class.forName(
// 因爲這個類在 flink-streaming-java 模塊中,FlinkPipelineTranslationUtil 在 flink-clients 模塊中,
// flink-clients 模塊沒有引入 flink-streaming-java 模塊,所以只能通過反射拿到
"org.apache.flink.streaming.api.graph.StreamGraphTranslator",
true,
FlinkPipelineTranslationUtil.class.getClassLoader());
} catch (ClassNotFoundException e) {
throw new RuntimeException("Could not load StreamGraphTranslator.", e);
}
FlinkPipelineTranslator streamGraphTranslator;
try {
streamGraphTranslator =
(FlinkPipelineTranslator) streamGraphTranslatorClass.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException("Could not instantiate StreamGraphTranslator.", e);
}
return streamGraphTranslator;
}
}
- 最後,調用 StreamGraphTranslator 的 translateToJobGraph() 方法,會一直調用到 StreamGraph 類自己的 getJobGraph() 方法。
public class StreamGraphTranslator implements FlinkPipelineTranslator {
/**
* 其實就是調用 StreamGraph 自己的 getJobGraph() 方法生成 JobGraph
*/
@Override
public JobGraph translateToJobGraph(
Pipeline pipeline,
Configuration optimizerConfiguration,
int defaultParallelism) {
checkArgument(pipeline instanceof StreamGraph,
"Given pipeline is not a DataStream StreamGraph.");
StreamGraph streamGraph = (StreamGraph) pipeline;
return streamGraph.getJobGraph(null);
}
@Override
public boolean canTranslate(Pipeline pipeline) {
return pipeline instanceof StreamGraph;
}
}
到此,我們知道 StreamGraph 到 JobGraph 轉換的核心轉換方法是 StreamingJobGraphGenerator 的 createJobGraph() 方法。
接下來我們先看下 JobGraph 涉及到的幾個類:
2. 源碼剖析
2.1 JobVertex
在 StreamGraph 中,每一個算子(Operator)對應了圖中的一個節點(StreamNode)。StreamGraph 會被進一步優化,將多個符合條件的節點 Chain 在一起形成一個節點,從而減少數據在不同節點之間流動產生的序列化、反序列化、網絡傳輸的開銷。多個算子被 chain 在一起的形成的節點在 JobGraph 中對應的就是 JobVertex。
每個 JobVertex 中包含一個或多個 Operators。
public class JobVertex {
/**
* The ID of the vertex.
* 頂點的id
*/
private final JobVertexID id;
/**
* The alternative IDs of the vertex.
* 頂點的可選id
*/
private final ArrayList<JobVertexID> idAlternatives = new ArrayList<>();
/**
* The IDs of all operators contained in this vertex.
* 此頂點中包含的所有運算符的ID
*/
private final ArrayList<OperatorID> operatorIDs = new ArrayList<>();
/**
* The alternative IDs of all operators contained in this vertex.
* 此頂點中包含的所有運算符的可選ID
*/
private final ArrayList<OperatorID> operatorIdsAlternatives = new ArrayList<>();
/**
* List of produced data sets, one per writer.
* 生成的數據集列表,每個 writer 一個
*/
private final ArrayList<IntermediateDataSet> results = new ArrayList<>();
/**
* List of edges with incoming data. One per Reader.
* 包含傳入數據的邊的列表,每個 reader 一個
*/
private final ArrayList<JobEdge> inputs = new ArrayList<>();
/**
* Number of subtasks to split this task into at runtime.
* 運行時要將此任務拆分爲的子任務數
*/
private int parallelism = ExecutionConfig.PARALLELISM_DEFAULT;
}
2.2 JobEdge
在 StreamGraph 中,StreamNode 之間是通過 StreamEdge 建立連接的。在 JobGraph 中對應的是 JobEdge 。
和 StreamEdge 中同時保留了源節點和目標節點(sourceId 和 targetId) 不同,在 JobEdge 中只有源節點的信息,JobEdge 是和節點的輸出結果相關聯的。
public class JobEdge {
/**
* The vertex connected to this edge.
* 連接到該邊的頂點
*/
private final JobVertex target;
/**
* The distribution pattern that should be used for this job edge.
* 應用於此作業邊的分發模式
*/
private final DistributionPattern distributionPattern;
/**
* The data set at the source of the edge, may be null if the edge is not yet connected
* 如果邊尚未連接,則邊的 source 源處的數據集可能爲空
*/
private IntermediateDataSet source;
/**
* The id of the source intermediate data set
* 源中間數據集的id
*/
private IntermediateDataSetID sourceId;
/** Optional name for the data shipping strategy (forward, partition hash, rebalance, ...),
* to be displayed in the JSON plan
* JSON計劃中顯示的數據傳送策略(轉發、分區哈希、重新平衡…)的可選名稱
*/
private String shipStrategyName;
/** Optional name for the pre-processing operation (sort, combining sort, ...),
* to be displayed in the JSON plan
* JSON計劃中顯示的預處理操作的可選名稱(排序、組合排序...)的可選名稱
*/
private String preProcessingOperationName;
/**
* Optional description of the caching inside an operator, to be displayed in the JSON plan
* JSON計劃中顯示的操作內部緩存的可選描述
*/
private String operatorLevelCachingDescription;
}
2.3 IntermediateDataSet
JobVertex 產生的數據被抽象爲 IntermediateDataSet ,字面意思爲中間數據集。
JobVertex 是 IntermediateDataSet 的生產者,JobEdge 是 IntermediateDataSet 的消費者。
public class IntermediateDataSet {
/**
* the identifier
* IntermediateDataSet ID
*/
private final IntermediateDataSetID id;
/**
* the operation that produced this data set
* JobVertex 是 IntermediateDataSet 的生產者
*/
private final JobVertex producer;
/**
* JobEdge 是和節點的輸出結果相關聯的,其實就是指可以把 JobEdge 看作是 IntermediateDataSet 的消費者
*/
private final List<JobEdge> consumers = new ArrayList<JobEdge>();
/**
* The type of partition to use at runtime
* 運行時要使用的分區類型,表示中間結果類型
*/
private final ResultPartitionType resultType;
}
ResultPartitionType 表示中間結果枚舉類型,有以下幾個屬性:
要結合 Flink 任務運行時的內存管理機制來看,後續再作分析。
public enum ResultPartitionType {
BLOCKING(false, false, false, false),
BLOCKING_PERSISTENT(false, false, false, true),
PIPELINED(true, true, false, false),
/**
* 在 Stream 模式下使用的類型
*/
PIPELINED_BOUNDED(true, true, true, false);
/**
* Can the partition be consumed while being produced?
* 分區正在生產時是否能被消費?
*/
private final boolean isPipelined;
/**
* Does the partition produce back pressure when not consumed?
* 當分區不消費時是否產生背壓?
*/
private final boolean hasBackPressure;
/**
* Does this partition use a limited number of (network) buffers?
* 分區是否使用有限制的網絡 buffer 數?
*/
private final boolean isBounded;
/**
* This partition will not be released after consuming if 'isPersistent' is true.
* 如果 isPersistent 爲 true,則在使用後不會釋放此分區
*/
private final boolean isPersistent;
}
2.4 StreamConfig
對於每一個 StreamOperator ,也就是 StreamGraph 中的每一個 StreamNode ,在生成 JobGraph 的過程中 StreamingJobGraphGenerator 都會創建一個對應的 StreamConfig 。 StreamConfig 中保存了這個算子 (operator) 在運行時需要的所有配置信息,這些信息都是 k/v 存儲在 Configuration 中的。
public class StreamConfig {
/**
* 保存 StreamOperator 信息
*/
@VisibleForTesting
public void setStreamOperator(StreamOperator<?> operator) {
setStreamOperatorFactory(SimpleOperatorFactory.of(operator));
}
/**
* 設置數據集的消費出邊集合
*/
public void setChainedOutputs(List<StreamEdge> chainedOutputs) {
try {
InstantiationUtil.writeObjectToConfig(chainedOutputs, this.config, CHAINED_OUTPUTS);
} catch (IOException e) {
throw new StreamTaskException("Cannot serialize chained outputs.", e);
}
}
// ...
}
2.5 StreamGraph 到 JobGraph 的核心轉換
- 下面我們就來看看 StreamGraph 中的 getJobGraph() 這個核心方法:
public class StreamGraph {
public JobGraph getJobGraph(@Nullable JobID jobID) {
return StreamingJobGraphGenerator.createJobGraph(this, jobID);
}
}
- 接着走到 StreamingJobGraphGenerator 的 createJobGraph() 方法:
public class StreamingJobGraphGenerator {
/**
* 傳入 StreamGraph,生成 JobGraph
*/
public static JobGraph createJobGraph(StreamGraph streamGraph) {
return createJobGraph(streamGraph, null);
}
public static JobGraph createJobGraph(StreamGraph streamGraph, @Nullable JobID jobID) {
return new StreamingJobGraphGenerator(streamGraph, jobID).createJobGraph();
}
private final StreamGraph streamGraph;
/**
* id -> JobVertex 的對應關係
*/
private final Map<Integer, JobVertex> jobVertices;
private final JobGraph jobGraph;
/**
* 已經構建的JobVertex的id集合
*/
private final Collection<Integer> builtVertices;
/**
* 物理邊集合(排除了chain內部的邊), 按創建順序排序
*/
private final List<StreamEdge> physicalEdgesInOrder;
/**
* 保存chain信息,部署時用來構建 OperatorChain,startNodeId -> (currentNodeId -> StreamConfig)
*/
private final Map<Integer, Map<Integer, StreamConfig>> chainedConfigs;
/**
* 所有節點的配置信息,id -> StreamConfig
*/
private final Map<Integer, StreamConfig> vertexConfigs;
/**
* 保存每個節點的名字,id -> chainedName
*/
private final Map<Integer, String> chainedNames;
private final Map<Integer, ResourceSpec> chainedMinResources;
private final Map<Integer, ResourceSpec> chainedPreferredResources;
private final Map<Integer, InputOutputFormatContainer> chainedInputOutputFormats;
/**
* 用於計算 hash 值的算法
*/
private final StreamGraphHasher defaultStreamGraphHasher;
private final List<StreamGraphHasher> legacyStreamGraphHashers;
/**
* 核心方法
* StreamGraph 轉 JobGraph 的整體流程
*/
private JobGraph createJobGraph() {
preValidate();
// make sure that all vertices start immediately
// 設置調度模式,streaming 模式下,默認是 ScheduleMode.EAGER ,調度模式是所有節點一起啓動
jobGraph.setScheduleMode(streamGraph.getScheduleMode());
// 1. 廣度優先遍歷 StreamGraph 並且爲每個 SteamNode 生成一個唯一確定的 hash id
// Generate deterministic hashes for the nodes in order to identify them across
// submission iff they didn't change.
// 保證如果提交的拓撲沒有改變,則每次生成的 hash id 都是一樣的,這裏只要保證 source 的順序是確定的,就可以保證最後生產的 hash id 不變
// 它是利用 input 節點的 hash 值及該節點在 map 中位置(實際上是 map.size 算的)來計算確定的
Map<Integer, byte[]> hashes = defaultStreamGraphHasher.traverseStreamGraphAndGenerateHashes(streamGraph);
// Generate legacy version hashes for backwards compatibility
// 這個設置主要是爲了防止 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<>();
// 2. 最重要的函數,生成 JobVertex/JobEdge/IntermediateDataSet 等,並儘可能地將多個 StreamNode 節點 chain 在一起
setChaining(hashes, legacyHashes, chainedOperatorHashes);
// 3. 將每個 JobVertex 的入邊集合也序列化到該 JobVertex 的 StreamConfig 中 (出邊集合已經在 setChaining 的時候寫入了)
setPhysicalEdges();
// 4. 根據 group name,爲每個 JobVertex 指定所屬的 SlotSharingGroup 以及設置 CoLocationGroup
setSlotSharingAndCoLocation();
// 5. 其他設置
// 設置 ManagedMemory 因子
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());
// 用戶的第三方依賴包就是在這裏(cacheFile)傳給 JobGraph
JobGraphGenerator.addUserArtifactEntries(streamGraph.getUserArtifacts(), jobGraph);
// set the ExecutionConfig last when it has been finalized
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;
}
}
這個方法首先爲所有節點生成一個唯一的 hash id,如果節點在多次提交中沒有改變(包括併發度、上下游等),那麼這個 id 就不會改變,這主要用於故障恢復。這裏之所以不能用 StreamNode.id 代替,是因爲 StreamNode.id 是一個從 1 開始的靜態計數變量,同樣的 job 在不同的提交中會得到不同的 id 。
如下所示兩個 job 是完全一樣的,但是 source A 和 B 的 id 卻不一樣了。
// 範例1: A.id=1 B.id=2
DataStream<String> A = ...
DataStream<String> B = ...
A.union(B).print();
// 範例2: A.id=2 B.id=1
DataStream<String> B = ...
DataStream<String> A = ...
A.union(B).print();
接着,就是最關鍵的 chaining 處理,生成 JobVertex、JobEdge 等。
先來看一下,Flink 是如何確定兩個 Operator 是否能夠被 chain 到同一個節點的,只要 StreamEdge 兩端的節點滿足以下條件,那麼這兩個節點就可以被串聯在同一個 JobVertex 中:
public class StreamingJobGraphGenerator {
/**
* StreamEdge 兩端的節點是否能夠被 chain 到同一個 JobVertex 中。
* 只要一條邊兩端的節點滿足下面的條件,那麼這兩個節點就可以被串聯在同一個 JobVertex 中
*/
public static boolean isChainable(StreamEdge edge, StreamGraph streamGraph) {
// 獲取到上游和下游節點
StreamNode upStreamVertex = streamGraph.getSourceVertex(edge);
StreamNode downStreamVertex = streamGraph.getTargetVertex(edge);
// 獲取到上游和下游節點具體的算子對應的 StreamOperator
StreamOperatorFactory<?> headOperator = upStreamVertex.getOperatorFactory();
StreamOperatorFactory<?> outOperator = downStreamVertex.getOperatorFactory();
// 要求下游節點只有一個輸入
return downStreamVertex.getInEdges().size() == 1
&& outOperator != null
&& headOperator != null
// 且在同一個 slot 共享組中
&& upStreamVertex.isSameSlotSharingGroup(downStreamVertex)
// 上下游算子的 chaining 策略,要允許 chaining ,默認是 ALWAYS
// 在添加算子時,也可以強制使用 disableChain 設置爲 NEVER
&& outOperator.getChainingStrategy() == ChainingStrategy.ALWAYS
&& (headOperator.getChainingStrategy() == ChainingStrategy.HEAD ||
headOperator.getChainingStrategy() == ChainingStrategy.ALWAYS)
// 上下游節點之間的數據傳輸方式必須是 FORWARD ,而不能是 REBALANCE 等其他模式
&& (edge.getPartitioner() instanceof ForwardPartitioner)
&& edge.getShuffleMode() != ShuffleMode.BATCH
// 上下游節點的並行度要一致
&& upStreamVertex.getParallelism() == downStreamVertex.getParallelism()
// chain enabled 配置項爲 true
&& streamGraph.isChainingEnabled();
}
}
下面來看下 setChaining() 這個關鍵方法:
public class StreamingJobGraphGenerator {
private void setChaining(Map<Integer, byte[]> hashes, List<Map<Integer, byte[]>> legacyHashes, Map<Integer, List<Tuple2<byte[], byte[]>>> chainedOperatorHashes) {
for (Integer sourceNodeId : streamGraph.getSourceIDs()) {
createChain(sourceNodeId, sourceNodeId, hashes, legacyHashes, 0, chainedOperatorHashes);
}
}
/**
* 構建 operator chain(可能包含一個或多個 StreamNode),返回值是當前的這個 operator chain 實際的輸出邊(不包含內部的邊)
* 如果 currentNodeId != startNodeId ,說明當前節點在 operator chain 的內部。
*
* 通過 DFS 遍歷所有的 StreamNode,並按照 chainable 的條件不停的將可以串聯的 operator 放在同一個 operator chain 中。
* 每一個 StreamNode 的配置信息都會被序列化到對應的 StreamConfig 中。只有 operator chain 的頭部節點會生成對應的 JobVertex ,
* 一個 operator chain 的所有內部節點都會以序列化的形式寫入頭部節點的 CHAINED_TASK_CONFIG 配置項中。
*/
private List<StreamEdge> createChain(
Integer startNodeId,
Integer currentNodeId,
Map<Integer, byte[]> hashes,
List<Map<Integer, byte[]>> legacyHashes,
int chainIndex,
Map<Integer, List<Tuple2<byte[], byte[]>>> chainedOperatorHashes) {
if (!builtVertices.contains(startNodeId)) {
// 當前 operator chain 最終的輸出邊,不包括內部的邊
List<StreamEdge> transitiveOutEdges = new ArrayList<StreamEdge>();
List<StreamEdge> chainableOutputs = new ArrayList<StreamEdge>();
List<StreamEdge> nonChainableOutputs = new ArrayList<StreamEdge>();
StreamNode currentNode = streamGraph.getStreamNode(currentNodeId);
// 將當前節點的出邊分爲兩組,即 chainable 和 nonChainable
for (StreamEdge outEdge : currentNode.getOutEdges()) {
// 判斷當前 StreamEdge 的上下游是否可以串聯在一起
if (isChainable(outEdge, streamGraph)) {
chainableOutputs.add(outEdge);
} else {
nonChainableOutputs.add(outEdge);
}
}
// 對於 chainable 的輸出邊,遞歸調用,找到最終的輸出邊並加入到輸出列表中
for (StreamEdge chainable : chainableOutputs) {
transitiveOutEdges.addAll(
createChain(startNodeId, chainable.getTargetId(), hashes, legacyHashes, chainIndex + 1, chainedOperatorHashes));
}
// 對於 nonChainable 的邊
for (StreamEdge nonChainable : nonChainableOutputs) {
// 這個邊本身就應該加入到當前節點的輸出列表中
transitiveOutEdges.add(nonChainable);
// 遞歸調用,以下游節點爲起點創建新的 operator chain
createChain(nonChainable.getTargetId(), nonChainable.getTargetId(), hashes, legacyHashes, 0, chainedOperatorHashes);
}
// 用於保存一個 operator chain 所有 operator 的 hash 信息
List<Tuple2<byte[], byte[]>> operatorHashes =
chainedOperatorHashes.computeIfAbsent(startNodeId, k -> new ArrayList<>());
byte[] primaryHashBytes = hashes.get(currentNodeId);
OperatorID currentOperatorId = new OperatorID(primaryHashBytes);
for (Map<Integer, byte[]> legacyHash : legacyHashes) {
operatorHashes.add(new Tuple2<>(primaryHashBytes, legacyHash.get(currentNodeId)));
}
// 當前節點的名稱,資源要求等信息
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());
}
// 如果當前節點是起始節點,則直接創建 JobVertex 並返回 StreamConfig ,否則先創建一個空的 StreamConfig
// createJobVertex 函數就是根據 StreamNode 創建對應的 JobVertex,並返回了空的 StreamConfig
StreamConfig config = currentNodeId.equals(startNodeId)
? createJobVertex(startNodeId, hashes, legacyHashes, chainedOperatorHashes)
: new StreamConfig(new Configuration());
// 設置 JobVertex 的 StreamConfig ,基本上是序列化 StreamNode 中的配置到 StreamConfig 中
// 其中包括 序列化器,StreamOperator,Checkpoint 等相關配置
setVertexConfig(currentNodeId, config, chainableOutputs, nonChainableOutputs);
if (currentNodeId.equals(startNodeId)) {
// 如果是 chain 的起始節點。(不是chain中的節點,也會被標記成 chain start)
config.setChainStart();
config.setChainIndex(0);
config.setOperatorName(streamGraph.getStreamNode(currentNodeId).getOperatorName());
// 把實際的輸出邊寫入配置,部署時會用到
config.setOutEdgesInOrder(transitiveOutEdges);
// operator chain 的頭部 operator 的輸出邊,包括內部的邊
config.setOutEdges(streamGraph.getStreamNode(currentNodeId).getOutEdges());
// 將當前節點(headOfChain)與所有出邊相連
for (StreamEdge edge : transitiveOutEdges) {
// 通過 StreamEdge 構建出 JobEdge,創建 IntermediateDataSet,用來將 JobVertex 和 JobEdge 相連
connect(startNodeId, edge);
}
// 將 operator chain 中所有子節點的 StreamConfig 寫入到 headOfChain 節點的 CHAINED_TASK_CONFIG 配置中
config.setTransitiveChainedTaskConfigs(chainedConfigs.get(startNodeId));
} else {
// 如果是 operator chain 內部的節點
chainedConfigs.computeIfAbsent(startNodeId, k -> new HashMap<Integer, StreamConfig>());
config.setChainIndex(chainIndex);
StreamNode node = streamGraph.getStreamNode(currentNodeId);
config.setOperatorName(node.getOperatorName());
// 將當前節點的 StreamConfig 添加到所在的 operator chain 的 config 集合中
chainedConfigs.get(startNodeId).put(currentNodeId, config);
}
// 設置當前 operator 的 OperatorID
config.setOperatorID(currentOperatorId);
if (chainableOutputs.isEmpty()) {
config.setChainEnd();
}
return transitiveOutEdges;
} else {
return new ArrayList<>();
}
}
}
上面的過程實際上就是通過 DFS 遍歷所有的 StreamNode,並按照 chainable 的條件不停的將可以串聯的 operator 放在同一個 operator chain 中。每一個 StreamNode 的配置信息都會被序列化到對應的 StreamConfig 中。只有 operator chain 的頭部節點會生成對應的 JobVertex ,一個 operator chain 的所有內部節點都會以序列化的形式寫入頭部節點的 CHAINED_TASK_CONFIG 配置項中。
每一個 operator chain 都會爲所有的實際輸出邊創建對應的 JobEdge,並和 JobVertex 連接,我們看下 createChain() 方法中的 connect() 方法:
public class StreamingJobGraphGenerator {
/**
* 每一個 operator chain 都會爲所有的實際輸出邊創建對應的 JobEdge,並和 JobVertex 連接
*/
private void connect(Integer headOfChain, StreamEdge edge) {
physicalEdgesInOrder.add(edge);
Integer downStreamvertexID = edge.getTargetId();
// 上下游節點
JobVertex headVertex = jobVertices.get(headOfChain);
JobVertex downStreamVertex = jobVertices.get(downStreamvertexID);
StreamConfig downStreamConfig = new StreamConfig(downStreamVertex.getConfiguration());
// 下游節點增加一個輸入
downStreamConfig.setNumberOfInputs(downStreamConfig.getNumberOfInputs() + 1);
StreamPartitioner<?> partitioner = edge.getPartitioner();
ResultPartitionType resultPartitionType;
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.");
}
JobEdge jobEdge;
// 創建 JobEdge 和 IntermediateDataSet
// 根據 StreamPartitioner 類型決定在上游節點(生產者)的子任務和下游節點(消費者)之間的連接模式
if (partitioner instanceof ForwardPartitioner || partitioner instanceof RescalePartitioner) {
jobEdge = downStreamVertex.connectNewDataSetAsInput(
headVertex,
DistributionPattern.POINTWISE,
resultPartitionType);
} else {
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);
}
}
}
3. 自帶 WordCount 示例詳解
對應着 4 層 Graph 的第二層:
後續補充debug詳細過程。
參考:
http://wuchong.me/blog/2016/05/10/flink-internals-how-to-build-jobgraph/
https://blog.jrwang.me/2019/flink-source-code-jobgraph/