话说在《Spark源码分析之五:Task调度(一)》一文中,我们对Task调度分析到了DriverEndpoint的makeOffers()方法。这个方法针对接收到的ReviveOffers事件进行处理。代码如下:
// Make fake resource offers on all executors
// 在所有的executors上提供假的资源(抽象的资源,也就是资源的对象信息,我是这么理解的)
private def makeOffers() {
// Filter out executors under killing
// 过滤掉under killing的executors
val activeExecutors = executorDataMap.filterKeys(executorIsAlive)
// 利用activeExecutors中executorData的executorHost、freeCores,构造workOffers,即资源
val workOffers = activeExecutors.map { case (id, executorData) =>
// 创建WorkerOffer对象
new WorkerOffer(id, executorData.executorHost, executorData.freeCores)
}.toSeq
// 调用scheduler的resourceOffers()方法,分配资源,并调用launchTasks()方法,启动tasks
// 这个scheduler就是TaskSchedulerImpl
launchTasks(scheduler.resourceOffers(workOffers))
}
代码逻辑很简单,一共分为三步:
第一,从executorDataMap中过滤掉under killing的executors,得到activeExecutors;
第二,利用activeExecutors中executorData的executorHost、freeCores,获取workOffers,即资源;
第三,调用scheduler的resourceOffers()方法,分配资源,并调用launchTasks()方法,启动tasks:这个scheduler就是TaskSchedulerImpl。
我们逐个进行分析,首先看看这个executorDataMap,其定义如下:
private val executorDataMap = new HashMap[String, ExecutorData]
它是CoarseGrainedSchedulerBackend掌握的集群中executor的数据集合,key为String类型的executorId,value为ExecutorData类型的executor详细信息。ExecutorData包含的主要内容如下:
1、executorEndpoint:RpcEndpointRef类型,RPC终端的引用,用于数据通信;
2、executorAddress:RpcAddress类型,RPC地址,用于数据通信;
3、executorHost:String类型,executor的主机;
4、freeCores:Int类型,可用处理器cores;
5、totalCores:Int类型,处理器cores总数;
6、logUrlMap:Map[String, String]类型,日志url映射集合。
这样,通过executorDataMap这个集合我们就能知道集群当前executor的负载情况,方便资源分析并调度任务。那么executorDataMap内的数据是何时及如何更新的呢?go on,继续分析。
对于第一步中,过滤掉under killing的executors,其实现是对executorDataMap中的所有executor调用executorIsAlive()方法中,判断是否在executorsPendingToRemove和executorsPendingLossReason两个数据结构中,这两个数据结构中的executors,都是即将移除或者已丢失的executor。
第二步,在过滤掉已失效或者马上要失效的executor后,利用activeExecutors中executorData的executorHost、freeCores,构造workOffers,即资源,这个workOffers更简单,是一个WorkerOffer对象,它代表了系统的可利用资源。WorkerOffer代码如下:
/**
* Represents free resources available on an executor.
*/
private[spark]
case class WorkerOffer(executorId: String, host: String, cores: Int)
而最重要的第三步,先是调用scheduler.resourceOffers(workOffers),即TaskSchedulerImpl的resourceOffers()方法,然后再调用launchTasks()方法将tasks加载到executor上去执行。
我们先看下TaskSchedulerImpl的resourceOffers()方法。代码如下:
/**
* Called by cluster manager to offer resources on slaves. We respond by asking our active task
* sets for tasks in order of priority. We fill each node with tasks in a round-robin manner so
* that tasks are balanced across the cluster.
*
* 被集群manager调用以提供slaves上的资源。我们通过按照优先顺序询问活动task集中的task来回应。
* 我们通过循环的方式将task调度到每个节点上以便tasks在集群中可以保持大致的均衡。
*/
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
// 标记每个slave节点为alive活跃的,并且记住它的主机名
// 同时也追踪是否有executor被加入
var newExecAvail = false
// 循环offers,WorkerOffer为包含executorId、host、cores的结构体,代表集群中的可用executor资源
for (o <- offers) {
// 利用HashMap存储executorId->host映射的集合
executorIdToHost(o.executorId) = o.host
// Number of tasks running on each executor
// 每个executor上运行的task的数目,这里如果之前没有的话,初始化为0
executorIdToTaskCount.getOrElseUpdate(o.executorId, 0)
// 每个host上executors的集合
// 这个executorsByHost被用来计算host活跃性,反过来我们用它来决定在给定的主机上何时实现数据本地性
if (!executorsByHost.contains(o.host)) {// 如果executorsByHost中不存在对应的host
// executorsByHost中添加一条记录,key为host,value为new HashSet[String]()
executorsByHost(o.host) = new HashSet[String]()
// 发送一个ExecutorAdded事件,并由DAGScheduler的handleExecutorAdded()方法处理
// eventProcessLoop.post(ExecutorAdded(execId, host))
// 调用DAGScheduler的executorAdded()方法处理
executorAdded(o.executorId, o.host)
// 新的slave加入时,标志位newExecAvail设置为true
newExecAvail = true
}
// 更新hostsByRack
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.
// 随机shuffle offers以避免总是把任务放在同一组workers上执行
val shuffledOffers = Random.shuffle(offers)
// Build a list of tasks to assign to each worker.
// 构造一个task列表,以分配到每个worker
val tasks = shuffledOffers.map(o => new ArrayBuffer[TaskDescription](o.cores))
// 可以使用的cpu资源
val availableCpus = shuffledOffers.map(o => o.cores).toArray
// 获得排序好的task集合
// 先调用Pool.getSortedTaskSetQueue()方法
// 还记得这个Pool吗,就是调度器中的调度池啊
val sortedTaskSets = rootPool.getSortedTaskSetQueue
// 循环每个taskSet
for (taskSet <- sortedTaskSets) {
// 记录日志
logDebug("parentName: %s, name: %s, runningTasks: %s".format(
taskSet.parent.name, taskSet.name, taskSet.runningTasks))
// 如果存在新的活跃的executor(新的slave节点被添加时)
if (newExecAvail) {
// 调用executorAdded()方法
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
var launchedTask = false
// 按照位置本地性规则调度每个TaskSet,最大化实现任务的本地性
// 位置本地性规则的顺序是:PROCESS_LOCAL(同进程)、NODE_LOCAL(同节点)、NO_PREF、RACK_LOCAL(同机架)、ANY(任何)
for (taskSet <- sortedTaskSets; maxLocality <- taskSet.myLocalityLevels) {
do {
// 调用resourceOfferSingleTaskSet()方法进行任务集调度
launchedTask = resourceOfferSingleTaskSet(
taskSet, maxLocality, shuffledOffers, availableCpus, tasks)
} while (launchedTask)
}
// 设置标志位hasLaunchedTask
if (tasks.size > 0) {
hasLaunchedTask = true
}
return tasks
}
首先来看下它的主体流程。如下:
1、设置标志位newExecAvail为false,这个标志位是在新的slave被添加时被设置的一个标志,下面在计算任务的本地性规则时会用到;
2、循环offers,WorkerOffer为包含executorId、host、cores的结构体,代表集群中的可用executor资源:
2.1、更新executorIdToHost,executorIdToHost为利用HashMap存储executorId->host映射的集合;
2.2、更新executorIdToTaskCount,executorIdToTaskCount为每个executor上运行的task的数目集合,这里如果之前没有的话,初始化为0;
2.3、如果新的slave加入:
2.3.1、executorsByHost中添加一条记录,key为host,value为new HashSet[String]();
2.3.2、发送一个ExecutorAdded事件,并由DAGScheduler的handleExecutorAdded()方法处理;
2.3.3、新的slave加入时,标志位newExecAvail设置为true;
2.4、更新hostsByRack;
3、随机shuffle offers(集群中可用executor资源)以避免总是把任务放在同一组workers上执行;
4、构造一个task列表,以分配到每个worker,针对每个executor按照其上的cores数目构造一个cores数目大小的ArrayBuffer,实现最大程度并行化;
5、获取可以使用的cpu资源availableCpus;
6、调用Pool.getSortedTaskSetQueue()方法获得排序好的ta