合久必分的特定任務
前面我們談到了TaskManager對每個Task實例會啓動一個獨立的線程來執行。在分析線程執行的核心代碼時,我們看到最終執行的是AbstractInvokable這樣執行體的invoke方法。所謂合久必分,鑑於流處理任務跟批處理任務執行模式上存在巨大的差異,在對AbstractInvokable的實現時,它們將會走向兩個不同的分支。
流處理相關的任務
流處理所對應的任務的繼承關係圖如下:
從上面的繼承關係圖可見,StreamTask是流處理任務的抽象。因爲在DataStream API中允許算子鏈接(operator-chain),算子鏈中的第一個算子稱之爲“head”,根據“head”算子輸入端個數以及角色的差別派生出三種不同類型的任務:
- SourceStreamTask:輸入源對應的流任務;
- OneInputStreamTask:單輸入端流任務;
- TwoInputStreamTask:雙輸入端流任務;
在這三大類流任務之下又會有其他一些特殊的流任務被派生。但StreamTask爲所有流任務的實現提供了基礎。它在AbstracInvokable的invoke核心方法中規定了一套統一的執行流程,這個流程會被任何一個流處理任務在執行時所遵循。這套流程步驟如下:
- 創建基本的輔助部件並加載算子鏈;
- 啓動算子;
- 特定任務相關的初始化;
- 打開算子;
- 執行run方法;
- 關閉算子;
- 銷燬算子;
- 公共的清理操作;
- 特定任務相關的清理操作;
以上步驟中被標記爲斜體字的步驟(3,5,9)都被定義爲抽象方法讓實現類填充邏輯。
除去SourceStreamTask之外,爲什麼要以輸入端的個數來區分任務呢?這一點跟Flink的設計是分不開的,它從API層到算子再到任務的設計都以輸入端的個數爲中心。我們可以看一下流處理中的所有算子的繼承關係圖:
可以看到除了StreamSource沒有擴展OneInputStreamOperator和TwoInputStreamOperator。其他所有算子都無一例外的擴展自這兩個接口。這一點跟在Task層面的繼承關係是一致的。
批處理相關的任務
批處理對應的任務繼承關係如下圖:
在批處理的任務設計中,將source和sink這兩個角色對應的任務獨立開,而其他承載業務邏輯的算子所對應的任務都由BatchTask來表示,迭代相關的任務也直接繼承自BatchTask。
如同流處理中的StreamTask一樣,BatchTask主要也是提供一套執行的流程並提供了基於驅動(Driver)的用戶邏輯代碼的抽象。
所謂Driver,它其實是銜接用戶邏輯代碼跟Flink運行時的任務(BatchTask)一個載體,這一點跟類似於計算機中介於軟硬件之間的驅動程序。
殊途同歸的UDF
其實不管Flink如何執行你的代碼,對終端用戶而言,它編寫Flink程序時做得最多的一件事是什麼?是覆寫Flink提供的一些函數並在其中實現自己的業務邏輯,這些包含業務邏輯的類,我們通常就稱之爲UDF(user-defined function,也即用戶定義的函數)。接下來,我們將關注點放在封裝了UDF的執行體上,看看在流處理和批處理中它們都是如何被實現的。我們選擇了一個很常見的函數Map,它在流處理中的實現爲StreamMap,在批處理中的實現爲MapDriver。
StreamMap在其processElement中的邏輯爲:
public void processElement(StreamRecord<IN> element) throws Exception {
output.collect(element.replace(userFunction.map(element.getValue())));
}
流處理算子的必須實現逐元素處理的processElement方法。
代碼段中的userFunction是MapFunction接口的實例。
接下來再看批處理中的MapDriver中的run方法:
public void run() throws Exception {
final MutableObjectIterator<IT> input = this.taskContext.getInput(0);
final MapFunction<IT, OT> function = this.taskContext.getStub();
final Collector<OT> output = new CountingCollector<>(
this.taskContext.getOutputCollector(), numRecordsOut);
if (objectReuseEnabled) {
IT record = this.taskContext.<IT>getInputSerializer(0).getSerializer().createInstance();
while (this.running && ((record = input.next(record)) != null)) {
numRecordsIn.inc();
output.collect(function.map(record));
}
}
else {
IT record = null;
while (this.running && ((record = input.next()) != null)) {
numRecordsIn.inc();
output.collect(function.map(record));
}
}
}
從Collector的collect方法中可以看到同樣會執行MapFunction的map方法。
兩者的MapFunction是同一個接口,來自org.apache.flink.api.common.functions。
所以歸根結底,不論流處理和批處理中它們的執行模式有何不同,對於相同語義的函數,UDF中用戶邏輯的執行是殊途同歸的。
關於更多用戶邏輯的執行細節,我們後續會進行分析。
微信掃碼關注公衆號:Apache_Flink
QQ掃碼關注QQ羣:Apache Flink學習交流羣(123414680)