在“Application啓動流程分析”文章的第4步提到了,driver接收到Executor發送的RegisterExecutor消息之後,通過makeOffers()將任務隨機分發給Executor。Executor(即CoarseGrainedExecutorBackend)收到後會將Task封裝成TaskRunner對象,然後提交到Executor的線程池中去執行。Executor的線程池是:newCachedThreadPool
Spark執行作業的大致流程爲:
Spark中的作業會按照RDD間的依賴關係劃分成多個stage,stage的劃分是由DAGScheduler來完成的,劃分的依據是寬依賴(是不是寬依賴是根據對RDD操作時使用哪個算子決定的,比如map就不是寬依賴)。然後DAGSchedule會按照調度依據數據本地性將任務交給TaskScheduler,由它將任務發送到Work節點,交給Executor執行。
如果某階段失敗,由DAGScheduler調度重新執行,如果某個Task失敗,由TaskScheduler調度重新執行。DAGSchedule還記錄RDD被存到磁盤,數據本地性,監控任務階段等操作。TaskSchedule發現某一任務沒有運行完,可能啓動另外一個相同的任務,哪個先完成用哪個結果(這個後續再分析下吧...目前沒看到相關的代碼)。
Spark中的Job分成兩類: 一類是action操作觸發的,另外一個是shuffle操作觸發的,前者返回的是ResultStage,後者返回的是ShuffleMapStage。看代碼的時候可以看到只有Shuffle操作的中間結果會寫磁盤,其餘窄依賴的中間結果是不會保存的!
作業執行源碼分析:
接下來以這個簡單的代碼爲例,對執行作業的源碼進行分析,作業代碼如下:
發現parallelize和map,filter等函數基本上都會調用到withScope()和sc.clean(f)這兩個函數,先來了解下這兩個函數是幹嘛用的。
WithScope{}:
這個方法是做DAG可視化用的,看下它的註釋,發現它是想讓所有created RDD的操作都在這個方法中進行。方法內部對輸入的方法沒有進行過任何的改動,所以不會影響方法的執行邏輯,所以這個函數我們可以不管它。
從代碼中可以看出,map函數的返回其實是一個MapPartitionRDD,所以map會被封裝在WithScope裏面
sc.clean(f):
它是爲了在分佈式環境上正確的閉包。閉包會把它對外的引用保存到自己內部,這樣閉包就可以單獨使用,而不用擔心它脫離了當前作用域。但是在分佈式環境中,如果外部引用是無法serializable的,就不能正確被髮送到Worker節點上去了。這個函數就是清理外圍類中無用域,降低序列化的開銷,防止不必要的不可序列化異常。(瞭解下就可以了,畢竟不是我們要關注的重點)
1.構建各個RDD之間的依賴關係,調用算子時完成。
parallelize()構造的RDD,返回一個ParallelCollectionRDD,這個沒啥好說了,new了一個對象而已。
沒有指定partition數量的情況下,默認生成多少個partition是由***SchedulerBackend類中根據如下方法計算的:
當代碼跑到count()操作的時候,我們發現在最後那個RDD(代碼中filter()方法返回的那個RDD)中已經完整的記錄了與所有父類RDD的依賴關係,並且記錄父類RDD是調用哪些方法生成的,對應的代碼在哪兒:
後來調細了之後才發現,rdd.map(***)的代碼最終會跳轉到RDD.scala裏面,然後構建RDD的依賴關係。也就是說創建RDD對象的時候,就已經記錄了該RDD和父RDD的依賴關係,並且根據Dependency的類型可以知道是寬依賴還是窄依賴。
可以看到,最終會使用當前RDD的引用構建一個OneToOneDependency對象,Dependency意思是RDD的依賴關係,OneToOneDependency代表窄依賴,RDD對象之間的依賴關係官方術語叫Lineage。
2.action操作觸發Job計算,將rdd、cleandFunc、所有partitions對應 的編號、整理partition結果集的函數等信息傳遞給dagScheduler,這一步是Job執行前的準備
在提交任務時已經構造好了存放返回結果集的數據結構,每個partition對應一個結果,所有的結果彙總到一個Array[U]中。如下圖所示:輸入是Partition編號,輸出是res,res是任意類型,然後將res賦值給results(index)
任務交給dagScheduler進行調度(全部完成之後做清理和檢查是否要checkPoint()的操作):
3.dagScheduler執行submitJob()方法,將DAGScheduler自身引用、jobId、partition size以及處理partition結果集的函數這四個信息封裝到一個JobWaiter對象中,並將這個對象交給eventProcessLoop內部類進行提交。然後提交任務的線程會阻塞等待Job完成,然後根據Job的返回狀態打印對應日誌。(ps:JobWaiter對象還可以用於取消本次Job)
submitJob()內部創建了一個JobWaiter對象,這個對象會立即返回。它是用於管理Job狀態用的,例如Finish、cancel,相當於JobWaiter是一個回調對象:
JobWaiter對象的定義如下:
注意,submitTask()方法中,任務是通過eventProcessLoop.post(JobSubmitted(...))提交的,這是向消息隊列中放入作業,然後執行對應的邏輯。eventProcessLoop這個對象是在new DAGSchedule時創建的,本質是一個單獨的線程。最終會調用到dagScheduler中的handleJobSubmitted()方法。
那不還是要讓dagScheduler執行麼?爲啥要單獨再開一個線程 – 按照網上找到的說法,這裏是爲了處理方式的統一,不管是別人的消息還是自己的消息統一放在一個地方處理利於擴展,並且代碼也會很乾淨。
4.eventProcessLoop線程會調用dagScheduler中的handleJobSubmitted()方法。使用createResultStage()劃分stage,該方法返回List[Stage]。
createRsultStage()方法創建的是所有的stage。創建stage的方法是從最後一個RDD生成的ResultStage開始,使用getOrCreateParentStages()找出其祖先RDD所有的shuffle操作。如果沒有Shuffle,當前job只有一個ResultStage。如果有shuffle,那麼當前job至少還有一個ShuffleMapStage,有ShuffleMapStage就代表該ResultStage存在父調度階段。
創建好所有parentStages(即ShuffleMapStage之後),放到一個List中返回,越靠近當前RDD的ShuffleMapStage越在前面,最後才創建action操作對應的ResultStage。
我們來看下getOrCreateParentStages()方法內部是怎麼劃分出當前RDD的所有ShuffleDependency的:
該方法中首先調用了getShuffleDependencies()方法,它的目的在於獲取該rdd的第一個寬依賴。舉個例子:如果 A => B => C,那麼返回的就是B => C的寬依賴,而不會返回A => B的寬依賴。
確實存在shuffle,則判斷對應的ShuffleMapStage的信息是否已存在(創建),如果已存在就直接返回,否則需要接着執行getMissingAncestorShuffleDependencies()方法,計算從shuffleDep所在的RDD開始往前回溯的所有父RDD的ShuffleDependency信息。
getMissingAncestorShuffleDependencies()用於查找沒有在shuffleToMapStage中註冊的所有祖先shuffle依賴。其實就是遞歸的調用上面的getShuffleDependencies()方法,將寬依賴一個個找出來,所以你會發現它們的代碼很像,就只有下面標紅的那一點不一樣:
最後調用createShuffleMapStage()方法爲每一個ShuffleDependency創建一個ShuffleMapStage,放入到List[Stage]中。離當前RDD越遠的寬依賴越在棧頂,所以計算stage是從後往前計算的,即最開始的RDD最先被計算。
創建ShuffleMapStage的代碼如下,從中可以看出,RDD有多少個Partition就會對應有多少個Tasks。一個ShuffleMapStage會記錄所有的父Stage以及當前RDD的shuffleDep等信息。最後將該stage註冊到mapOutputTracker中,這樣做是避免stage重試的時候全部重新計算。
通俗點說就是一個ShuffleMapStage會記錄它自己和它的祖先們是怎麼來的。至此就已經完成了Stage的劃分和註冊。
5.Stage劃分完成之後,接着調用handleJobSubmitted()方法中的submitStage()方法提交finalStage(即ResultStage)。方法內部其實是依次遞歸的解析和提交每個stage所依賴的父stage,所以最終最先提交的是沒有任何依賴的stage(最開始的那一個stage)。
getMissingParentStages()方法和之前創建stage的方法類似,這裏就不再對它進行詳細分析了。它的作用就是看下該stage的父stage如果沒有提交的話,先提交父stage。我們直接看submitMissingTasks(stage, jobId.get)方法看下提交一個stage到底要。
6.按Stage的順序提交Stage,根據Partition的數據本地性爲每一個Stage構建對應的TaskLocation,將構建好之後的Stage序列化發送到Spark的各節點。(注意如果Stage是重新提交的話,已經Success的partition是不需要重新計算的)
首先判斷多少個Partition需要計算,側面說明了stage中某個partition完成之後是會做一個標記的,這樣做是爲了避免stage重提時的重複計算。輸出的是0-job.numPartitions這樣的partiton編號。
將stage的信息(id+partitionNum)記錄到outputCommitCoordinator,該變量是在SparkEnv中,具體幹啥用的還不太清楚……
然後爲每個Partition找到最佳執行位置,即考慮到數據本地性。該方法會遞歸調用父RDD的getPreferredLocations(split:Partition)找到最佳執行位置。(數據本地性怎麼確定,後續“補充”那裏有講到)
然後將stage的信息序列化,broadcast到各個Executor上。
最後根據stage中需要compute的Partition的數量對應創建多少個Task,將這些Task集中放到Seq裏面。Task會記錄locality等信息。對於ResultStage生成ResultTask,對於ShuffleMapStage生成ShuffleMapTask。
最後的最後,將這些Seq[Task[_]]交給TaskScheduler執行;或者當前stage完成,提交下一個stage。
7.將TaskSet交給TaskScheduler的調度池,再調用***SchedulerBackend的reviveOffers()給這些Task分配資源並執行Task
Task是由TaskManager來管理的,每批次TaskSet都會新建一個TaskManager,然後加入到調度器統一調配。調度器的種類有兩種,在初始化SparkContext的時候創建的,默認的是FIFO,可以看下之前的初始化SparkContex那篇文章。
這裏的rootPool又是個啥????它是在SparkContex初始化TaskSchedulerImpl時創建的,好像是個隊列…
最後調用***SchedulerBackend中的reviveOffers(),該方法估計又是爲了統一,反正最終又執行到了***SchedulerBackend 中的makeOffers()給Task分配資源然後執行:
makeOffers()會先獲取集羣中可用的Executor,然後發送到TaskSchedulerImpl中對任務集的任務分配資源,最後提交到LaunchTask方法中:
資源分配的過程是這樣的:(補充裏面其實講到了)
給每個Task創建Description的代碼如下,創建Description相當於是說這個Task要在哪個Executor上運行,並且數本地性怎樣…不知道這個屬性用不用的上:
然後就調用launchTasks,給對應的Executor發送消息,讓Executor執行任務了。
8.Executor反序列化Task信息(TaskDescription),構造一個TaskRunner對象(Runnable)然後扔到線程池中執行Task。
看下TaskRunner中的run()方法,看下任務內部到底是怎麼執行的:
首先是序列化任務依賴的jar包以及文件,因爲我是把所有的依賴都打到一個包裏面的,所以這裏看到只有一個with-dependencies.jar的整包。接着是執行Task,Task是一個抽象類,真正執行的是ShuffleMapTask或者ResultTask中的方法。執行完畢之後,將結果發送回Driver。
再來細看下ShuffleMapTask和ResultTask分別是怎麼執行的:
先看ResultTask:
它在計算時會調用func(context,rdd.iterator(partition,context)),其中rdd.iterator會遞歸調用調用RDD的compute(),最終會從第一個RDD的元素開始計算。從代碼調試的結果也可以看出,確實是遞歸執行的。
再看下ShuffleMapTask:
它返回給Driver的是MapStatus,它是將中間結果寫到文件,然後將這些文件的位置返回給Driver。
這個涉及到Shuffle內部的細節後續分析到Shuffle的時候再細講。
根據之前的描述,梳理下Job執行的流程圖:
補充:Task的數據本地性介紹
Spark在處理任務時會考慮數據的本地性,畢竟移動計算比移動數據代價要小,利於提升程序的執行效率。Spark的數據本地性共劃分爲五種情況:
- PROCESS_LOCAL 同一個Executor中,速度最快
- NODE_LOCAL 本地,數據在其他Executor中/HDFS恰好有個Block
- NO_PREF 沒有偏好,哪裏訪問都也一樣快
- RACK_LOCAL 本地機架,需要通過網絡傳輸
- ANY 任何,速度最慢
之前劃分stage的時候說過,在爲每個Partition構建對應的Task信息(即TaskLocation)時會執行getPreferredLocations(split:Partition)方法,確定Partition的優先位置,代碼邏輯如下:
該方法的返回是一個TaskLocation變量,這是一個trait,它有三個實現類,分別代表數據存儲在不同的位置,從上到下它們的含義分別是:
- 數據存儲在Executor內存中,即Partition被cache到了內存中(返回executorId+host)
- 數據存儲在HDFS上(返回host)
- 數據存儲在host這個節點的磁盤上(返回“hdfs_”+host)
知道了partition的本地性之後,接着就是將任務加入到隊列,並計算整個TaskSet的本地性了,計算是在submitTask()方法構建TaskSetManager時進行的:
接着會執行上面說過的步驟7中的第6步再細講下到低是怎麼爲每一個TaskSet分配資源的: resourceOfferSingleTaskSet(),爲每一個TaskSet分配資源:
(怎麼爲每個Task分的?這裏後續補充下吧)
從編寫的代碼運行邏輯中也可以看出,因爲我們的RDD是第一次計算並且沒有真正cache過(真正cache是指第一次action操作觸發之後,會記錄信息到cacheLocs裏面,第二次纔會從這個裏面找信息),所以不會走第一個方法。而且我們的RDD也沒有checkpoint,也不是最開始離數據源最近拉數據的那個RDD,所以會走第三個方法,遞歸的去找數據:
任務執行時,本地性可以在Spark的UI界面直接看到:
回答之前留下的幾個疑問:
問題1:收到RegisterExecutor的消息的時候,也會調用makeOffer()。兩者哪個在前哪個在後,優先看下sparkContext是怎麼啓動的吧,不然又是一頭霧水:…確實是SparkContext啓動在前,但是那個時候是沒有任務的,發送的空的,所以它是在等待執行具體的task的時候用的。
問題2:注意下,這個***ScheduleBackend是在driver端初始化用於進行一些資源管理的,CoarseGrainedExecutorBackend是在Executor端進行初始化,它是Executor運行的容器。兩者名字很像,但是功能是不一樣的。(看過SparkContext啓動流程之後,這兩個幹嘛用的就特別清晰了,輕者是管理任務怎麼分配什麼的,收集Worker的信息,計算Executor和Driver啓動在哪些Worker上,Task扔給哪些Executor執行)
問題3:最後就是之前提到過的……Stage完成之後,DAGSchedule會調用handleTaskCompletion方法,根據Stage返回的結果判定是否是 Success/Resubmitted/FetchFailed…然後進行相應的處理。
遺留的問題:
- ShuffleMapTask調用write的時候是如何進行Shuffle的?
- RDD的checkpoint是怎麼操作的?
- Job執行的過程中,內存是怎麼管理的?
- map之類的dep比較好構建,groupByKey的依賴是怎麼構建的?
- Executor中途掛了怎麼辦?
後續博客分析這些問題
參考:
http://www.mamicode.com/info-detail-1066067.html(withScope的作用)
https://www.jianshu.com/p/51f5a34e2785(sc.clean(f)的作用)
https://www.cnblogs.com/jcchoiling/p/6438435.html(DAGSchedule中爲什麼要單獨再開一個線程處理消息)
https://www.jianshu.com/p/8e7cd025d0ba(getProferLocation()方法解析)
http://www.cnblogs.com/chushiyaoyue/p/7468952.html(SparkContex初始化的時候調用makeOffers())
https://blog.csdn.net/qq_41774522/article/details/81707613(Spark執行流程圖,很詳細)
https://www.jianshu.com/p/05034a9c8cae/(Task數據本地性介紹)