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上的資源,有可能造成浪費。