在DAGScheduler中创建好stage后,针对stage的task,创建TaskSet对象,调用TaskScheduler的submitTasks()方法,提交TaskSet,默认情况下我们的standalone模式使用的是TaskSchedulerImpl
taskScheduler.submitTasks(new TaskSet(
tasks.toArray, stage.id, stage.latestInfo.attemptId, jobId, properties))
点击进入submitTasks,首先先执行如下语句
val manager = createTaskSetManager(taskSet, maxTaskFailures)
给每一个TaskSet,都会创建一个TaskSetManager , 它会负责它的那个TaskSet的任务执行状况的监视和管理
stageTaskSets(taskSet.stageAttemptId) = manager
然后加入内存缓存中
backend.reviveOffers()
SparkContext 原理剖析的时候,创建TaskScheduler的时候,一件非常重要的事情,就是为TaskSchedulerImpl创建一个SparkDeploySchedulerBackend,这里的backend就是之前创建好的SparkDeploySchedulerBackend , 而且这个backend是负责创建AppClient,向Master注册application的,随后点击进入reviveOffers
override def reviveOffers() {
driverEndpoint.send(ReviveOffers)
}
case ReviveOffers =>
makeOffers()
private def makeOffers() {
// Filter out executors under killing
// 第一步 调用TaskSchedulerImpl的resourceOffers()方法,执行任务分配算法,将各个task分配到executor上去
// 第二步 分配好task到executor之后,执行自己的launchTask,将分配的task发送LauncherTask消息
// 到对应的executor上去,有executor启动并执行task
// resourceOffers分配算法
val activeExecutors = executorDataMap.filterKeys(executorIsAlive)
val workOffers = activeExecutors.map { case (id, executorData) =>
new WorkerOffer(id, executorData.executorHost, executorData.freeCores)
}.toSeq
launchTasks(scheduler.resourceOffers(workOffers))
}
其中比较重要的是resourceOffers,它是task的分配算法,点击进入其中
def resourceOffers(offers: Seq[WorkerOffer]): Seq[Seq[TaskDescription]] = synchronized {
// Mark each slave as alive and remember its hostname
// Also track if new executor is added
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
}
}
// Randomly shuffle offers to avoid always placing tasks on the same set of workers.
// 将可用的executor进行shuffle,也就是说,进行打散,尽量可以进行负责均衡
val shuffledOffers = Random.shuffle(offers)
// Build a list of tasks to assign to each worker.
// 然后针对WorkOffer,创建一堆需要用的东西
// 比如Tasks,可以理解为一个二维数组,ArrayBuffer,元素又是一个ArrayBuffer
// 并且每个子ArrayBuffer的数量是固定的,也就是这个executor可用的cpu数量
val tasks = shuffledOffers.map(o => new ArrayBuffer[TaskDescription](o.cores))
val availableCpus = shuffledOffers.map(o => o.cores).toArray
// 从roolPool中取出了排序的TaskSet
// 之前讲解TaskScheduler初始化的时候,我们知道,创建完TaskSchedulerImpl、SparkDeploySchedulerBackend之后
// 执行一个initialize() 方法,在这个方法中,其实会创建一个调度池
// 这里相当于说,所有提交的TaskSet,首先,会放入调度池
// 然后再执行task分配算法的时候,会从这个调度池中取出拍好队的TaskSet
val sortedTaskSets = rootPool.getSortedTaskSetQueue
for (taskSet <- sortedTaskSets) {
logDebug("parentName: %s, name: %s, runningTasks: %s".format(
taskSet.parent.name, taskSet.name, taskSet.runningTasks))
if (newExecAvail) {
taskSet.executorAdded()
}
}
// Take each TaskSet in our scheduling order, and then offer it each node in increasing order
// of locality levels so that it gets a chance to launch local tasks on all of them.
// NOTE: the preferredLocality order: PROCESS_LOCAL, NODE_LOCAL, NO_PREF, RACK_LOCAL, ANY
// 任务分配算法的核心
// 双重for循环,遍历所有的taskset,以及每一种本地化级别
// 本地化级别:
// PROCESS_LOCAL -> 进程本地化,rdd的partition和task进入一个executor内,那么速度当然快
// NODE_LOCAL -> rdd的partition和task,不在一个executor中,不在一个进程中,但是在一个worker节点上
// NO_PREF -> 无,没有所谓的本地化级别
// ROCK_LOCAL -> 机架本地化,至少rdd的partition和task在同一个机架上
// ANY -> 任意的本地化级别
// 对每个taskset,从最好的一种本地化级别遍历
//
var launchedTask = false
for (taskSet <- sortedTaskSets; maxLocality <- taskSet.myLocalityLevels) {
do {
// 对当前taskset
// 尝试优先使用最小的本地化级别,将taskset的task,在executor上进行启动
// 如果启动不了,那么就跳出这个do while循环,进入下一种本地化级别,也就是放大本地化级别
// 以此类推,知道尝试将taskset在某些本地化级别,让task在executor上全部启动
launchedTask = resourceOfferSingleTaskSet(
taskSet, maxLocality, shuffledOffers, availableCpus, tasks)
} while (launchedTask)
}
if (tasks.size > 0) {
hasLaunchedTask = true
}
return tasks
}