Flink運行時之合久必分的特定任務

合久必分的特定任務

前面我們談到了TaskManager對每個Task實例會啓動一個獨立的線程來執行。在分析線程執行的核心代碼時,我們看到最終執行的是AbstractInvokable這樣執行體的invoke方法。所謂合久必分,鑑於流處理任務跟批處理任務執行模式上存在巨大的差異,在對AbstractInvokable的實現時,它們將會走向兩個不同的分支。

流處理相關的任務

流處理所對應的任務的繼承關係圖如下:

AbstractInvokable-for-streaming

從上面的繼承關係圖可見,StreamTask是流處理任務的抽象。因爲在DataStream API中允許算子鏈接(operator-chain),算子鏈中的第一個算子稱之爲“head”,根據“head”算子輸入端個數以及角色的差別派生出三種不同類型的任務:

  • SourceStreamTask:輸入源對應的流任務;
  • OneInputStreamTask:單輸入端流任務;
  • TwoInputStreamTask:雙輸入端流任務;

在這三大類流任務之下又會有其他一些特殊的流任務被派生。但StreamTask爲所有流任務的實現提供了基礎。它在AbstracInvokable的invoke核心方法中規定了一套統一的執行流程,這個流程會被任何一個流處理任務在執行時所遵循。這套流程步驟如下:

  1. 創建基本的輔助部件並加載算子鏈;
  2. 啓動算子;
  3. 特定任務相關的初始化;
  4. 打開算子;
  5. 執行run方法;
  6. 關閉算子;
  7. 銷燬算子;
  8. 公共的清理操作;
  9. 特定任務相關的清理操作;

以上步驟中被標記爲斜體字的步驟(3,5,9)都被定義爲抽象方法讓實現類填充邏輯。

除去SourceStreamTask之外,爲什麼要以輸入端的個數來區分任務呢?這一點跟Flink的設計是分不開的,它從API層到算子再到任務的設計都以輸入端的個數爲中心。我們可以看一下流處理中的所有算子的繼承關係圖:

all-StreamOperator-class-diagram

可以看到除了StreamSource沒有擴展OneInputStreamOperator和TwoInputStreamOperator。其他所有算子都無一例外的擴展自這兩個接口。這一點跟在Task層面的繼承關係是一致的。

批處理相關的任務

批處理對應的任務繼承關係如下圖:

AbstractInvokable-for-batch

在批處理的任務設計中,將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

apache_flink_weichat


QQ掃碼關注QQ羣:Apache Flink學習交流羣(123414680)

qrcode_for_apache_flink_qq_group

發佈了173 篇原創文章 · 獲贊 765 · 訪問量 171萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章