標籤:
本節課內容:
1. TaskScheduler工作原理
2. TaskScheduler源碼
一、TaskScheduler工作原理
總體調度圖:
通過前幾節課的講解,RDD和DAGScheduler以及Worker都已有深入的講解,這節課我們主要講解TaskScheduler的運行原理。
回顧:
DAGScheduler面向整個Job劃分多個Stage,劃分是從後往前的回溯過程;運行時從前往後啟動並執行。每個Stage中有很多任務Task,Task是可以並存執行的。它們的執行邏輯完全相同的,只不過是處理的資料不同而已,DAGScheduler通過TaskSet的方式,把其構造的所有Task提交給底層調度器TaskScheduler。
& TaskScheduler是一個trait,與具體的資源調度解耦合,這符合物件導向中依賴抽象不依賴具體的原則,帶來底層資源調度器的可插拔性,導致Spark可以啟動並執行眾多的資源調度模式上,例如:StandAlone、Yarn、Mesos、Local、EC2或者其他自訂的資源調度器。
在StandAlone模式下,我們來看看TaskScheduler的一個實現TaskSchedulerImpl。
1.TaskScheduler的核心任務
TaskScheduler的核心任務是提交TaskSet到叢集並彙報結果,主要負責Application的不同Job之間的調度。
具體來講有以下幾點:
(1) 為TaskSet建立和維護一個TaskSetManager並追蹤任務的本地性以及錯誤資訊;
(2) Task執行失敗時啟動重試機制,以及遇到Straggle任務會在其他節點上啟動備份任務;
(3) 向DAGScheduler彙報執行情況,包括在shuffle輸出丟失的時候報告fetch failed錯誤等資訊。
2.TaskScheduler的核心功能
(1)註冊當前程式。
& TaskScheduler內部會握有SchedulerBackend引用,SchedulerBackend是一個trait,它主要負責管理Executor資源,從StandAlone模式來講,具體實現是SparkDeploySchedulerBackend。SparkDeploySchedulerBackend在啟動時會構造AppClient執行個體並在該執行個體start的時候啟動ClientEndpoint(訊息迴圈體),ClientEndpoint在啟動時會向Master註冊當前程式。
(1) 註冊Executor資訊。
& SparkDeploySchedulerBackend的父類CoarseGrainedSchedulerBackend會在Start的時候執行個體化一個類型DriverEndpoint的訊息迴圈體。DriverEndpoint就是我們程式運行時的Driver對象。SparkDeploySchedulerBackend是專門給來負責收集Worker上的資源資訊,當ExecutorBackend啟動的時候會發送RegisteredExecutor資訊向Driver中的DriverBackend進行註冊。(可以參考前幾講的Master註冊部分。)此時SparkDeploySchedulerBackend就掌握了當前應用應用程式所擁有的計算資源,TaskScheduler就是通過SparkDeploySchedulerBackend擁有的計算資源來具體運行Task。
& 補充:SparkContext、DAGScheduler、TaskSchedulerImpl、SparkDeploySchedulerBackend在應用程式啟動的時候值執行個體化一次,應用程式存在期間始終存在這些對象。SparkDeploySchedulerBackend是一個輔助類,主要是協助TaskSchedulerImpl中的Task擷取計算資源和發送Task到叢集中
3.TaskScheduler的執行個體化時機
TaskScheduler是在SparkContext執行個體化時進行執行個體化的,如TaskSchedulerImpl的執行個體化。(Spark1.6.0 SparkContext.scala #521-#526)
// Create and start the scheduler val (sched, ts) = SparkContext.createTaskScheduler(this, master) _schedulerBackend = sched _taskScheduler = ts _dagScheduler = new DAGScheduler(this) _heartbeatReceiver.ask[Boolean](TaskSchedulerIsSet)
在SparkContext#createTaskScheduler方法中會建立TaskScheduler和SparkDeploySchedulerBackend:
private def createTaskScheduler( sc: SparkContext, master: String): (SchedulerBackend, TaskScheduler) = { import SparkMasterRegex._ //省略部分代碼case SPARK_REGEX(sparkUrl) => val scheduler = new TaskSchedulerImpl(sc) val masterUrls = sparkUrl.split(",").map("spark://" + _) val backend = new SparkDeploySchedulerBackend(scheduler, sc, masterUrls)//利用SparkDeploySchedulerBackend來初始化TaskScheduler scheduler.initialize(backend) //1 (backend, scheduler)}
4.TaskScheduler初始化
<pre name="code" class="plain">//1被處調用def initialize(backend: SchedulerBackend) { this.backend = backend // temporarily set rootPool name to empty rootPool = new Pool("", schedulingMode, 0, 0) //2//根據rootPool中的演算法建立可調度對象 schedulableBuilder = { schedulingMode match { case SchedulingMode.FIFO =>//FIFO模式 new FIFOSchedulableBuilder(rootPool) case SchedulingMode.FAIR =>//Fair模式 new FairSchedulableBuilder(rootPool, conf) } } //建立調度池 schedulableBuilder.buildPools() }
建立調度池
(1)建立rootPool(實現調度演算法)
//2處被調用private[spark] class Pool( val poolName: String, val schedulingMode: SchedulingMode, initMinShare: Int, initWeight: Int) extends Schedulable with Logging { val schedulableQueue = new ConcurrentLinkedQueue[Schedulable] val schedulableNameToSchedulable = new ConcurrentHashMap[String, Schedulable] var weight = initWeight var minShare = initMinShare var runningTasks = 0 var priority = 0 // A pool's stage id is used to break the tie in scheduling. var stageId = -1 var name = poolName var parent: Pool = null//根據不同的調度演算法建立調度演算法的執行個體 var taskSetSchedulingAlgorithm: SchedulingAlgorithm = { schedulingMode match { case SchedulingMode.FAIR => new FairSchedulingAlgorithm() case SchedulingMode.FIFO => new FIFOSchedulingAlgorithm() } }
(2)建立可調度對象
Org.apache.spark.scheduler.Pool包含了一組可以調度的實體。對於FIFO來說,rootPool包含了一組TaskSetManager;而對於FAIR來說,rootPool包含了一組Pool,這些Pool構成了一顆調度樹,其中這棵樹的葉子節點就是TaskSetManager。
(3)建立調度池
schedulableBuilder.buildPools()因調度方式而異,如果是FIFO,它的實現是空的如下:
private[spark] class FIFOSchedulableBuilder(val rootPool: Pool) extends SchedulableBuilder with Logging { override def buildPools() { // nothing }//定義了如何將TaskSetManager加入到調度池中 override def addTaskSetManager(manager: Schedulable, properties: Properties) { rootPool.addSchedulable(manager) //3 }}
因為rootPool並沒有包含Pool,而是直接包含TaskSetManager:submitTasks直接將TaskSetManager添加到rootPool(調度隊列,隊列預設是先入先出)即可。
//將可調度對象加入到調度隊列 3處被調用 override def addSchedulable(schedulable: Schedulable) { require(schedulable != null) schedulableQueue.add(schedulable) schedulableNameToSchedulable.put(schedulable.name, schedulable) schedulable.parent = this }
而FAIR模式則需要在運行前先進行一定的配置。它需要在rootPool的基礎上根據這個設定檔來構建這顆調度樹。
具體實現見如下代碼:
override def buildPools() { var is: Option[InputStream] = None try { is = Option {//以spark.scheduler.allocation.file設定的檔案名稱字來建立FileInputStream schedulerAllocFile.map { f => new FileInputStream(f) }.getOrElse {//若spark.Scheduler.allocation.file沒有設定,則直接以fairscheduler.xml建立//FileInputStream Utils.getSparkClassLoader.getResourceAsStream(DEFAULT_SCHEDULER_FILE) } }//以is對應的內容建立Pool is.foreach { i => buildFairSchedulerPool(i) } } finally { is.foreach(_.close()) }建立名為“default”的Pool buildDefaultPool() }
(4)調度演算法
private[spark] traitSchedulingAlgorithm { <span style="white-space:pre"></span>def comparator(s1: Schedulable, s2:Schedulable): Boolean }
從代碼來看調度演算法是一個trait,需要子類實現。其實質就是封裝了一個比較函數。子類只需實現這個比較函數即可。
(a)FIFO
採用FIFO任務調度的順序:
首先要保證JobID較小的先被調度,如果是同一個Job,那麼StageID小的先被調度(同一個Job,可能多個Stage可以並存執行,比如Stage劃分過程中Stage0和Stage1)。
調度演算法:
private[spark] class FIFOSchedulingAlgorithm extends SchedulingAlgorithm {//比較可調度對象s1與s2,這裡s1與s2其實就是TaskSetManager。 override def comparator(s1: Schedulable, s2: Schedulable): Boolean = { val priority1 = s1.priority //這個priority實際上就是Job ID val priority2 = s2.priority //同上 var res = math.signum(priority1 - priority2) //首先比較Job ID if (res == 0) { //如果Job ID相同,那麼比較Stage ID val stageId1 = s1.stageId val stageId2 = s2.stageId res = math.signum(stageId1 - stageId2) } if (res < 0) { true } else { false } }}
(2)
FAIR
對於FAIR,首先是掛在rootPool下面的pool先確定調度順序,然後在每個pool內部使用相同的演算法來確定TaskSetManager的調度順序。
演算法實現:
private[spark] class FairSchedulingAlgorithm extends SchedulingAlgorithm { override def comparator(s1: Schedulable, s2: Schedulable): Boolean = {//最小共用,可以理解為執行需要的最小資源即CPU核心數,其他相同時,所需最小核心數小的優先//調度 val minShare1 = s1.minShare val minShare2 = s2.minShare//啟動並執行任務的數量 val runningTasks1 = s1.runningTasks val runningTasks2 = s2.runningTasks//查看是否有調度隊列處於饑餓狀態,看可分配的核心數是否少於任務數,如果資源不夠用,那麼//處於挨餓狀態 val s1Needy = runningTasks1 < minShare1 val s2Needy = runningTasks2 < minShare2//計算ShareRatio, 最小資源佔用比例,這裡可以理解為偏向任務較輕的 val minShareRatio1 = runningTasks1.toDouble / math.max(minShare1, 1.0).toDouble val minShareRatio2 = runningTasks2.toDouble / math.max(minShare2, 1.0).toDouble//計算Task的Weight比重即權重,任務數相同,權重高的優先 val taskToWeightRatio1 = runningTasks1.toDouble / s1.weight.toDouble val taskToWeightRatio2 = runningTasks2.toDouble / s2.weight.toDouble var compare: Int = 0//首先處於饑餓優先 if (s1Needy && !s2Needy) { return true } else if (!s1Needy && s2Needy) { return false } else if (s1Needy && s2Needy) { //都處於挨餓狀態則,需要資源佔用比小的優先 compare = minShareRatio1.compareTo(minShareRatio2) } else {//都不挨餓,則比較權重比,比例低的優先 compare = taskToWeightRatio1.compareTo(taskToWeightRatio2) } if (compare < 0) { true } else if (compare > 0) { false } else {//如果都一樣,那麼比較名字,按照字母順序比較,所以名字比較重要 s1.name < s2.name } }}
註:
& 公平原則本著的原則就是誰最需要就給誰,所以挨餓者優先;
& 資源佔用比這塊有點費解,如果把他理解成一個貪心問題就容易理解了。對雩都是出於挨餓狀態的任務可以這樣理解,負載大的即時給你資源你也不一定能有效緩解,莫不如給負載小的,讓其快速使用,完成後可以釋放更多的資源,這是一種貪心策略。如JobA和JobB的Task數量相同都是10,A的minShare是2,B的是5,那佔用比為5和2,顯然B的佔用比更小,貪心的策略應該給B先調度處理;
& 對雩都處於滿足狀態的,當然誰的權重有著更好的決定性,權重比低得優先(偏向權利大的);
& 如果所有上述的比較都相同,那麼名字字典排序靠前的優先(哈哈,名字很重要哦);名字aaa要比abc優先,所以這裡在給Pool或者TaskSetManager起名字的時候要考慮這一點
(備忘來源:https://yq.aliyun.com/articles/6041)
補充:這兩種調度的排序演算法針對的可比較對象都是Schedule的具體對象,這裡我們對這個對象Schedulable做簡單的解釋。
前面講到,Schedulable可調度對象在Spark有兩種形式:Pool和TaskSetManager。Pool是一個調度池,Pool裡面還可以有子Pool,Spark中的rootPool即根節點預設是一個無名(default)的Pool。對於FIFO和FAIR有不同的層次。
對於FIFO模式的調度,rootPool管理的直接就是TaskSetManager,沒有子Pool這個概念,就只有兩層,rootPool和葉子節點TaskSetManager,實現如下所示。
但對於FAIR這種模式來說,是三層的,根節點是rootPool,為無名Pool,下一層為使用者定義的Pool(不指定名稱預設名稱為default),再下一層才是TaskSetManager,即根調度池管理一組調度池,每個調度池管理自己的TaskSetManager,其實現如下所示。
這裡的調度順序是指在一個SparkContext之內的調度,一般情況下我們自行使用是不太會需要Pool這個概念的,因為不存在Pool之間的競爭,但如果我們提供一個Spark應用,大家都可以提交任務,服務端有一個常駐的任務,對應一個SparkContext,每個使用者提交的任務都由其代理執行,那麼針對每個使用者提交的任務可以按照使用者等級和任務優先順序設定一個Pool,這樣不同的使用者的Pool之間就存在競爭關係了,可以用Pool的優先順序來區分任務和使用者的優先順序了,**但要再強調一點名字很重要,因為FAIR機制中,如果其他比較無法判斷,那麼會按照名字來進行字典排序的**。(補充來源:https://yq.aliyun.com/articles/6041)
5.建立DAGScheduler,調用TaskScheduler#start方法(SparkContext初始化過程中)
//SparkContext.scala525) _dagScheduler = new DAGScheduler(this)526) _heartbeatReceiver.ask[Boolean](TaskSchedulerIsSet)527)528) // start TaskScheduler after taskScheduler sets DAGScheduler reference in DAGScheduler's529) // constructor530) _taskScheduler.start()
//TaskSchedulerImpl.scalaoverride def start() { //啟動SparkDeploySchedulerBackend backend.start() if (!isLocal && conf.getBoolean("spark.speculation", false)) { logInfo("Starting speculative execution thread") speculationScheduler.scheduleAtFixedRate(new Runnable { override def run(): Unit = Utils.tryOrStopSparkContext(sc) { checkSpeculatableTasks() } }, SPECULATION_INTERVAL_MS, SPECULATION_INTERVAL_MS, TimeUnit.MILLISECONDS) } }
6.啟動Executor
override def start() { super.start() launcherBackend.connect() // The endpoint for executors to talk to us val driverUrl = rpcEnv.uriOf(SparkEnv.driverActorSystemName, RpcAddress(sc.conf.get("spark.driver.host"), sc.conf.get("spark.driver.port").toInt), CoarseGrainedSchedulerBackend.ENDPOINT_NAME) val args = Seq( "--driver-url", driverUrl, "--executor-id", "{{EXECUTOR_ID}}", "--hostname", "{{HOSTNAME}}", "--cores", "{{CORES}}", "--app-id", "{{APP_ID}}", "--worker-url", "{{WORKER_URL}}") val extraJavaOpts = sc.conf.getOption("spark.executor.extraJavaOptions") .map(Utils.splitCommandString).getOrElse(Seq.empty) val classPathEntries = sc.conf.getOption("spark.executor.extraClassPath") .map(_.split(java.io.File.pathSeparator).toSeq).getOrElse(Nil) val libraryPathEntries = sc.conf.getOption("spark.executor.extraLibraryPath")// Start executors with a few necessary configs for registering with the scheduler val sparkJavaOpts = Utils.sparkJavaOpts(conf, SparkConf.isExecutorStartupConf) val javaOpts = sparkJavaOpts ++ extraJavaOpts//定義分配資源的進程名稱 val command = Command("org.apache.spark.executor.CoarseGrainedExecutorBackend", args, sc.executorEnvs, classPathEntries ++ testingClassPath, libraryPathEntries, javaOpts) .map(_.split(java.io.File.pathSeparator).toSeq).getOrElse(Nil)//省略部分代碼,詳細內容參見前幾節,Executor註冊過程。 client = new AppClient(sc.env.rpcEnv, masters, appDesc, this, conf)//註冊應用程式 client.start() launcherBackend.setState(SparkAppHandle.State.SUBMITTED) waitForRegistration() launcherBackend.setState(SparkAppHandle.State.RUNNING) }
在client#start方法中最終會註冊應用程式。
二、總結在SparkContext執行個體化的時候調用createTaskScheduler來建立TaskSchedulerImpl和SparkDeploySchedulerBackend,同時在SparkContext執行個體化的時候會調用TaskSchedulerImpl#start方法,在該方法中會調用SparkDeploySchedulerBackend#start;在這個start方法中會建立AppClient對象並調用AppClient#start方法,這時會建立ClientEndpoint,在建立ClientEndpoint時會傳入來指定具體為當前應用程式啟動的Executor進程的入口類的名稱為CoarseGrainedExecutorBackend,然後ClientEndpoint啟動並通過tryRegisterMaster來註冊當前的應用程式到Master中,Master接收到註冊資訊後,如果可以運行程式,則會為該程式產生JobID並通過Schedule來分配計算資源,具體計算資源分派是通過應用程式運行方式、Memory、core等配置資訊來決定的,最後Master會發送指令給Worker;Worker中為當前應用程式分配計算資源時,首先分配ExecutorRunner,ExecutorRunner內部會通過Thread的方式構建ProcessBuilder來啟動另一個JVM進程,這個JVM進程啟動時候會載入的main方法所在的類的名稱就是建立ClientEndpoint傳入的Command指定的入口類CoarseGrainedExecutorBackend,此時JVM在通過ProcessBuilder啟動的時候獲得了CoarseGrainedExecutorBackend後,載入並調用其中的main方法。在main方法中會執行個體化CoarseGrainedExecutorBackend本身這個訊息迴圈體,而其執行個體化時會通過回調onStart向DriverEndpoint發送RegisterExecutor來註冊當前的CoarseGrainedExecutorBackend,此時DriverEndpoint收到該註冊資訊並儲存在了SparkDeployScheduler執行個體的記憶體資料結構中,這樣Driver就獲得了計算資源!
Task運行各個階段互動過程
圖35-6 資源分派過程
備忘:有關FIFO、FAIR調度演算法解析部分參考 張安站 --Spark技術內幕一書
說明:
本文是由DT大資料夢工廠的IFM課程第35課為基礎所做的筆記
DT大資料夢工廠第三十五課 Spark系統運行迴圈流程