Spark系列 - (4) Spark任務調度

目前已經更新完《Java併發編程》,《Spring核心知識》《Docker教程》和《JVM性能優化》,都是多年面試總結。歡迎關注【後端精進之路】,輕鬆閱讀全部文章。

Java併發編程:

Docker教程:

JVM性能優化:

Spring MVC系列:

Spark系列:

4. Spark任務調度

4.1 核心組件

本節主要介紹Spark運行過程中的核心以及相關組件。

4.1.1 Driver

Spark驅動器節點,用於執行Spark任務中的main方法,負責實際代碼的執行工作。Driver在Spark作業時主要負責:

  • 將用戶程序轉化爲任務(job)
  • 在Executor之間調度任務
  • 跟蹤Executor的執行情況
  • 通過UI展示查詢運行情況

4.1.2 Executor

Spark Executor 節點是一個JVM進程,負責在Spark作業中運行具體任務,任務彼此之間相互獨立。 Spark應用啓動時, Executor節點被同時啓動, 並且始終伴隨着整個Spark應用的生命週期而存在。 如果有Executor節點發生了故障或崩潰, Spark應用也可以繼續執行,會將出錯節點上的任務調度到其他 Executor節點上繼續運行。

Executor 有兩個核心功能:

  • 負責運行組成Spark應用的任務,並將結果返回給Driver進程;
  • 他們通過自身的塊管理器(Block Manager)爲用戶程序中要求緩存的RDD提供內存式存儲。RDD是直接緩存在Executor進程內的,因此任務可以在運行時充分利用緩存數據加速運算。

4.1.3 SparkContext

在Spark中由SparkContext負責與集羣進行通訊、資源的申請以及任務的分配和監控等。當Work節點中的Executor運行完Task後,Driver同時負責將SparkContext關閉,通常也可以使用SparkContext來代表驅動程序(Driver)。

SparkContext 是用戶通往 Spark 集羣的唯一入口,可以用來在Spark集羣中創建RDD 、累加器和廣播變量。SparkContext 也是整個 Spark 應用程序中至關重要的一個對象, 可以說是整個Application運行調度的核心(不包括資源調度)。

SparkContext的核心作用是初始化 Spark 應用程序運行所需的核心組件,包括高層調度器(DAGScheduler)、底層調度器( TaskScheduler )和調度器的通信終端( SchedulerBackend ), 同時還會負責Spark程序向ClusterManager的註冊等。

當RDD的action算子觸發了作業( Job )後, SparkContext 會調用DAGScheduler根據寬窄依賴將 Job 劃分成幾個小的階段( Stage ),TaskScheduler 會調度每個 Stage 的任務( Task ),另外,SchedulerBackend 負責申請和管理集羣爲當前Applic ation分配的計算資源(即 Executor ) 。

下圖描述了Spark-On-Yarn 模式下在任務調度期間, ApplicationMaster、Driver以及Executor內部模塊的交互過程:

Driver初始化SparkContext過程中,會分別初始化DAGScheduler、TaskScheduler 、SchedulerBackend 以及 HeartbeatReceiver,並啓動SchedulerBackend 以及 HeartbeatReceiver。SchedulerBackend通過 ApplicationMaster申請資源,並不斷從TaskScheduler 中拿到合適的Task分發到Executor執行。HeartbeatReceiver 負責接收Executor的心跳信息,監控Executor的存活狀況,並通知到TaskScheduler 。

4.2 YARN

Yarn雖然不屬於Spark的組件,但是現在Spark程序基本都是依賴Yarn來調度,因此專門介紹下YARN。

Yarn(Yet Another Resource Negotiator)是Hadoop集羣的資源管理系統,它是一個通用資源管理系統,可爲上層應用提供統一的資源管理和調度,它的引入爲集羣在利用率、資源統一管理和數據共享等方面帶來了巨大好處。

4.2.1 架構

將JobTracker和TaskTracker進行分離,它由下面幾大構成組件:

a. 一個全局的資源管理器 ResourceManager
b. ResourceManager的每個節點代理 NodeManager
c. 表示每個應用的ApplicationMaster
d. 每一個ApplicationMaster擁有多個Container在NodeManager上運行

  • Client:提交job。
  • Resource Manager:它是YARN的主守護進程,負責所有應用程序之間的資源分配和管理。每當它接收到處理請求時,它都會將其轉發給相應的節點管理器,並相應地分配資源以完成請求。它有兩個主要組成部分:
  1. Scheduler:它根據分配的應用程序和可用資源執行調度。它是一個純調度程序,意味着它不執行其他任務,例如監控或跟蹤,並且不保證在任務失敗時重新啓動。 YARN調度器支持Capacity Scheduler、Fair Scheduler等插件對集羣資源進行分區。
  2. Application Manager:它負責接受應用程序並與資源管理器協商第一個容器。如果任務失敗,它還會重新啓動 Application Master 容器。
  • Node Manager:它負責 Hadoop 集羣上的單個節點,並管理應用程序和工作流以及該特定節點。它的主要工作是跟上資源管理器的步伐。它向資源管理器註冊併發送帶有節點健康狀態的心跳。它監控資源使用情況,執行日誌管理,還根據資源管理器的指示殺死容器。它還負責創建容器進程並根據Application master的請求啓動它。

  • Application Master:應用程序是提交給框架的單個作業。應用主負責與資源管理器協商資源,跟蹤單個應用的狀態和監控進度。應用程序主機通過發送一個容器啓動上下文(CLC)從節點管理器請求容器,其中包括應用程序需要運行的所有內容。一旦應用程序啓動,它會不時地向資源管理器發送健康報告。

  • Container:它是單個節點上物理資源的集合,例如 RAM、CPU 內核和磁盤。容器由容器啓動上下文(CLC)調用,這是一個包含環境變量、安全令牌、依賴項等信息的記錄。

容器(Container)這個東西是 Yarn 對資源做的一層抽象。就像我們平時開發過程中,經常需要對底層一些東西進行封裝,只提供給上層一個調用接口一樣,Yarn 對資源的管理也是用到了這種思想。

如上所示,Yarn 將CPU核數,內存這些計算資源都封裝成爲一個個的容器(Container)。

4.2.2 任務提交流程

  1. 客戶端提交申請
  2. Resource Manager分配一個Container來啓動Application Manager
  3. Application Manager向Resource Manager註冊自己
  4. AM從RM申請容器資源
  5. AM通知 Node Manager 啓動容器
  6. 應用程序代碼在容器中執行
  7. 客戶端聯繫RM/AM以監控應用程序的狀態
  8. Job完成後,AM向RM取消註冊

4.3 Spark程序運行流程

在實際生產環境下, Spark集羣的部署方式一般爲 YARN-Cluster模式,之後的內核分析內容中我們默認集羣的部署方式爲YARN-Cluster模式。

下圖展示了一個Spark應用程序從提交到運行的完整流程:

提交一個Spark應用程序,首先通過Client向 ResourceManager請求啓動一個Application,同時檢查是否有足夠的資源滿足Application的需求,如果資源條件滿 足,則準備ApplicationMaster的啓動上下文,交給 ResourceManager,並循環監控Application狀態。

當提交的資源隊列中有資源時, ResourceManager 會在某個 NodeManager 上啓動 ApplicationMaster 進程,ApplicationMaster會單獨啓動Driver後臺線程,當 Driver啓動後,ApplicationMaster 會通過本地的 RPC 連接 Driver ,並開始向 ResourceManager 申請 Container 資源運行Executor進程(一個Executor對應與一個 Container),當ResourceManager返回Container 資源,ApplicationMaster則在對應的Container上啓動 Executor 。

Driver 線程主要是初始化 SparkContext 對象, 準備運行所需的上下文, 然後一方面保持與ApplicationMaster 的 RPC 連接,通過 ApplicationMaster 申請資源,另一方面根據用戶業務邏輯開始調度任務,將任務下發到已有的空閒Ex ecutor上。

當 ResourceManager 向 ApplicationMaster返回 Container 資源時,ApplicationMaster 就嘗試在對應的 Container 上啓動Executor 進程Executor進程起來後,會向Driver反向註冊,註冊成功後保持與Driver的心跳,同時等待 Driver 分發任務,當分發的任務執行完畢後,將任務狀態上報給Driver 。

從上述時序圖可知,Client只負責提交Application並監控 Application的狀態。對於Spark的任務調度主要是集中在兩個方面: 資源申請和任務分發,其主要是通過ApplicationMaster、Driver以及Executor之間來完成。

4.4 Spark任務調度概述

一個Spark程序包括Job、Stage以及Task三個概念:

  • Job是以Action方法爲界,遇到一個Action方法則觸發一個Job;
  • Stage是Job的子集,以RDD寬依賴(即Shuffle)爲界,遇到Shuffle做一次劃分;
  • Task是Stage的子集,以並行度(分區數)來衡量,分區數是多少,則有多少個task。

Spark任務的調度總體上分兩路進行,一路是Stage級的調度,一路是Task級的調度,總體的調度流程如下:

Spark RDD通過Transformation操作,形成了RDD血緣關係圖,即DAG,最後通過Action的調用,觸發job並調度執行。

  • DAGScheduler負責Stage級的調度,主要是將job切分成若干個Stages,並將每個Stage打包成TaskSet交給TaskScheudler調度。

  • TaskScheduler負責Task級的調度,將DAGScheduler傳過來的TaskSet按照指定的調度策略分發到Executor上執行,調度過程中SchedulerBackend負責提供可用資源,其中SchedulerBackend有多種實現,分別對接不同的資源管理系統。

4.4.1 Spark Stage級調度

下圖是Spark Stage級調度時的流程圖:

SparkContext將Job交給DAGScheduler提交, 它會根據 RDD 的血緣關係構成的 DAG 進行切分,將一個Job 劃分爲若干Stages,具體劃分策略是,由最終的RDD不斷通過依賴回溯判斷父依賴是否是寬依賴,即以Shuffle爲界,劃分Stage,窄依賴的 RDD之間被劃分到同一個 Stage 中,可以進行 pipeline 式的計算,如上圖紫色流程部分。劃分的Stages 分兩類,一類叫做ResultStage,爲 DAG 最下游的Stage,由Action方法決定,另一類叫做ShuffleMapStage,爲下游Stage準備數據。

下圖以WordCount爲例,說明整個過程:

一個Stage是否被提交,需要判斷它的父Stage是否執行,只有在父Stage執行完畢才能提交當前Stage,如果一個Stage沒有父Stage,那麼從該Stage開始提交。Stage提交時會將Task信息(分區信息以及方法等)序列化並被打包成TaskSet交給TaskScheduler。

4.4.2 Spark Task級調度

Spark Task的調度是由TaskScheduler來完成,由前文可知,DAGScheduler將Stage打包到TaskSet交給TaskScheduler,TaskScheduler會將TaskSet封裝爲TaskSetManager加入到調度隊列中,TaskSetManager結構如下圖所示。

TaskSetManager負責監控管理同一個Stage中的Tasks,TaskScheduler就是以TaskSetManager爲單元來調度任務。
TaskScheduler初始化後會啓動SchedulerBackend,它負責跟外界打交道,接收Executor的註冊信息,並維護Executor的狀態,TaskScheduler在SchedulerBackend輪詢它的時候,會從調度隊列中按照指定的調度策略選擇TaskSetManager去調度運行,大致方法調用流程如下圖所示:

TaskScheduler提交Tasks的原理

  1. 獲取當前TaskSet裏的所有Task;

  2. 根據當前的TaskSet封裝成對應的TaskSetManager。每一個TaskSet都會創建一個TaskSetManager與之對應。該TaskSetManager的作用就是監控它對應的所有的Task的執行狀態和管理。TaskScheduler就是以TaskSetManager爲調度單元去執行Tasks的;

  3. 將封裝好的TaskSetManager加入到等待的調度隊列等待調度,又schedueBuilder決定調度的順序,scheduleBuilder有兩種實現類,一種是FIFOOSchedulerBuilder,另一個是FairSchedulerBuilder,而Spark默認採用的是FIFO調度模式。

  4. 在初始化TaskSchedulerImpl的時候會調用start方法來啓動SchedulerBackend,SchedulerBackend(實際上是CoarseGrainedSchedulerBackend)調用riviveOffers方法。SchedulerBackend負責與外界打交道,接受來自Executor的註冊,並維護Executor的狀態。

  5. 調用CoarseGrainedSchedulerBackend的riviveOffers方法對Tasks進行調度。

  6. reviveOffers方法裏向DriverEndpoint發送ReviveOffers消息觸發調度任務的執行,DriverEndpoint接受到ReviveOffers消息後接着調用makeOffers方法

  7. SchedulerBackend(實際上是CoarseGrainedSchedulerBackend)負責將新創建的Task分發給Executor上執行。

調度策略

TaskScheduler支持兩種調度策略,一種是FIFO,也是默認的調度策略,另一種是FAIR。

在TaskScheduler初始化過程中會實例化rootPool,表示樹的根節點,是Pool類型。

1. FIFO調度策略

如果是採用FIFO調度策略,則直接簡單地將TaskSetManager按照先來先到的方式入隊,出隊時直接拿出最先進隊的TaskSetManager,其樹結構如下圖所示,TaskSetManager保存在一個FIFO隊列中。

2. FAIR調度策略(0.8開始支持)

FAIR模式中有一個rootPool和多個子Pool,各個子Pool中存儲着所有待分配的TaskSetMagager。

在FAIR模式中,需要先對子Pool進行排序,再對子Pool裏面的TaskSetMagager進行排序,因爲Pool和TaskSetMagager都繼承了Schedulable特質,因此使用相同的排序算法。

排序過程的比較是基於Fair-share來比較的,每個要排序的對象包含三個屬性: runningTasks值(正在運行的Task數)、minShare值、weight值,比較時會綜合考量runningTasks值,minShare值以及weight值。

注意,minShare、weight的值均在公平調度配置文件fairscheduler.xml中被指定,調度池在構建階段會讀取此文件的相關配置。

比較規則如下:

  1. 如果 A 對象的runningTasks大於它的minShare,B 對象的runningTasks小於它的minShare,那麼B排在A前面;(runningTasks 比 minShare 小的先執行)
  2. 如果A、B對象的 runningTasks 都小於它們的 minShare,那麼就比較 runningTasks 與 math.max(minShare1, 1.0) 的比值(minShare使用率),誰小誰排前面;(minShare使用率低的先執行)
  3. 如果A、B對象的runningTasks都大於它們的minShare,那麼就比較runningTasks與weight的比值(權重使用率),誰小誰排前面。(權重使用率低的先執行)
  4. 如果上述比較均相等,則比較名字。

整體上來說就是通過minShare和weight這兩個參數控制比較過程,可以做到讓minShare使用率和權重使用率少(實際運行task比例較少)的先運行。

FAIR模式排序完成後,所有的TaskSetManager被放入一個ArrayBuffer裏,之後依次被取出併發送給Executor執行。

從調度隊列中拿到TaskSetManager後,由於TaskSetManager封裝了一個Stage的所有Task,並負責管理調度這些Task,那麼接下來的工作就是TaskSetManager按照一定的規則一個個取出Task給TaskScheduler,TaskScheduler再交給SchedulerBackend去發到Executor上執行。

可以採用如下設置啓動公平調度器:

val conf = new SparkConf().setMaster(...).setAppName(...)
conf.set("spark.scheduler.mode", "FAIR")
val sc = new SparkContext(conf)
本地化調度

DAGScheduler切割Job,劃分Stage, 通過調用submitStage來提交一個Stage對應的tasks,submitStage會調用submitMissingTasks,submitMissingTasks 確定每個需要計算的 task 的preferredLocations,通過調用getPreferrdeLocations()得到partition的優先位置,由於一個partition對應一個Task,此partition的優先位置就是task的優先位置,

對於要提交到TaskScheduler的TaskSet中的每一個Task,該ask優先位置與其對應的partition對應的優先位置一致。

從調度隊列中拿到TaskSetManager後,那麼接下來的工作就是TaskSetManager按照一定的規則一個個取出task給TaskScheduler,TaskScheduler再交給SchedulerBackend去發到Executor上執行。前面也提到,TaskSetManager封裝了一個Stage的所有Task,並負責管理調度這些Task。 根據每個Task的優先位置,確定Task的Locality級別,Locality一共有五種,優先級由高到低順序:

在調度執行時,Spark 調度總是會盡量讓每個task以最高的本地性級別來啓動,當一個task以本地性級別啓動,但是該本地性級別對應的所有節點都沒有空閒資源而啓動失敗,此時並不會馬上降低本地性級別啓動而是在某個時間長度內再次以本地性級別來啓動該task,若超過限時時間則降級啓動,去嘗試下一個本地性級別,依次類推。

可以通過調大每個類別的最大容忍延遲時間,在等待階段對應的Executor可能就會有相應的資源去執行此task,這就在在一定程度上提升了運行性能。

失敗重試和黑名單

除了選擇合適的Task調度運行外,還需要監控Task的執行狀態,前面也提到,與外部打交道的是SchedulerBackend,Task被提交到Executor啓動執行後,Executor會將執行狀態上報給SchedulerBackend,SchedulerBackend則告訴TaskScheduler,TaskScheduler找到該Task對應的TaskSetManager,並通知到該TaskSetManager,這樣TaskSetManager就知道Task的失敗與成功狀態,對於失敗的Task,會記錄它失敗的次數,如果失敗次數還沒有超過最大重試次數,那麼就把它放回待調度的Task池子中,否則整個Application失敗。

在記錄Task失敗次數過程中,會記錄它上一次失敗所在的Executor Id和Host,這樣下次再調度這個Task時,會使用黑名單機制,避免它被調度到上一次失敗的節點上,起到一定的容錯作用。黑名單記錄Task上一次失敗所在的Executor Id和Host,以及其對應的“拉黑”時間,“拉黑”時間是指這段時間內不要再往這個節點上調度這個Task了。


參考:

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