Flink源碼剖析:flink-streaming-java 之 JobGraph

本文主要圍繞 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 。

在這裏插入圖片描述

圖1: 獲取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 文件:
在這裏插入圖片描述

圖2: flink-clients模塊的META-INF文件

PipelineExecutorFactory 的實現子類,分別對應着 Flink 的不同部署模式,如 local、standalone、yarn、kubernets 等:
在這裏插入圖片描述

圖3: PipelineExecutorFactory子類

這裏我們只看下 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 執行任務的提交:
在這裏插入圖片描述

圖4: PipelineExecutor子類
  • 接着,調用到 LocalExecutor 中的 getJobGraph() 方法,會反射出 StreamGraphTranslator 類,並調用它的 translateToJobGraph() 方法。

在這裏插入圖片描述

圖5:LocalExecutor的getJobGraph()方法的時序圖
@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;
    }
}
  1. 最後,調用 StreamGraphTranslator 的 translateToJobGraph() 方法,會一直調用到 StreamGraph 類自己的 getJobGraph() 方法。
    在這裏插入圖片描述
圖6:StreamGraphTranslator的translateToJobGraph()方法的時序圖
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 的核心轉換

  1. 下面我們就來看看 StreamGraph 中的 getJobGraph() 這個核心方法:
public class StreamGraph {
    public JobGraph getJobGraph(@Nullable JobID jobID) {
	    return StreamingJobGraphGenerator.createJobGraph(this, jobID);
    }
}
  1. 接着走到 StreamingJobGraphGenerator 的 createJobGraph() 方法:
    在這裏插入圖片描述
圖7: 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 的第二層:
在這裏插入圖片描述

圖8: WordCount示例從StreamGraph轉成JobGraph的示意圖

後續補充debug詳細過程。

參考:
http://wuchong.me/blog/2016/05/10/flink-internals-how-to-build-jobgraph/
https://blog.jrwang.me/2019/flink-source-code-jobgraph/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章