前面已經分析到了 DAGScheduler 對 stage 劃分,並對 Task 的最佳位置進行計算以後,經過調用 taskScheduler 的 submitTasks 方法,將每一個 stage 的 taskSet 進行提交。java
在 taskScheduler 的 submitTasks 方法中會爲每一個 taskSet 建立一個 TaskSetManager,用於管理 taskSet。而後向調度池中添加該 TaskSetManager,最後會調用 backend.reviveOffers() 方法爲 task 分配資源。app
override def submitTasks(taskSet: TaskSet) {
//獲取 taskSet 中的 task
val tasks = taskSet.tasks logInfo("Adding task set " + taskSet.id + " with " + tasks.length + " tasks") this.synchronized {
// 爲每一個 taskSet 建立一個 TaskSetManager
val manager = createTaskSetManager(taskSet, maxTaskFailures)
// 拿到 stage 的 id
val stage = taskSet.stageId
// 建立一個 HashMap ,用來存儲 stage 對應的 TaskSetManager
val stageTaskSets =
taskSetsByStageIdAndAttempt.getOrElseUpdate(stage, new HashMap[Int, TaskSetManager])
// 將上面建立的 taskSetManager 存入 map 中
stageTaskSets(taskSet.stageAttemptId) = manager
val conflictingTaskSet = stageTaskSets.exists { case (_, ts) =>
ts.taskSet != taskSet && !ts.isZombie
}
if (conflictingTaskSet) {
throw new IllegalStateException(s"more than one active taskSet for stage $stage:" +
s" ${stageTaskSets.toSeq.map{_._2.taskSet.id}.mkString(",")}")
}
// 向調度池中添加剛纔建立的 TaskSetManager
schedulableBuilder.addTaskSetManager(manager, manager.taskSet.properties)
// 判斷程序是否爲 local 模式,而且 TaskSchedulerImpl 沒有收到 Task
if (!isLocal && !hasReceivedTask) {
// 建立一個定時器,經過指定時間檢查 TaskSchedulerImpl 的飢餓狀況
starvationTimer.scheduleAtFixedRate(new TimerTask() {
override def run() {
// 若是 TaskSchedulerImpl 已經安排執行了 Task,則取消定時器
if (!hasLaunchedTask) {
logWarning("Initial job has not accepted any resources; " +
"check your cluster UI to ensure that workers are registered " +
"and have sufficient resources")
} else {
this.cancel()
}
}
}, STARVATION_TIMEOUT_MS, STARVATION_TIMEOUT_MS)
}
// 標記已經接收到 Task
hasReceivedTask = true
}
// 給 Task 分配資源
backend.reviveOffers()
}
複製代碼
下面主要看 backend.reviveOffers() 這個方法,在提交模式是 standalone 模式下,其實是調用 StandaloneSchedulerBackend 的 reviveOffers 方法,實則調用的是其父類 CoarseGrainedSchedulerBackend 的 reviveOffers 方法,這個方法是向 driverEndpoint 發送一個 ReviveOffers 消息。ide
override def reviveOffers() {
// 向 driverEndpoint 發送 ReviveOffers 消息
driverEndpoint.send(ReviveOffers)
}
複製代碼
DriverEndpoint 收到信息後會調用 makeOffers 方法:ui
case ReviveOffers =>
makeOffers()
複製代碼
makeOffers 方法內部會將 application 全部可用的 executor 封裝成一個 workOffers,每一個 workOffers 內部封裝了每一個 executor 的資源數量。this
而後調用 taskScheduler 的 resourceOffers 從上面封裝的 workOffers 信息爲每一個 task 分配合適的 executor。spa
最後調用 launchTasks 啓動 task。線程
private def makeOffers() {
// 過濾出可用的 executor
val activeExecutors = executorDataMap.filterKeys(executorIsAlive)
// 將這些 executor 封裝成 workOffers
val workOffers = activeExecutors.map { case (id, executorData) =>
new WorkerOffer(id, executorData.executorHost, executorData.freeCores)
}.toIndexedSeq
// 給每一個 task 分配 executor,而後調用 launchTasks 啓動這些 task
launchTasks(scheduler.resourceOffers(workOffers))
}
複製代碼
下面看一下 launchTasks 這個方法。日誌
這個方法主要作了這些操做:code
1,遍歷每一個 task,而後將每一個 task 信息序列化。orm
2,判斷序列化後的 task 信息,若是大於 rpc 發送消息的最大值,則中止,建議調整 rpc 的 maxRpcMessageSize,若是小於 rpc 發送消息的最大值,則找到 task 對應的 executor,而後更新該 executor 對應的一些內存資源信息。
3,向 executor 發送 LaunchTask 消息。
private def launchTasks(tasks: Seq[Seq[TaskDescription]]) {
// 遍歷全部的 task
for (task <- tasks.flatten) {
// 序列化 task 信息
val serializedTask = ser.serialize(task)
// 判斷序列化後的 task 信息是否大於 rpc 可以傳送的最大信息量
if (serializedTask.limit >= maxRpcMessageSize) {
scheduler.taskIdToTaskSetManager.get(task.taskId).foreach { taskSetMgr =>
try {
var msg = "Serialized task %s:%d was %d bytes, which exceeds max allowed: " +
"spark.rpc.message.maxSize (%d bytes). Consider increasing " +
"spark.rpc.message.maxSize or using broadcast variables for large values."
msg = msg.format(task.taskId, task.index, serializedTask.limit, maxRpcMessageSize)
taskSetMgr.abort(msg)
} catch {
case e: Exception => logError("Exception in error callback", e)
}
}
}
else {
// 找到對應的 executor
val executorData = executorDataMap(task.executorId)
// 更新 executor 的資源信息
executorData.freeCores -= scheduler.CPUS_PER_TASK logDebug(s"Launching task ${task.taskId} on executor id: ${task.executorId} hostname: " + s"${executorData.executorHost}.") // 向 executor 發送 LaunchTask 消息 executorData.executorEndpoint.send(LaunchTask(new SerializableBuffer(serializedTask))) } } } 複製代碼
下面就看 Executor 收到消息後作了哪些操做,這裏 executorData.executorEndpoint 實際上就是在建立 Executor 守護進程時候建立的那個 CoarseGrainedExecutorBackend。
因此找到 CoarseGrainedExecutorBackend 處理接收到 LaunchTask 消息後作了哪些操做。
首先他會判斷當前的 executor 是否是爲空,若是不爲空就會反序列化 task 的信息,而後調用 executor 的 launchTask 方法。
case LaunchTask(data) =>
// 判斷當前 executor 是否是空
if (executor == null) {
exitExecutor(1, "Received LaunchTask command but executor was null")
} else {
// 反序列化 task 的信息
val taskDesc = ser.deserialize[TaskDescription](data.value)
logInfo("Got assigned task " + taskDesc.taskId)
// 調用 executor 的 lauchTask 方法
executor.launchTask(this, taskId = taskDesc.taskId, attemptNumber = taskDesc.attemptNumber,
taskDesc.name, taskDesc.serializedTask)
}
複製代碼
下面就看下 executor 的 launchTask 作了哪些操做。
首先 executor 會爲每一個 task 建立一個 TaskRunner,而後會會將 task 添加到 runningTasks 的集合中,並標記其爲運行狀態,最後將 taskRunner 放到一個線程池中執行。
def launchTask( context: ExecutorBackend, taskId: Long, attemptNumber: Int, taskName: String, serializedTask: ByteBuffer): Unit = {
// 建立 TaskRunner
val tr = new TaskRunner(context, taskId = taskId, attemptNumber = attemptNumber, taskName,
serializedTask)
runningTasks.put(taskId, tr)
// 將 taskRunner 放到線程池中執行
threadPool.execute(tr)
}
複製代碼
在 taskRunner 的 run 方法中回去執行每一個 task,並會輸出一系列的日誌。task 運行完成後迴向 driver 發送消息,driver 會更新 executor 的一些資源數據,並標記 task 已完成。
至此 task 啓動完成。