Spark RDD上的map operators是如何pipeline起來的?

最近在工作討論中,同事提出了這麼一個問題:作用在一個RDD/DataFrame上的連續的多個map是在對數據的一次循環遍歷中完成的還是需要多次循環?

當時我很自然地回答說:不需要多次循環,spark會將多個map操作pipeline起來apply到rdd partition的每個data element上.

事後仔細想了想這個問題,雖然我確信spark不可能傻到每個map operator都循環遍歷一次數據,但是這些map操作具體是怎麼被pipeline起來apply的呢?這個問題還真不太清楚。於是乎,閱讀了一些相關源碼,力求把這個問題搞清楚。本文就是看完源碼後的一次整理,以防過幾天又全忘了。

我們從DAGScheduler的submitStage方法開始,分析一下map operators(包括map, filter, flatMap等) 是怎樣被pipeline起來執行的。

submit stage

我們知道,spark的每個job都會被劃分成多個stage,這些stage會被DAGScheduler以task set的形式提交給TaskScheduler以調度執行,DAGScheduler的submitStage方法實現了這一步驟。

如果當前stage沒有missingParentStage(未完成的parent stages),submitStage會調用submitMissingTasks,這個方法是做了一些工作的,主要有:

1. 找到當前stage需要計算的partitions

stage的partitions就是其對應rdd的partitions,那麼stage對應的rdd是怎麼確定的呢?源碼註釋是這樣解釋的:

@param rdd RDD that this stage runs on: for a shuffle map stage, it's the RDD we run map tasks on, while for a result stage, it's the target RDD that we ran an action on

我的理解是:對於shuffle map stage,它的rdd就是引發shuffle的那個operator(比如reduceByKey)所作用的rdd;對於result stage,就是action(比如count)所作用的rdd.

2. 初始化當前stage的authorizedCommiters

一個partition對應一個task,當一個task完成後,它會commit它的輸出結果到HDFS. 爲了防止一個task的多個attempt都commit它們的output,每個task attempt在commit輸出結果之前都要向OutputCommitCoordinator請求commit的permission,只有獲得批准的attempt才能commit. 批准commit的原則是: "first committer wins" . 

在submitMissingTasks方法中會把當前stage的所有partitions對應的tasks的authorizedCommitter都設置爲-1,也就是還沒有獲批的committer.

3. 獲取每個需要計算的partitions的preferred location

根據每個partition的數據locality信息獲取對應task的preferred locations.

4. 序列化並廣播taskBinary

taskBinary包含了執行task所需要的信息(包括數據信息,代碼信息)。對於不同的task type,taskBinary包含的信息有所不同。

spark有兩種類型的task : shuffle map task和result task, 與上面提到的shuffle map stage和result stage相對應。

shuffle map task的作用是生成rdd對應partition的output數據並將其劃分到多個buckets(每個reducer對應一個bucket)裏面,以便shuffle過程使用。這裏的劃分是依據shuffleDependency中指定的partitioner進行的,所以shuffle map task的taskBinary反序列化後的類型是(RDD[_], ShuffleDependency[_, _, _])

result task的作用是在對應的rdd partition上執行指定的function,所以result task的taskBinary反序列化後的類型是(RDD[T], (TaskContext, Iterator[T]) => U)

生成taskBinary的代碼:

5. 生成tasks

result stage生成result tasks,shuffle map stage生成shuffle map tasks.

有多少個missing partition,就會生成多少個task. 

可以看到taskBinary被作爲參數用於構建task對象。

6. 構建task set並向taskScheduler提交

spark map operators如何被pipeline的

通過上面的分析,我們知道rdd的map operators最終都會被轉化成shuffle map task和result task,然後分配到exectuor端去執行。那麼這些map operators是怎麼被pipeline起來執行的呢?也就是說shuffle map task和result task是怎麼把這些operators串聯起來的呢?

爲了回答這個問題,我們還需要閱讀一下ShuffleMapTask和ResultTask的源碼 : 

shuffle map task和result task都會對taskBinary做反序列化得到rdd對象並且調用rdd.iterator函數去獲取對應partition的數據。我們來看看rdd.iterator函數做了什麼:

rdd.iterator調用了rdd.getOrCompute

getOrCompute會先通過當前executor上的blockManager獲取指定block id的block,如果block不存在則調用computeOrReadCheckpoint,computeOrReadCheckpoint會調用compute方法進行計算,而這個compute方法是RDD的一個抽象方法,由RDD的子類實現。

因爲filter, map, flatMap操作生成的RDD都是MapPartitionsRDD, 所以我們以MapPartitionsRDD爲例:

可以看到,compute方法調用了parent RDD的iterator方法,然後apply了當前MapPartitionsRDD的f參數. 那這個f又是什麼function呢?我們需要回到RDD.scala中看一下map, filter, flatMap的code:



從上面的源碼可以看出,MapPartitionsRDD中的f函數就是對parent rdd的iterator調用了相同的map函數以執行用戶給定的function. 

所以這是一個逐層嵌套的rdd.iterator方法調用,子rdd調用父rdd的iterator方法並在其結果之上調用scala.collection.Iterator的map函數以執行用戶給定的function,逐層調用,直到調用到最初的iterator(比如hadoopRDD partition的iterator)。

現在,我們最初的問題:“多個連續的spark map operators是如何pipeline起來執行的?” 就轉化成了“scala.collection.Iterator的多個連續map操作是如何pipeline起來的?”

scala.collection.Iterator的map operators是怎麼構成pipeline的?

看一下scala.collection.Ierator中map, filter, flatMap函數的源碼:

從上面的源碼可以看出,Iterator的map, filter, flatMap方法返回的Iterator就是基於當前Iterator (self)override了next和hasNext方法的Iterator實例。比如,對於map函數,結果Iterator的hasNext就是直接調用了self iterator的hasNext,next方法就是在self iterator的next方法的結果上調用了指定的map function.

flatMap和filter函數稍微複雜些,但本質上一樣,都是通過調用self iterator的hasNext和next方法對數據進行遍歷和處理。

所以,當我們調用最終結果iterator的hasNext和next方法進行遍歷時,每遍歷一個data element都會逐層調用父層iterator的hasNext和next方法。各層的map function組成了一個pipeline,每個data element都經過這個pipeline的處理得到最終結果數據。

總結

1. 對RDD的operators最終會轉化成shuffle map task和result task在exectuor上執行。

2. 每個task (shuffle map task 或 result task)都會被分配一個taskBinary,taskBinary以broadCast的方式分發到每個executor,每個executor都會對taskBinary進行反序列化,得到對應的rdd,以及對應的function或shuffle dependency(function for result task, shuffle dependency for shuffle map task)。

3. task通過調用對應rdd的iterator方法獲取對應partition的數據,而這個iterator方法又會逐層調用父rdd的iterator方法獲取數據。這一過程底層是通過覆寫scala.collection.iterator的hasNext和next方法實現的。

4. RDD/DataFrame上的連續的map, filter, flatMap函數會自動構成operator pipeline一起對每個data element進行處理,單次循環即可完成多個map operators, 無需多次遍歷。

說明

1. 本文源碼均是apache spark 2.1.1版本。

2. 本文只討論了像filter, map, flatMap這種依次處理每個data element的map operators,對於像mapPartitions這種對partition進行處理的operator未做討論。

3. 如有錯誤,敬請指正。

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