任务调度器TaskScheduler定义了对任务进行调度的接口规范,允许向Spark调度系统插入不同的TaskScheduler实现,但目前只有TaskSchedulerImpl这一个具体实现。TaskScheduler只为单个Driver调度任务。TaskSchedulerImpl的功能包括接收DAGScheduler给每个Stage创建的Task集合,按照调度算法将资源分配给Task,将Task交给Spark集群不同节点上的Executor运行,在这些Task执行失败时进行重试,通过推断执行减轻落后的Task对整体作业进度的影响。
Spark的资源调度为分两层:
- 第一层是ClusterManager(在YARN模式下为ResourceManager,在Mesos模式下为MesosMaster,在Standalone模式下为Master)将资源分配给Application
- 第二层是Application进一步将资源分配给Application的各个Task。TaskSchedulerImpl中的资源调度就是第二层的资源调度
1 TaskSchedulerImpl的属性
- maxTaskFailure:任务失败的最大次数
- isLocal:是否是Local部署模式
- SPECULATION_INTERVAL_MS:任务推断执行的时间间隔。可以通过spark.speculation.interval属性进行配置,默认为100ms
- MIN_TIME_TO_SPECULATION:用于保证原始任务至少需要运行的时间。
- taskResultGetter:类型为TaskResultGetter,它的作用是通过线程池(此线程池由Executors.newFixedThreadPool创建,大小默认为4,生成的线程名以task-result-getter开关),对Slave发送的Task的执行结果进行处理
2 TaskSchedulerImpl的初始化
用于对TaskSchedulerImpl进行初始化
//org.apache.spark.scheduler.TaskSchedulerImpl
def initialize(backend: SchedulerBackend) {
this.backend = backend
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()
}
- 1)使用参数传递的SchedulerBackend设置TaskSchedulerImpl的backend属性
- 2)创建根调度池
- 3)根据调度模式,创建相应的调度池构建器。
- 4)调用调度池构建器buildPools方法构建调度池
3 TaskSchedulerImpl的启动
启动任务调度器是通过其start方法实现的。TaskSchedulerImpl的start方法的实现如下:
override def start() {
backend.start()
if (!isLocal && conf.getBoolean("spark.speculation", false)) {
logInfo("Starting speculative execution thread")
speculationScheduler.scheduleAtFixedRate(new Runnable {
override def run(): Unit = Utils.tryOrStopSparkContext(sc) {
checkSpeculatableTasks()
}
}, SPECULATION_INTERVAL_MS, SPECULATION_INTERVAL_MS, TimeUnit.MILLISECONDS)
}
}
- 1)调用SchedulerBackend的start方法启动SchedulerBackend
- 2)当应用不是在Local模式下,并且设置了推断执行(即spark.speculation属性为true),那么设置一个执行间隔为SPECULATION_INTERVAL_MS(默认为100ms)的检查可推断任务的定时器
4 TaskSchedulerImpl与Task的提交
DAGScheduler将Stage中各个分区的Task封装为TaskSet后,会将TaskSet交给TaskSchedulerImpl处理。TaskSchedulerImpl的submitTasks方法是这一过程的入口,其实现如下:
override def submitTasks(taskSet: TaskSet) {
val tasks = taskSet.tasks //获取TaskSet中的所有Task
logInfo("Adding task set " + taskSet.id + " with " + tasks.length + " tasks")
this.synchronized {
val manager = createTaskSetManager(taskSet, maxTaskFailures) //创建TaskManager
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
}
if (conflictingTaskSet) {
throw new IllegalStateException(s"more than one active taskSet for stage $stage:" +
s" ${stageTaskSets.toSeq.map{_._2.taskSet.id}.mkString(",")}")
}
schedulableBuilder.addTaskSetManager(manager, manager.taskSet.properties)
if (!isLocal && !hasReceivedTask) { //设置检查TaskSchedulerImpl的饥饿状况的定时器
starvationTimer.scheduleAtFixedRate(new TimerTask() {
override def run() {
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)
}
hasReceivedTask = true //表示TaskSchedulerImpl已经接收到Task
}
backend.reviveOffers() //给Task分配资源并运行Task
}
- 1)获取TaskSet中的所有Task
- 2)调用createTaskSetManager方法创建TaskSetManager
- 3)在taskSetsByStageIdAndAttemp中设置TaskSet关联的Stage、Stage尝试及刚创建的TaskSetManager之间的三级映射关系
- 4)对当前TaskSet进行冲突检测,即taskSetsByStageIdAndAttempt中不应该存在同属于当前Stage,但是TaskSet却不相同的情况
- 5)调用调度池构建器的addTaskSetManager方法,将刚创建的TaskSetManager添加到调度池构建器的调度池中
- 6)如果当前应用程序不是Local模式并且TaskSchedulerImpl还没有接收到Task,那么设置一个定时器按照指定的时间间隔检查TaskSchedulerImpl的饥饿状况,当TaskSchedulerImpl已经运行Task后,取消此定时器
- 7)将hasReceivedTask设置为true,以表示TaskSchedulerImpl已经接收到Task
- 8)调用SchedulerBackend的reviveOffers方法给Task分配资源并运行Task
5 TaskSchedulerImpl与资源分配
以Local模式下SchedulerBackend的实现LocalSchedulerBackend为例,LocalSchedulerBackend的reviveOffers方法实际向LocalEndpoint发送ReviveOffers消息。LocalEndpoint接收到ReviveOffers消息后,将调用LocalEndpoint自己的reviveOffers方法,reviveOffers方法最终调用TaskSchedulerImpl的resourceOffers方法给Task分配资源
def resourceOffers(offers: Seq[WorkerOffer]): Seq[Seq[TaskDescription]] = synchronized {
var newExecAvail = false
for (o <- offers) {
executorIdToHost(o.executorId) = o.host
executorIdToTaskCount.getOrElseUpdate(o.executorId, 0)
if (!executorsByHost.contains(o.host)) { //更新Host与Executor的各种映射关系
executorsByHost(o.host) = new HashSet[String]()
executorAdded(o.executorId, o.host)
newExecAvail = true //标记添加了新的Executor
}
for (rack <- getRackForHost(o.host)) { //更新Host与机架之间的关系
hostsByRack.getOrElseUpdate(rack, new HashSet[String]()) += o.host
}
}
val shuffledOffers = Random.shuffle(offers) //随机洗牌,避免将任务总是分配给同样一组Worker
val tasks = shuffledOffers.map(o => new ArrayBuffer[TaskDescription](o.cores))
val availableCpus = shuffledOffers.map(o => o.cores).toArray //统计每个Worker的可用的CPU核数
val sortedTaskSets = rootPool.getSortedTaskSetQueue //所有TaskSetManager按照调度算法排序
for (taskSet <- sortedTaskSets) {
logDebug("parentName: %s, name: %s, runningTasks: %s".format(
taskSet.parent.name, taskSet.name, taskSet.runningTasks))
if (newExecAvail) {
taskSet.executorAdded() //重新计算TaskSet的本地性
}
}
var launchedTask = false
for (taskSet <- sortedTaskSets; maxLocality <- taskSet.myLocalityLevels) { //按照最大本地性的原则,给Task提供资源
do {
launchedTask = resourceOfferSingleTaskSet(
taskSet, maxLocality, shuffledOffers, availableCpus, tasks)
} while (launchedTask)
}
if (tasks.size > 0) {
hasLaunchedTask = true
}
return tasks //返回已经获得资源的任务列表
}
- 1)遍历WorkerOffers序列,对每一个WorkerOffer执行以下操作
-
①更新Host与Executor的各种映射关系
-
②调用TaskSchedulerImpl的executorAdded方法向DAGScheduler的DAGSchedulerEventProcessLoop投递ExecutorAdded事件
-
③标记添加了新的Executor(将newExecAvail设置为true)
-
④更新Host与机架之间的关系
-
- 2)对所有WorkerOffer随机洗牌,避免将任务总是分配给同样一组Worker
- 3)根据每个WorkerOffer的可用的CPU核数创建同等尺寸的任务描述数组
- 4)对每个WorkerOffer的可用的CPU核数统计到可用CPU数组中
- 5)调用rootPool的getSortedTaskSetQueue方法,对rootPool中的所有TaskSetManager按照调度算法排序
- 6)如果newExecAvail为true,那么调用每个TaskSetManager的executorAdded方法。
- 7)遍历TaskSetManager,按照最大本地性的原则(即从高本地性级别到低本地性级别)调用 resourceOfferSingleTaskSet方法,给单个TaskSet中的Task提供资源。如果在任何TaskSet所允许的本地性级别下,TaskSet中没有任何一个任务获得了资源,那么将调用TaskSetManager的abortIfCompletelyBlacklisted方法,放弃在黑名单中的Task
- 8)返回生成的TaskDescription列表,即已经获得了资源的任务列表
6 TaskSchedulerImpl的调度流程
上图描绘了TaskSchedulerImpl的调度流程,使用了SchedulerBackend,而不是SchedulerBackend的具体实现类,RpcEndpoint代表ShedulerBackend的具体实现中与其他组件进行通信的实例。
- 记号①:代表DAGScheduler调用TaskScheduler的submitTasks方法向TaskScheduler提交TaskSet
- 记号②:代表TaskScheduler接收到TaskSet后,创建对此TaskSet进行管理的TaskSetManager,并将此TaskSetManager通过调度池构建器添加到根调度池中
- 记号③:代表TaskScheduler调用SchedulerBackend的revieOffers方法给Task提供资源
- 记号④:SchedulerBackend向RpcEndpoint发送ReviveOffers消息
- 记号⑤:RpcEndpoint将调用TaskScheduler的resourceOffers方法给Task提供资源
- 记号⑥:TaskScheduler调用根调度池的getSortedTaskSetQueue方法对所有TaskSetManager按照调度算法进行排序后,对TaskSetManager管理的TaskSet按照“最大本地性”的原则选择其中的Task,最后为Task创建尝试执行信息,对Task进行序列化、生成TaskDescription等
- 记号⑦:调用Executor的launchTask方法运行Task尝试
7 TaskSchedulerImpl对执行结果的处理
Task在执行的时候会不断发送StatusUpdate消息,在Local模式下,LocalEndpoint接收到StatusUpdate消息后会先匹配执行TaskSchedulerImpl的statusUpdate方法,然后调用reviveOffers方法给其它Task分配资源。
TaskSchedulerImpl的statusUpdate方法用于更新Task的状态。Task的状态包括:运行中(RUNNING)、已完成(FINISHED)、失败(FAILED)、被杀死(KILLED)、丢失(LOST)五种。会从taskIdToTaskSetId、taskIdToExecutorId中移除此任务,并且调用taskResultGetter的enqueueSuccessfulTask方法。
def statusUpdate(tid: Long, state: TaskState, serializedData: ByteBuffer) {
var failedExecutor: Option[String] = None
var reason: Option[ExecutorLossReason] = None
synchronized {
try {
if (state == TaskState.LOST && taskIdToExecutorId.contains(tid)) { //从taskIdToExecutorId中获取Task对应的Executor的身份标识
val execId = taskIdToExecutorId(tid)
if (executorIdToTaskCount.contains(execId)) {
reason = Some(
SlaveLost(s"Task $tid was lost, so marking the executor as lost as well."))
removeExecutor(execId, reason.get) //移除Executor,移除的原因是SlaveLost
failedExecutor = Some(execId)
}
}
taskIdToTaskSetManager.get(tid) match {
case Some(taskSet) =>
if (TaskState.isFinished(state)) {
taskIdToTaskSetManager.remove(tid) //清除Task在taskIdToTaskSetManager、taskIdToExecutorId中的数据
taskIdToExecutorId.remove(tid).foreach { execId =>
if (executorIdToTaskCount.contains(execId)) {
executorIdToTaskCount(execId) -= 1
}
}
}
if (state == TaskState.FINISHED) { //对执行成功的任务的结果进行处理
taskSet.removeRunningTask(tid) //减少正在运行的任务数量
taskResultGetter.enqueueSuccessfulTask(taskSet, tid, serializedData)
} else if (Set(TaskState.FAILED, TaskState.KILLED, TaskState.LOST).contains(state)) { //对执行失败的任务的结果进行处理
taskSet.removeRunningTask(tid)
taskResultGetter.enqueueFailedTask(taskSet, tid, state, serializedData)
}
case None =>
logError(
("Ignoring update with state %s for TID %s because its task set is gone (this is " +
"likely the result of receiving duplicate task finished status updates)")
.format(state, tid))
}
} catch {
case e: Exception => logError("Exception in statusUpdate", e)
}
}
//重新安排丢失的Executor上正在运行的Task
if (failedExecutor.isDefined) {
assert(reason.isDefined)
dagScheduler.executorLost(failedExecutor.get, reason.get)
backend.reviveOffers()
}
}