Spark — Master資源調度機制 schedule()分析

Driver向Master進行Application註冊的時候,Master註冊完之後,會調用schedule()方法,進行資源調度。下面我們對schedule()源碼進行分析,schedule()源碼如下:

 private def schedule(): Unit = {
    // 首先判斷master狀態不是alive的話,直接返回,也就是說standby是不會進行資源調度的
    if (state != RecoveryState.ALIVE) { return }
    // Drivers take strict precedence over executors
    // Random.shuffle的原理主要是遍歷整個ArrayBuffer,隨機交換從後往前的兩個位置的數
    // 對傳入集合中的元素進行隨機打亂
    val shuffledWorkers = Random.shuffle(workers) // Randomization helps balance drivers

    // 取出worker中之前所有註冊上來的worker,進行過濾,worker必須是alive狀態
    for (worker <- shuffledWorkers if worker.state == WorkerState.ALIVE) {
      // 首先調度Driver。
      // 爲什麼要調度Driver,什麼情況下會註冊Driver,調度Driver?
      // 其實只有用yarn-cluster模式提交的時候,纔會註冊Driver,因爲Standalone和yarn-client模式
      // 都直接在本地啓動Driver,不會註冊Driver,更不會讓master調度Driver了
      // 遍歷等待調度的Driver
      for (driver <- waitingDrivers) {
        // 如果當前worker的空閒內存量,大於等於Driver需要的內存,
        // 並且worker的空閒cpu core,大於Driver所需的cpu數量
        // Driver是在Worker上啓動的。。因此Worker節點的內存和CPU需要能夠讓Driver運行
        if (worker.memoryFree >= driver.desc.mem && worker.coresFree >= driver.desc.cores) {
          // 啓動Driver
          launchDriver(worker, driver)
          // 從緩存中移除
          waitingDrivers -= driver
        }
      }
    }
    // 對worker的調度機制,在worker上啓動executor
    startExecutorsOnWorkers()
  }

從上述源碼中可以看出,首先對所有已註冊的worker進行隨機打散;接着進行遍歷,去除不是alive狀態的worker,首先對Driver進行調度,爲什麼一開始要調度Driver?

因爲只有在yarn-cluster模式下才需要調度Driver,在這個模式下,YARN需要找一個NodeManager來啓動Driver,因此需要在已註冊的worker節點集合中尋找滿足條件的worker,來啓動Driver。由於standalone和yarn-client都是在本地啓動Driver,所以無需進行調度。

調度完Driver之後,就正式開始進行executor的調度了,調用了方法startExecutorsOnWorkers(),源碼如下:

private def startExecutorsOnWorkers(): Unit = {
    // Right now this is a very simple FIFO scheduler. We keep trying to fit in the first app
    // in the queue, then the second app, etc.
    // Application的調度機制,默認採用的是spreadOutApps調度算法

    // 首先遍歷waitingApps中的appInfo,並且過濾出,還需要調度的core的app,
    // 說白了,就是處理app需要的cpu core
    for (app <- waitingApps if app.coresLeft > 0) {
      // 這是腳本文件中的 --executor-cores 這個參數
      val coresPerExecutor: Option[Int] = app.desc.coresPerExecutor
      // Filter out workers that don't have enough resources to launch an executor
      // 過濾掉沒有足夠資源啓動的worker
      // 從worker中過濾出狀態爲alive的worker,並且這個worker的資源能夠被Application使用
      // 然後按照剩餘cpu數量倒敘排序,從大到小排序
      val usableWorkers = workers.toArray.filter(_.state == WorkerState.ALIVE)
        .filter(worker => worker.memoryFree >= app.desc.memoryPerExecutorMB &&
          worker.coresFree >= coresPerExecutor.getOrElse(1))
        .sortBy(_.coresFree).reverse

      // cpu core 和 memory 資源分配
      val assignedCores = scheduleExecutorsOnWorkers(app, usableWorkers, spreadOutApps)

      // Now that we've decided how many cores to allocate on each worker, let's allocate them
      // 給每個worker分配完資源給application之後
      // 遍歷每個worker節點
      for (pos <- 0 until usableWorkers.length if assignedCores(pos) > 0) {
        // 啓動executor
        allocateWorkerResourceToExecutors(
          app, assignedCores(pos), coresPerExecutor, usableWorkers(pos))
      }
    }
  }

這裏的調度機制默認是採用FIFO的調度算法,這個算法之前有介紹過,這裏不做闡述;下面進行源碼分析。

首先在等待調度隊列的App中取出一個Application,並且過濾出還需要調度core的App(還沒有調度完的App);

接着讀取App中的coresPerExecutors參數,代表了每個executor被分配的多少個cpu core(spark-submit腳本中可設置的參數 --executor-cores);

然後下面這句話比較複雜,它的意思是,從worker中過濾出狀態爲alive的worker,然後對這些worker再過濾出它們的內存和cpu core能夠啓動Application的worker,也就是對存活着的worker中過濾出有足夠資源去啓動Application的worker,並按照cpu core的大小降序排序,下一步使用scheduleExecutorsOnWorkers()方法,給每個worker分配資源,最後使用allocateWorkerResourceToExecutors()啓動executor。

我們下面就看看,worker節點怎麼被分配executor,scheduleExecutorsOnWorkers()源碼如下:

 private def scheduleExecutorsOnWorkers(
      app: ApplicationInfo,
      usableWorkers: Array[WorkerInfo],
      spreadOutApps: Boolean): Array[Int] = {
    // --executor-cores app中每個executor被分配的cores
    val coresPerExecutor = app.desc.coresPerExecutor
    // 每個worker最少被分配的cpu core,默認就是coresPerExecutor
    val minCoresPerExecutor = coresPerExecutor.getOrElse(1)
    // 如果沒有設置--executor-cores 參數的話,就默認分配一個executor
    val oneExecutorPerWorker = coresPerExecutor.isEmpty
    // --executor-memory 每個executor要被分配的內存大小
    val memoryPerExecutor = app.desc.memoryPerExecutorMB
    // 可用worker的個數
    val numUsable = usableWorkers.length
    // 創建一個空數組,存儲了要分配給每個worker的cpu core數量
    val assignedCores = new Array[Int](numUsable) // Number of cores to give to each worker
    // 每個worker上分配幾個executor
    val assignedExecutors = new Array[Int](numUsable) // Number of new executors on each worker
    // 獲取需要分配的core的數量,取app剩餘還需要分配的cpu core數量 和 worker總共可用CPU core數量的最小值
    // 如果worker資源總數不夠,那麼只能先分配這麼多cpu core
    // app.coresLeft = requestedCores - coresGranted,
    // 其中requestedCores代表app需要分配多少個cpu core,coresGranted代表當前集羣worker節點已經被分配了多少個core
    var coresToAssign = math.min(app.coresLeft, usableWorkers.map(_.coresFree).sum)

    /** Return whether the specified worker can launch an executor for this app. */
    // 判斷當前worker剩餘 cpu core 和 memory是否能夠分配給app運行
    def canLaunchExecutor(pos: Int): Boolean = {
      // 只要當前剩餘的cpu core,還沒有被分配完,這個標誌位就是true
      val keepScheduling = coresToAssign >= minCoresPerExecutor
      // 當前worker剩餘的core是否能夠分配給App
      val enoughCores = usableWorkers(pos).coresFree - assignedCores(pos) >= minCoresPerExecutor

      // If we allow multiple executors per worker, then we can always launch new executors.
      // Otherwise, if there is already an executor on this worker, just give it more cores.
      // 每個worker資源足夠的情況下,可以啓動多個executor,
      // 否則的話,就給當前啓動的這個worker足夠的core和memory
      val launchingNewExecutor = !oneExecutorPerWorker || assignedExecutors(pos) == 0
      if (launchingNewExecutor) {
        // 當前節點已分配出去的內存
        val assignedMemory = assignedExecutors(pos) * memoryPerExecutor
        // 剩餘內存是否足夠分配
        val enoughMemory = usableWorkers(pos).memoryFree - assignedMemory >= memoryPerExecutor
        val underLimit = assignedExecutors.sum + app.executors.size < app.executorLimit
        keepScheduling && enoughCores && enoughMemory && underLimit
      } else {
        // 在一個worker持續分配資源
        keepScheduling && enoughCores
      }
    }
    
    // 過濾掉資源不夠的worker
    var freeWorkers = (0 until numUsable).filter(canLaunchExecutor)
    while (freeWorkers.nonEmpty) {
      freeWorkers.foreach { pos =>
        var keepScheduling = true
        while (keepScheduling && canLaunchExecutor(pos)) {
          // 當前worker內存和core資源足夠分配給一個app
          // 可用worker可用core減去已分配的core
          coresToAssign -= minCoresPerExecutor
          // pos節點worker已被分配出去多少core
          assignedCores(pos) += minCoresPerExecutor

          // If we are launching one executor per worker, then every iteration assigns 1 core
          // to the executor. Otherwise, every iteration assigns cores to a new executor.
          if (oneExecutorPerWorker) {
            // 一個worker點一個executor
            assignedExecutors(pos) = 1
          } else {
            // 資源足夠的情況下,一個worker可以分配多個executor
            assignedExecutors(pos) += 1
          }

          // Spreading out an application means spreading out its executors across as
          // many workers as possible. If we are not spreading out, then we should keep
          // scheduling executors on this worker until we use all of its resources.
          // Otherwise, just move on to the next worker.
          // 如果是spreadOutApps模式下,那麼就每個worker在分配executor後,接着就到下一個worker上分配
          // 循環分配,保證每個可用worker都可以分配到executor
          // 如果不是,那麼就給當前這個executor一直分配core,直到這個executor所在的節點資源
          // 已經都分配完了。
          if (spreadOutApps) {
            keepScheduling = false
          }
        }
      }
      // 過濾掉資源不足的worker
      freeWorkers = freeWorkers.filter(canLaunchExecutor)
    }
    assignedCores
  }

從上面代碼中可以看出,master的資源調度算法主要有兩個:一個是SpreadOut算法,另一個是非SpreadOut算法,它兩的區別從源碼中就可以看出來,SpreadOut算法,是將executor儘可能的分配到較多的worker節點上,這樣做的好處是,每個節點都能工作,防止資源浪費,而第二種就是將executor儘可能少的分配到worker,直到這個worker資源不足,纔到下一個worker上分配資源。這裏注意,這個算法相對老版本的算法做了優化,老版本中比如(1.3,我之前研究的版本),分配core的單位是1個,而這裏則是按照我們spark-submit腳本中配置的--executor-cores,爲單位進行分配,這裏要注意。

下面舉個例子,

假設Application需要 4個executor,每個executor2個cpu core,假如現在有4個worker,每個worker 4個cpu core,那麼SpreadOut算法就會讓4個worker都啓動一個executor,每個executor2個cpu core,而非SpreadOut算法,則啓動2個worker,每個worker啓動一個executor,每個executor 4個cpu core。有兩個worker節點的資源就浪費了。

綜上所述,master資源調度算法主要是兩種:一種是SpreadOut,一種是非SpreadOut算法,兩個方法的區別主要一個可以合理利用各個worker上的資源,一個是最大限度的每個使用worker上的資源,有可能造成浪費。

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