Flink運行時之批處理程序生成計劃

批處理程序生成計劃

DataSet API所編寫的批處理程序跟DataStream API所編寫的流處理程序在生成作業圖(JobGraph)之前的實現差別很大。流處理程序是生成流圖(StreamGraph),而批處理程序是生成計劃(Plan)並由優化器對其進行優化並生成優化後的計劃(OptimizedPlan)。

什麼是計劃

計劃(Plan)以數據流(dataflow)的形式來表示批處理程序,但它只是批處理程序最初的表示,在一個批處理程序生成作業圖之前,計劃還會被進行優化以產生更高效的方案。Plan不同於流圖(StreamGraph),它以sink爲入口,因爲一個批處理程序可能存在若干個sink,所以Plan採用集合來存儲它:

protected final List<GenericDataSinkBase<?>> sinks = new ArrayList<>(4);

另外Plan還封裝了批處理作業的一些基本屬性:jobId、jobName以及defaultParallelism等。

Plan實現了Visitable接口,該接口表示其實現者是可遍歷的。Visitable要求實現者完善其accept方法,該方法接收一個Visitor作爲遍歷器對實現Visitable接口的對象進行遍歷。Plan對accept方法的實現是依次對所有的sink進行遍歷:

public void accept(Visitor<Operator<?>> visitor) {   
    for (GenericDataSinkBase<?> sink : this.sinks) {      
        sink.accept(visitor);   
    }
}

代碼段中的GenericDataSinkBase也間接實現了Visitable接口,在for循環中會調用它的accept方法。

Visitor接口提供了兩個遍歷方法,分別是前置遍歷的preVisit和用於後置遍歷的postVisit方法。Plan在內部實現了獲得當前批處理程序最大並行度的MaxDopVisitor遍歷器,preVisit會將當前遍歷算子的並行度跟已知的最大並行度進行對比,在兩者之間取較大值:

public boolean preVisit(Operator<?> visitable) {   
    this.maxDop = Math.max(this.maxDop, visitable.getParallelism());   
    return true;
}

獲取最大並行度的getMaximumParallelism方法,會實例化該遍歷器並調用accept方法進行遍歷來獲得整個批處理程序的最大並行度:

public int getMaximumParallelism() {   
    MaxDopVisitor visitor = new MaxDopVisitor();   
    accept(visitor);   
    return Math.max(visitor.maxDop, this.defaultParallelism);
}

代碼段中的accept方法即爲我們之前所展示的那個實現。由此可見accept內部定義了一種遍歷模式,而具體遍歷過程中要實現的邏輯,取決於對其應用的Visitor。這種設計將遍歷模式和遍歷邏輯進行了分離。

生成計劃源碼分析

跟流處理程序中生成流圖(StreamGraph)的方式類似,批處理程序中生成計劃(Plan)的觸發位置也位於執行環境類中。具體而言,是通過createProgramPlan方法來生成Plan的。生成Plan的核心部件是算子翻譯器(OperatorTranslation),createProgramPlan方法通過它來”翻譯“出計劃,核心代碼如下:

OperatorTranslation translator = new OperatorTranslation();
Plan plan = translator.translateToPlan(this.sinks, jobName);

根據之前我們對Plan的介紹,可知它是以sink爲源頭的,所以這裏在對計劃進行翻譯時,也接收的是sink集合。

OperatorTranslation,該類提供了大量的翻譯方法來對批處理程序進行翻譯。大致來看,它們之間的調用關係如下圖:

OperatorTranslation-method-call-chain

上圖中的藍色帶箭頭的線表示調用關係,而紅色的線表示互相調用的關係,也就是說它們之間存在遞歸調用

可以看出translateToPlan是這個類對外提供能力的入口方法。translateToPlan的完整實現如下代碼段:

public Plan translateToPlan(List<DataSink<?>> sinks, String jobName) {   
    List<GenericDataSinkBase<?>> planSinks = new ArrayList<GenericDataSinkBase<?>>();
    //遍歷sinks集合      
    for (DataSink<?> sink : sinks) {      
        //將翻譯生成的GenericDataSinkBase加入planSinks集合
        planSinks.add(
            //對每個sink進行”翻譯“
            translate(sink)
        );   
    }      
    //以planSins集合構建Plan對象
    Plan p = new Plan(planSinks);   
    p.setJobName(jobName);   
    return p;
}

上面代碼段中的translate方法, 它接收每個需遍歷的DataSink對象,然後將其轉換成GenericDataSinkBase對象。其實現如下:

private <T> GenericDataSinkBase<T> translate(DataSink<T> sink) {        
    Operator<T> input = translate(sink.getDataSet());      
    GenericDataSinkBase<T> translatedSink = sink.translateToDataFlow(input);            
    return translatedSink;
}

translate方法內部分爲兩步,第一步是對當前遍歷的sink的DataSet進行遞歸翻譯並獲得其輸入端的Operator對象:

Operator<T> input = translate(sink.getDataSet());

注意這裏的Operator對象是Flink core包中的Operator類型,而非批處理API包中的。

批處理相關的設計、命名相比流處理略顯混亂,這裏面當然有一些歷史包袱存在,不過這不是我們關心的重點。爲了避免產生混淆,同時爲下文作鋪墊,我們先分析一下批處理API的頂層設計以及core包中相關的類型設計。批處理API中的幾個關鍵對象DataSet、Operator、DataSource、DataSink之間的繼承和關聯關係如下圖:

DataSet-Operator-DataSource-DataSink-relationship

DataSet作爲批處理API抽象的同時也是Operator的父類,而Operator則是批處理中所有算子的父類。DataSource和DataSink在哪裏都是特殊的,這裏也不例外。DataSource繼承自Operator,因此它是一種特殊的算子。而DataSink跟上述這三個類不存在繼承關係,但它保持了對DataSet的引用,表示跟它關聯的數據集。

批處理API模塊跟流處理API模塊是完全獨立的,就算名稱相同的類,也不是雙方API所共享的。

上面緊接着的這行代碼中的translate方法在對DataSet進行翻譯的過程中會枚舉所有具體被支持的DataSet,並進行有針對性的翻譯,具體被支持的DataSet總共有下圖中被框起來的五個:

translate-supported-DataSet-type

對於Operator分支而言,因爲這這三種基本的Operator類型處於繼承鏈的最頂端,所以它們基本代表了所有後續派生的Operator。

注意,translate方法返回的Operator並不是批處理API包中的Operator類型,而是基礎包中的。具體而言,Flink提供了兩套Operator的抽象,它們分別是處於org.apache.flink.api.common.operators包以及org.apache.flink.api.java.operators包。上面展示的繼承關係圖中的Operator就是批處理的API模塊中的,在這個體系中,DataSink是獨立的。而core模塊中的operators包中的Operator是所有算子的抽象,在這個包中,source、sink都派生自Operator,繼承體系如下圖所示:

core-module-operators-package-class-diagram

因此批處理Java API模塊中的operators包不是核心模塊中的operators包的擴展與延伸。核心模塊中只是提供了一套公共的抽象,而批處理Java API提供的是面向編程接口的抽象。但他們之間並不是毫無聯繫,因爲在translate方法中,會從批處理Java API模塊中operators包往核心模塊中operators包的轉換,對應的轉換關係如下:

  • DataSource -> GenericDataSourceBase (通過DataSource的translateToDataFlow方法)
  • DataSink -> GenericDataSinkBase(通過DataSink的translateToDataFlow方法)
  • SingleInputOperator -> Operator (通過SingleInputOperator抽象的translateToDataFlow方法,供子類實現)
  • TwoInputOperator -> Operator (通過TwoInputOperator抽象的translateToDataFlow方法,供子類實現)
  • BulkIterationResultSet -> BulkIterationBase (直接構建)
  • DeltaIterationResultSet -> DeltaIterationBase (直接構建)

translate方法將會在對特定類型的DataSet的翻譯中觸發對其遞歸調用,其順序是從sink開始逆向往source方向進行的,同時會在它們之間建立關係。

這裏需要注意的是,這種模式跟流處理中的生成StreamGraph的差別很大。StreamGraph是依靠StreamNode以及StreamEdge來建立節點和邊之間的關係,並基於一個統一的StreamGraph數據結構在遍歷中收集所有的StreamNode以及StreamEdge。而批處理所生成的Plan卻並非是依靠一箇中心化的數據結構,在從sink開始進行逆向遍歷時,只構建當前算子跟其輸入端算子這種臨近算子之間的關係,這些關係被封裝在各個算子對象中。如果需要串聯起它們或者需要訪問DAG整體,那麼就需要通過遍歷器從sink開始依據這種兩兩之間的關係進行遍歷,因此這種模式可以看成是非中心化的。

每翻譯一個DataSet會將其加入到一個名爲translated的Map中去。translate方法最終返回的是sink緊鄰接着的輸入端的算子對象,該輸入端算子目前還沒有跟該sink進行關聯。所以,第二步就是調用下面這句將它們建立關係同時將批處理API中的DataSink翻譯爲核心包中的GenericDataSinkBase表示:

GenericDataSinkBase<T> translatedSink = sink.translateToDataFlow(input);

最終在遍歷完sinks集合後產生planSinks集合並以此創建Plan對象。

現在我們將注意力收回到createProgramPlan方法中來,剛剛已經創建完Plan對象,如果配置了自動類型註冊,那麼Plan將注入一個用於類型註冊的遍歷器來遍歷所有算子並對其類型進行註冊:

if (!config.isAutoTypeRegistrationDisabled()) {   
    plan.accept(new Visitor<org.apache.flink.api.common.operators.Operator<?>>() {            
        private final HashSet<Class<?>> deduplicator = new HashSet<>();            
        @Override      
        public boolean preVisit(org.apache.flink.api.common.operators.Operator<?> visitable) {         
            OperatorInformation<?> opInfo = visitable.getOperatorInfo();         
            Serializers.recursivelyRegisterType(opInfo.getOutputType(), config, deduplicator);         
            return true;      
        }      
        @Override      
        public void postVisit(org.apache.flink.api.common.operators.Operator<?> visitable) {}   
    });
}

完成自動類型註冊之後,下一步是將緩存文件註冊到Plan對象上:

registerCachedFilesWithPlan(plan);

何謂緩存文件?這裏的緩存文件是指用戶通過執行環境對象註冊的帶有名稱以及路徑的文件,該路徑可以是最終執行任務的工作節點本地的文件路徑,也可以是分佈式文件系統的路徑(這種情況Flink會將文件拷貝到本地)。

上面的這個方法會將註冊到執行環境對象的緩存文件註冊給Plan對象,以便後續生成JobGraph。

計劃優化

其實在Flink爲批處理程序生成計劃(Plan)之後,它會對計劃進行優化產生優化後的計劃(OptimizedPlan),而批處理程序對應的作業圖(JobGraph)則是基於OptimizedPlan生成的。OptimizedPlan的生成涉及到優化器相關的內容,更深入的分析請參考“優化器”相關的內容。因爲這裏的重心是介紹用戶程序的執行,而瞭解OptimizedPlan是分析JobGraph的前提,所以我們會對OptimizedPlan進行簡單介紹。

OptimizedPlan主要封裝瞭如下這些屬性:

  • dataSources:SourcePlanNode集合;
  • dataSinks:SinkPlanNode集合;
  • allNodes:優化後計劃中的所有PlanNode節點集合;
  • originalProgram:最初未被優化的Plan對象

在ClusterClient的run方法中生成OptimizedPlan:

OptimizedPlan optPlan = getOptimizedPlan(compiler, program, parallelism);

對getOptimizedPlan方法進行追蹤會發現其實生成OptimizedPlan的核心代碼就一句:

compiler.compile(p);

這裏的compiler是優化器Optimizer的實例,而參數p是Plan的實例。

在這之前optimizer模塊的名稱一直是compiler,近幾個版本才完成更名,但模塊內的很多註釋、變量以及方法名還是能發現那些歷史遺留痕跡。可以將後續遇到的所有compiler當作optimizer來理解。

在分析流處理程序生成StreamGraph時,我們展示了通過Flink的計劃可視化器生成StreamGraph的圖形化表示。同樣,計劃可視化器也可以展示批處理的OptimizedPlan的圖形化表示(遺憾的是無法展示Plan的圖形化表示)。我們以flink-examples-batch模塊中自帶的WordCount作爲示例程序來展示其執行計劃圖,在獲得其OptimizedPlan的JSON表示之前,需要對源程序進行一些改造。

首先,將執行環境的並行度設置爲2:

env.setParallelism(2);

然後將最終觸發程序執行的這句註釋掉:

//env.execute("WordCount Example");

換成下面這句:

System.out.print(env.getExecutionPlan());

將打印出來的OptimizedPlan的JSON字符串貼到Flink的計劃可視化器中,點擊下方的“Draw”按鈕即可生成。生成的圖如下:

Batch-WordCount-OptimizedPlan

從上圖中各個算子的ID編號可以看出生成計劃時其遍歷的順序是從sink開始的,因爲ID生成器是一個靜態計數器。

最後我們來看一下生成OptimizedPlan的JSON字符串的代碼:

public String getExecutionPlan() throws Exception {   
    Plan p = createProgramPlan("plan", false);     
    if (executor != null) {      
        return executor.getOptimizerPlanAsJSON(p);   
    }   else {      
        PlanExecutor le = PlanExecutor.createLocalExecutor(null);      
        return le.getOptimizerPlanAsJSON(p);   
    }
}

通過調用PlanExecutor的getOptimizerPlanAsJSON方法獲得OptimizedPlan並輸出其JSON字符串表示。

更多生成OptimizedPlan的有待分析“優化器”時再分析。


微信掃碼關注公衆號:Apache_Flink

apache_flink_weichat


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

qrcode_for_apache_flink_qq_group

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