在Task调度相关的两篇文章《Spark源码分析之五:Task调度(一)》与《Spark源码分析之六:Task调度(二)》中,我们大致了解了Task调度相关的主要逻辑,并且在Task调度逻辑的最后,CoarseGrainedSchedulerBackend的内部类DriverEndpoint中的makeOffers()方法的最后,我们通过调用TaskSchedulerImpl的resourceOffers()方法,得到了TaskDescription序列的序列Seq[Seq[TaskDescription]],相关代码如下:
// 调用scheduler的resourceOffers()方法,分配资源,并在得到资源后,调用launchTasks()方法,启动tasks
// 这个scheduler就是TaskSchedulerImpl
launchTasks(scheduler.resourceOffers(workOffers))
/**
* 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 {
这个TaskDescription很简单,是传递到executor上即将被执行的Task的描述,通常由TaskSetManager的resourceOffer()方法生成。代码如下:
/**
* Description of a task that gets passed onto executors to be executed, usually created by
* [[TaskSetManager.resourceOffer]].
*/
private[spark] class TaskDescription(
val taskId: Long,
val attemptNumber: Int,
val executorId: String,
val name: String,
val index: Int, // Index within this task's TaskSet
_serializedTask: ByteBuffer)
extends Serializable {
// Because ByteBuffers are not serializable, wrap the task in a SerializableBuffer
// 由于ByteBuffers不可以被序列化,所以将task包装在SerializableBuffer中,_serializedTask为ByteBuffer类型的Task
private val buffer = new SerializableBuffer(_serializedTask)
// 序列化后的Task, 取buffer的value
def serializedTask: ByteBuffer = buffer.value
override def toString: String = "TaskDescription(TID=%d, index=%d)".format(taskId, index)
}
此时,得到Seq[Seq[TaskDescription]],即Task被调度到相应executor上后(仅是逻辑调度,实际上并未分配到executor上执行),接下来要做的,便是真正的将Task分配到指定的executor上去执行,也就是本篇我们将要讲的Task的运行。而这部分的开端,源于上述提到的CoarseGrainedSchedulerBackend的内部类DriverEndpoint中的launchTasks()方法,代码如下:
// Launch tasks returned by a set of resource offers
private def launchTasks(tasks: Seq[Seq[TaskDescription]]) {
// 循环每个task
for (task <- tasks.flatten) {
// 序列化Task
val serializedTask = ser.serialize(task)
// 序列化后的task的大小超出规定的上限
// 即如果序列化后task的大小大于等于框架配置的Akka消息最大大小减去除序列化task或task结果外,一个Akka消息需要保留的额外大小的值
if (serializedTask.limit >= akkaFrameSize - AkkaUtils.reservedSizeBytes) {
// 根据task的taskId,在TaskSchedulerImpl的taskIdToTaskSetManager中获取对应的TaskSetManager
scheduler.taskIdToTaskSetManager.get(task.taskId).foreach { taskSetMgr =>
try {
var msg = "Serialized task %s:%d was %d bytes, which exceeds max allowed: " +
"spark.akka.frameSize (%d bytes) - reserved (%d bytes). Consider increasing " +
"spark.akka.frameSize or using broadcast variables for large values."
msg = msg.format(task.taskId, task.index, serializedTask.limit, akkaFrameSize,
AkkaUtils.reservedSizeBytes)
// 调用TaskSetManager的abort()方法,标记对应TaskSetManager为失败
taskSetMgr.abort(msg)
} catch {
case e: Exception => logError("Exception in error callback", e)
}
}
}
else {// 序列化后task的大小在规定的大小内
// 从executorDataMap中,根据task.executorId获取executor描述信息executorData
val executorData = executorDataMap(task.executorId)
// executorData中,freeCores做相应减少
executorData.freeCores -= scheduler.CPUS_PER_TASK
// 利用executorData中的executorEndpoint,发送LaunchTask事件,LaunchTask事件中包含序列化后的task
executorData.executorEndpoint.send(LaunchTask(new SerializableBuffer(serializedTask)))
}
}
}
launchTasks的执行逻辑很简单,针对传入的TaskDescription序列,循环每个Task,做以下处理:
1、首先对Task进行序列化,得到serializedTask;
2、针对序列化后的Task:serializedTask,判断其大小:
2.1、序列化后的task的大小达到或超出规定的上限,即框架配置的Akka消息最大大小,减去除序列化task或task结果外,一个Akka消息需要保留的额外大小的值,则根据task的taskId,在TaskSchedulerImpl的taskIdToTaskSetManager中获取对应的TaskSetManager,并调用其abort()方法,标记对应TaskSetManager为失败;
2.2、序列化后的task的大小未达到上限,在规定的大小范围内,则:
2.2.1、从executorDataMap中,根据task.executorId获取executor描述信息executorData;
2.2.2、在executorData中,freeCores做相应减少;
2.2.3、利用executorData中的executorEndpoint,即Driver端executor通讯端点的引用,发送LaunchTask事件,LaunchTask事件中包含序列化后的task,将Task传递到executor中去执行。
接下来,我们重点分析下上述流程。
先说下异常流程,即序列化后Task的大小超过上限时,对TaskSet标记为失败的处理。入口方法为TaskSetManager的abort()方法,代码如下:
def abort(message: String, exception: Option[Throwable] = None): Unit = sched.synchronized {
// TODO: Kill running tasks if we were not terminated due to a Mesos error
// 调用DAGScheduler的taskSetFailed()方法,标记TaskSet运行失败
sched.dagScheduler.taskSetFailed(taskSet, message, exception)
// 标志位isZombie设置为true
isZombie = true
// 满足一定条件的情况下,将TaskSet标记为Finished
maybeFinishTaskSet()
}
abort()方法处理逻辑共分三步:
第一,调用DAGScheduler的taskSetFailed()方法,标记TaskSet运行失败;
第二,标志位isZombie设置为true;
第三,满足一定条件的情况下,将TaskSet标记为Finished。
首先看下DAGScheduler的taskSetFailed()方法,代码如下:
/**
* Called by the TaskSetManager to cancel an entire TaskSet due to either repea