Task分配算法
接着上一篇的Task最佳位置,我们分析了submitMissingTasks()方法,其中里面比较重要的:一个是task的最佳位置计算,另一个就是提交TaskSet给TaskScheduler。下面分析提交到TaskScheduler后的TaskSet中的task是如何被分配到Executor上去的。
默认情况下,standalone模式,是使用的TaskSchedulerImpl,TaskScheduler只是一个trait,到TaskSchedulerImpl中找到submitTasks()方法,源码如下:
override def submitTasks(taskSet: TaskSet) {
val tasks = taskSet.tasks
logInfo("Adding task set " + taskSet.id + " with " + tasks.length + " tasks")
this.synchronized {
// 为TaskSet创建TaskSetManager,它会负责它的那个TaskSet的任务执行状况的监视和管理
// TaskManager会负责追踪它所管理的那个TaskSet,如果task失败,它也会重试task等等
val manager = createTaskSetManager(taskSet, maxTaskFailures)
// 对TaskSet的信息进行提取和封装
val stage = taskSet.stageId
val stageTaskSets =
taskSetsByStageIdAndAttempt.getOrElseUpdate(stage, new HashMap[Int, TaskSetManager])
stageTaskSets(taskSet.stageAttemptId) = manager
val conflictingTaskSet = stageTaskSets.exists { case (_, ts) =>
ts.taskSet != taskSet && !ts.isZombie
}
// 将TaskSetManager放入调度池中,这是之前初始化的时候创建的调度池,默认是FIFO
// 这里将TaskSet放入调度池,会对Task进行排序。
schedulableBuilder.addTaskSetManager(manager, manager.taskSet.properties)
// 省略部分代码
....
}
// 调用SparkDeploySchedulerBackend的reviveOffers,而SparkDeploySchedulerBackend
// 又继承自CoarseGrainedSchedulerBackend。
backend.reviveOffers()
}
CoarseGrainedSchedulerBackend的reviveOffers又会被DriverEndPoint发送ReviveOffers消息,而这个消息里面调用了makeOffers()方法,下面分析这个方法:
private def makeOffers() {
// 过滤掉被kill的executor
val activeExecutors = executorDataMap.filterKeys(executorIsAlive)
// 将Application所有可用的executor,将其封装成WorkerOffer,每个WorkerOffer
// 代表了每个executor可用cpu资源数量
val workOffers = activeExecutors.map { case (id, executorData) =>
new WorkerOffer(id, executorData.executorHost, executorData.freeCores)
}.toSeq
// 调用resourceOffers()方法,执行任务分配算法,将各个task分配到executor上去
// 将分配好task到executor之后,执行launchTasks()方法,将分配的task发送LaunchTask消息
// 到对应的executor上去,由executor启动并启动task
launchTasks(scheduler.resourceOffers(workOffers))
}
首先将注册的executor的可用资源封装为workerOffers,接着执行TaskShcedulerImpl的resourceOffers方法,执行任务分配算法,将各个task分配到executor上去,最后执行自己的launchTasks()方法,将分配的task发送LaunckTask消息到对应的executor上去,由executor启动并执行。
首先看TaskShcedulerImpl的resourceOffers()方法。
def resourceOffers(offers: Seq[WorkerOffer]): Seq[Seq[TaskDescription]] = synchronized {
// 缓存每个executor的信息,查看是否有新的executor产生,也一并写入缓存中
var newExecAvail = false
for (o <- offers) {
executorIdToHost(o.executorId) = o.host
executorIdToTaskCount.getOrElseUpdate(o.executorId, 0)
if (!executorsByHost.contains(o.host)) {
executorsByHost(o.host) = new HashSet[String]()
executorAdded(o.executorId, o.host)
newExecAvail = true
}
for (rack <- getRackForHost(o.host)) {
hostsByRack.getOrElseUpdate(rack, new HashSet[String]()) += o.host
}
}
// 首先将可用的executor进行shuffle,进行打散,尽量进行负载均衡
val shuffledOffers = Random.shuffle(offers)
// Build a list of tasks to assign to each worker.
// 针对Worker创建出所需的组件
// 创建一个tasks列表,它是一个二维数组,其中一维是TaskDescription,
// 它对应的子ArrayBuffer是这个task对应的executor可用的cpu数量
val tasks = shuffledOffers.map(o => new ArrayBuffer[TaskDescription](o.cores))
// 每个worker可用cpu数量
val availableCpus = shuffledOffers.map(o => o.cores).toArray
// 从rootPool中,取出了排序的TaskSet,
// 刚开始创建的TaskSetManager被放入调度池中,会对提交上来的task进行排序
val sortedTaskSets = rootPool.getSortedTaskSetQueue
for (taskSet <- sortedTaskSets) {
logDebug("parentName: %s, name: %s, runningTasks: %s".format(
taskSet.parent.name, taskSet.name, taskSet.runningTasks))
if (newExecAvail) {
// 计算task的本地化级别 -- 这里是计算的新加入executor的taskset的本地化级别
taskSet.executorAdded()
}
}
// 这里就是任务分配算法的核心了
// 双重for循环,遍历所有的TaskSet,以及每一种本地化级别
var launchedTask = false
// 对每个taskset,从最好的一种本地化级别开始遍历
for (taskSet <- sortedTaskSets; maxLocality <- taskSet.myLocalityLevels) {
do {
// 对当前taskset,尝试优先使用最小的本地化级别,将taskset的task,在executor上进行启动
// 如果启动不了,那么就跳出这个循环,进入下一个本地化级别,也就是放大本地化级别
// 依次类推,直到尝试将TaskSet在某些本地化级别下,让task在executor上全部启动
launchedTask = resourceOfferSingleTaskSet(
taskSet, maxLocality, shuffledOffers, availableCpus, tasks)
} while (launchedTask)
}
if (tasks.size > 0) {
hasLaunchedTask = true
}
return tasks
}
上面的核心就是那个双重for循环,它就是任务分配算法的核心。首先介绍一下本地化级别有哪些,一共有5种:
PROCESS_LOCAL:进程本地化,RDD的partition和task进入到同一个executor中,速度快。
NODE_LOCAL:节点本地化,RDD的partition和task,不在一个executor中,但在同一个worker节点上。
NO_PREF:无本地化,计算数据在关系型数据库中,所以无论哪个节点都可以。
RACK_LOCAL:机架本地化,RDD的partition和task在同一个机架上,不同worker上。
ANY:任意本地化级别,就是在集群的任何一个节点上都可以,这是最高级别的,在其他级别都不可行的时候,会使用这个。
这些本地化级别,从好到坏,从小到大,越往前的本地化级别就越好。上面的双重for循环,就是对每个TaskSet,从最好的一种本地化级别开始遍历;对当前的TaskSet优先使用最好的本地化级别,将TaskSet中的task,在executor上进行启动;如果启动不了,跳出这个循环,进入下一个本地化级别,也就是放大本地化级别,以此类推,直到尝试将TaskSet在某些本地化级别下,让task在executor上启动。
实现上述功能的方法就是resourceOfferSingleTaskSet(),它会去找在这个executor上,使用某个本地化级别,taskset的哪些task可以启动。里面调用了TaskSetManager的resourceOffer()方法。这个方法去判断这个本地化级别的task能不能再这个executor上启动,它通过判断这个executor在这个本地化级别的等待时间是多少,如果在一定范围内,那么就认为这个本地化级别的task可以在这个executor上启动。