在DAGScheduer对Stage划分,DAGScheduer将Task提交给TaskScheduler,将多个Task打包为TaskSet后,调用taskScheduler的submitTasks方法。TaskScheduler是一个Trait,在Spark中唯一的实现是TaskSchedulerImpl类,TaskSchedulerImpl用于接收DAGSchduler给每个Stage创建好的TaskSet,按照调度算法将资源分配给Task并且将Task交给Spark集群上Executor运行,当Task失败时进行重试,通过推断执行减轻落后的Task对整体作业进度的影响。
TaskSchedulerImpl中包含一些重要的组件:
1、Pool调度池:TaskSchedulerImpl对任务的调度基于调度池
2、TaskSetManager:用于管理TaskSet,包括任务推断,Task本地性,并对Task的资源进行分配
Pool调度池中有一个根队列rootPool,根队列中包含了多个子调度池,子调度池中还可以包含其他的调度池或TaskSetManager。所以,整个调度池是个多层级的结构。Pool的创建需要四个参数:
1.poolName:调度池名称
2.schedulingMode:调度池模式,共有三种类型:FAIR(公平调度模式)、FIFO(先入先出模式)、NONE
3.initMinShare:MinShare初始值
4.initWeight:Weight初始值
在Pool类中,有一个比较重要的方法taskSetSchedulingAlgorithm。调度池中对taskSet的调度,取决于调度算法,根据调度模式进行匹配。如果是FIFO调度模式,则为FIFO先入先出算法,如果是FAIR调度模式,则为FAIR公平调度算法
var taskSetSchedulingAlgorithm: SchedulingAlgorithm = {
schedulingMode match {
case SchedulingMode.FAIR =>
new FairSchedulingAlgorithm()
case SchedulingMode.FIFO =>
new FIFOSchedulingAlgorithm()
case _ =>
val msg = "Unsupported scheduling mode: $schedulingMode. Use FAIR or FIFO instead."
throw new IllegalArgumentException(msg)
}
}
下面来看看两种调度算法的实现。两种调度算法位于org.apache.spark.scheduler.SchedulingAlgorithm
类,该类中仅定义了一个方法,对两个Schedulable进行比较
def comparator(s1: Schedulable, s2: Schedulable): Boolean
FIFO先入先出调度算法的实现
FAIR公平调度算法的实现
接下面来看看Pool的创建方法。根调度池的创建,在TaskSchedulerImpl的initialize初始化方法中:
1.创建rootPool,传入参数,其中schedulingMode调度模式,是我们可以通过添加spark参数spark.scheduler.mode进行配置,默认为FIFO
2.根据调度匹配,获得对应的schedulableBuilder
3.调用schedulableBuilder的buildPools方法
rootPool = new Pool("", schedulingMode, 0, 0)
schedulableBuilder = {
// 根据调度模式匹配
schedulingMode match {
case SchedulingMode.FIFO =>
new FIFOSchedulableBuilder(rootPool)
case SchedulingMode.FAIR =>
new FairSchedulableBuilder(rootPool, conf)
case _ =>
throw new IllegalArgumentException(s"Unsupported spark.scheduler.mode: $schedulingMode")
}
}
schedulableBuilder.buildPools()
schedulableBuilder是一个Trait,定义了三个方法:
1.rootPool:获取根调度池
2.buildPools:构建调度池
3.addTaskSetManager:向调度池内添加TaskSetManager
schedulableBuilder中,对FIFO和FAIR两种调度算法有两种实现FIFOSchedulableBuilder
和FairSchedulableBuilder
。
FIFOSchedulableBuilder的实现如下:
1.buildPools方法不做任何实现
2.addTaskSetManager方法向rootPool中添加了TaskSetManager
FairSchedulableBuilder的实现如下:
FairSchedulableBuilder读取用户指定的参数spark.scheduler.allocation.file
对应的文件,如果没有指定该文件,则默认加载默认路径的配置文件:$SPARK_HOME/CONF/fairscheduler.xml
buildPools方法:用于创建调度池
1.调用buildFairSchedulerPool方法去获取文件输入流,解析XML配置文件,创建调度池
2.最后调用buildDefaultPool方法,创建默认池
buildFairSchedulerPool方法:用于创建FAIR调度算法调度池
1.将文件输入流转换为XML
2.将XML中对应的配置项,转换成调度池中对应的属性。例如获取XML中pool name=”production”,将production作为调度池名称,以此类推,获取schedulingMode,minShare ,weight配置,根据获取到的配置,创建调度池,将刚创建的调度池添加到根调度池
buildDefaultPool方法:用于创建默认调度池
1.判断根调度池中是否存在default Pool默认调度池
2.如果不存在,就创建
3.将默认调度池添加到根调度池
在FairSchedulableBuilder中,预先设置好了默认的配置常量,默认为default调度池,默认调度模式为FIFO,默认MINIMUM_SHARE为0,默认WEIGHT为1
addTaskSetManager方法:用于向调度池中添加TaskSetManager
1.TaskSetManager的默认父调度池为默认调度池default Pool
2.如果TaskSet中properties参数不为空,获取properties中FAIR_SCHEDULER_PROPERTIES,DEFAULT_POOL_NAME配置,得到父调度池信息,将TaskSetManager父调度池修改为从properties获取到的调度池
3.如果properties中,没有获取到父调度池,则创建默认父调度池default Pool
4.将TaskSetManager添加到父调度池中
在DAGScheduer对Stage划分,并对Task最佳位置进行了计算后。在DAGScheduer将Task提交给TaskScheduler之前,将多个Task打包为TaskSet后,调用taskScheduler的submitTasks方法,实则是调用了TaskSchedulerImpl的submitTasks方法。查看submitTasks方法,做了如下事情:
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
// 由于TaskSetManager不是线程安全的,所有对它的访问都应该同步,所以添加了synchronized修饰
// 并创建一个HashMap,用于存储stage对应的TaskSetManager
val stageTaskSets =
taskSetsByStageIdAndAttempt.getOrElseUpdate(stage, new HashMap[Int, TaskSetManager])
// 将TaskSetManager加入刚才创建的HashMap中,进行缓存
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
方法为Task分配资源。以Standalone模式为例,当前的backend为StandaloneSchedulerBackend。StandaloneSchedulerBackend的reviveOffers方法是继承其父类的CoarseGrainedSchedulerBackend
的reviveOffers方法。
CoarseGrainedSchedulerBackend的reviveOffers方法:向driver Endpoint发送消息ReviveOffers,driver Endpoint实则就是CoarseGrainedSchedulerBackend在启动时创建的driver Endpoint
override def reviveOffers() {
// 向driver Endpoint发送消息ReviveOffers
driverEndpoint.send(ReviveOffers)
}
在CoarseGrainedSchedulerBackend driver Endpoint接受到发送来的ReviveOffers消息后,进行模式匹配,调用makeOffers方法。makeOffers方法向所有可用executor分配资源,该方法内容如下:
在启动task之前还有重要的一步,就是TaskSchedulerImpl的resourceOffers方法,该方法负责进行资源分配,步骤如下:
【1】遍历WorkerOffer
【2】可用的executor进行shuffle分散,避免将task放在同一个worker上,进行负载均衡
【3】根据每个WorkerOffer的可用的cpu核数创建同等尺寸的TaskDescription数组
【4】将每个WorkerOffer的可用的cpu核数统计到availableCpus数组中
【5】按照调度算法排序,从调度池中获取排序的taskSet队列
【6】taskSet任务分配算法
算法中,执行步骤如下:
(1) 遍历taskSet,从最快的本地化级别开始,调用resourceOfferSingleTaskSet方法,给每个Task Set中Task进行分配资源。resourceOfferSingleTaskSet方法:
遍历WorkerOffer,如果当前executor的cpu数大于每个task所使用的cpu数量,则选择在该executor上启动task,调用taskSet的resourceOffer方法,在executor上,使用这次本地化级别,查看哪些task可以启动。
即对当前taskSet,尝试使用最快的本地化级别,给task分配资源,让task在executor上启动,如果启动异常,跳出循环,换下一种本地化级别尝试,直到taskSet在某个本地化级别下,task在executor上启动
(2)如果在所有TaskSet所允许的本地级别下,TaskSet中没有任何一个Task成功启动,调用taskSet的abortIfCompletelyBlacklisted方法,将其添加到黑名单,放弃该task
(3)返回已经获得资源的task列表