在理解Flink運行時架構之前,我們先用一個生活化的比喻來建立直觀認識:
想象你是一家大型工廠的總經理,需要生產一批復雜的產品。你會怎么做?
- 制定生產計劃:首先畫出生產流程圖,明確每個環節的工作內容
- 分解任務:將復雜的生產過程分解為多個可并行的工序
- 分配工人:為每個工序安排合適數量的工人并行作業
- 協調執行:確保各個工序之間的協調配合
Flink的運行時架構正是這樣一個"智能工廠"的管理系統。
ExecutionGraph:生產總指揮圖
什么是ExecutionGraph?
ExecutionGraph就像是工廠的總生產指揮圖,它是Flink程序在運行時的完整執行計劃。
// 用戶編寫的Flink程序(簡化示例)
DataStream<String> source = env.addSource(new FlinkKafkaConsumer<>(...));
DataStream<WordCount> counts = source.flatMap(new Tokenizer()) // 分詞算子.keyBy(value -> value.word) // 按key分組.window(TumblingEventTimeWindows.of(Time.seconds(10))) // 窗口.sum("count"); // 聚合算子
counts.addSink(new FlinkKafkaProducer<>(...));
這段用戶代碼經過Flink內部轉換,最終形成ExecutionGraph:
JobGraph (邏輯計劃)↓
ExecutionGraph (物理執行計劃)↓
實際運行的Task和SubTask
ExecutionGraph的關鍵特征
- 包含并行度信息:每個算子應該啟動多少個并行實例
- 包含資源分配:每個并行實例需要多少資源
- 包含數據流向:數據如何在各個并行實例之間流轉
- 包含容錯信息:如何進行checkpoint和故障恢復
Task:工廠中的生產線
Task的概念
Task可以理解為工廠中的一條完整生產線。由于算子鏈(Operator Chain)的優化,多個相鄰的算子會被合并到同一個Task中執行。
// 原始算子鏈
Source -> FlatMap -> Map -> KeyBy -> Window -> Sum -> Sink// 經過算子鏈優化后,可能形成這樣的Task:
Task1: Source -> FlatMap -> Map (算子鏈合并)
Task2: KeyBy -> Window -> Sum (算子鏈合并)
Task3: Sink
為什么要有算子鏈?
就像工廠為了提高效率,會把相關的工序安排在同一條生產線上,避免半成品在不同車間之間頻繁搬運。
算子鏈的好處:
- 減少數據序列化/反序列化開銷
- 減少網絡傳輸
- 減少線程切換
- 提高整體處理效率
Task的實際示例
public class ChainedMapTask extends StreamTask<String, StreamMap<String, String>> {@Overrideprotected void init() {// 初始化算子鏈中的所有算子SourceFunction sourceOperator = ...;MapFunction mapOperator = ...;// 構建算子鏈}@Overrideprotected void processInput() {// 處理輸入數據,在算子鏈中依次執行while (isRunning()) {Record record = sourceOperator.next();Record mapped = mapOperator.map(record);output.collect(mapped);}}
}
SubTask:生產線上的具體工位
SubTask的概念
如果Task是一條生產線,那么SubTask就是這條生產線上的具體工位。當我們設置并行度為4時,一個Task會被分解為4個SubTask,就像一條生產線復制了4份,同時工作。
// 設置并行度
source.flatMap(new Tokenizer()).setParallelism(4); // 創建4個SubTask// 在TaskManager中的實際執行
SubTask-0: 處理數據分區0
SubTask-1: 處理數據分區1
SubTask-2: 處理數據分區2
SubTask-3: 處理數據分區3
SubTask的生命周期
public class SubTask {// 1. 初始化階段public void initialize() {setupOperators();initializeState();registerMetrics();}// 2. 運行階段 public void run() {while (isRunning()) {processNextRecord();if (shouldCheckpoint()) {performCheckpoint();}}}// 3. 清理階段public void cleanup() {closeOperators();releaseResources();}
}
SubTask之間的數據交換
SubTask之間通過數據分區和網絡傳輸進行協作:
// KeyBy操作會觸發數據重分布
stream.keyBy(record -> record.getUserId()) // 按用戶ID分區.map(new UserProcessor());// 數據流轉示意:
SubTask-0: 用戶1,5,9... → 重分區 → SubTask-0: 所有用戶1的數據
SubTask-1: 用戶2,6,10... → 重分區 → SubTask-1: 所有用戶2的數據
SubTask-2: 用戶3,7,11... → 重分區 → SubTask-2: 所有用戶3的數據
SubTask-3: 用戶4,8,12... → 重分區 → SubTask-3: 所有用戶4的數據
三者關系總結
讓我們用一個完整的示例來理解三者關系:
// 1. 用戶程序
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();DataStream<String> lines = env.socketTextStream("localhost", 9999);
DataStream<Tuple2<String, Integer>> counts = lines.flatMap(new Tokenizer()) // 并行度4.keyBy(value -> value.f0).timeWindow(Time.seconds(5)).sum(1); // 并行度4counts.print(); // 并行度1
轉換過程:
1. ExecutionGraph層面:
ExecutionVertex-1: Source+FlatMap (并行度4)
ExecutionVertex-2: KeyBy+Window+Sum (并行度4)
ExecutionVertex-3: Print (并行度1)
2. Task層面:
Task-1: [Source -> FlatMap] 算子鏈
Task-2: [KeyBy -> Window -> Sum] 算子鏈
Task-3: [Print]
3. SubTask層面:
Task-1的SubTask實例:- SubTask-1-0 (處理數據分片0)- SubTask-1-1 (處理數據分片1) - SubTask-1-2 (處理數據分片2)- SubTask-1-3 (處理數據分片3)Task-2的SubTask實例:- SubTask-2-0 (處理特定key的數據)- SubTask-2-1 (處理特定key的數據)- SubTask-2-2 (處理特定key的數據) - SubTask-2-3 (處理特定key的數據)Task-3的SubTask實例:- SubTask-3-0 (匯總所有結果)
性能調優要點
理解了這三者關系后,我們就能更好地進行性能調優:
1. 合理設置并行度
// 根據數據量和CPU核數設置
env.setParallelism(Runtime.getRuntime().availableProcessors());// 為不同算子設置不同并行度
source.setParallelism(2); // IO密集型,并行度可以適當小些
transform.setParallelism(8); // 計算密集型,并行度可以大些
sink.setParallelism(1); // 輸出匯總,通常并行度為1
2. 優化算子鏈
// 禁用算子鏈(在需要時)
someStream.map(new MyMapper()).disableChaining() // 禁用與下游算子的鏈接.keyBy(...).startNewChain() // 從這里開始新的算子鏈.sum(1);
3. 監控SubTask運行狀況
// 通過Flink Web UI觀察:
// - 各個SubTask的吞吐量是否均衡
// - 是否存在數據傾斜
// - 網絡傳輸是否成為瓶頸
// - SubTask的CPU和內存使用情況
小結
- ExecutionGraph:整個作業的執行藍圖,包含所有執行細節
- Task:經過算子鏈優化的執行單元,是邏輯上的"工作組"
- SubTask:Task的并行實例,是實際執行計算的"工人"
三者關系就像建筑施工:ExecutionGraph是施工總圖紙,Task是各個專業工種組(如水電組、瓦工組),SubTask是每個工種組里的具體工人。理解這個關系有助于我們更好地設計和優化Flink應用程序。