前面我们已经分析了DAGScheduler对Stage划分,并对Task的最佳位置进行计算之后,通过调用taskScheduler的submitTasks方法,将每个stage的taskSet进行提交。
今天给大家讲一下taskScheduler是如何将Task发送到各个Executor上执行的。
TaskScheduler
在taskScheduler的submitTasks方法中会为每个TaskSet创建一个TaskSetManager,
用于管理taskSet。然后向调度池中添加该TaskSetManager,
最后调用backend.reviveOffers()方法为Task分配资源
//TODO 该方法用于提交Tasks
override def submitTasks(taskSet: TaskSet) {
//TODO 获取taskSet 中的 task
val tasks = taskSet.tasks
logInfo("Adding task set " + taskSet.id + " with " + tasks.length + " tasks")
this.synchronized {
//TODO 为taskSet 创建一个TaskSetManager
val manager = createTaskSetManager(taskSet, maxTaskFailures)
//TODO 保存到一个map中
activeTaskSets(taskSet.id) = manager
//TODO 向调度池中添加刚才创建TaskSetManager
schedulableBuilder.addTaskSetManager(manager, manager.taskSet.properties)
//TODO 判断程序是否为local,并且TaskSchedulerImpl没有收到Task
if (!isLocal && !hasReceivedTask) {
//TODO 创建一个定时器定时检查TaskSchedulerImpl的饥饿情况
starvationTimer.scheduleAtFixedRate(new TimerTask() {
override def run() {
//TODO 如果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, STARVATION_TIMEOUT)
}
//TODO 标记已经接收到Task
hasReceivedTask = true
}
//TODO 给 Task 分配资源
backend.reviveOffers()
}
下面我们就来看一下backend.reviveOffers()这个方法,在提交模式是standalone模式下,
实际上是调用SparkDeploySchedulerBackend的reviveOffers方法,由于没有对父类的方法进行重写,
因此调用的是父类CoarseGrainedSchedulerBackend的reviveOffers方法,这个方法是向Driver发送一个ReviveOffers消息
CoarseGrainedSchedulerBackend
override def reviveOffers() {
//TODO 向DriverActor发送一个reviceOffers消息
driverActor ! ReviveOffers
}
DriverActor收到信息后会调用makeOffers方法
//TODO 调用makeOffers向Executor提交Task
case ReviveOffers =>
makeOffers()
makeOffers方法内部会将application所有可用的executor封装成多个workOffer,每个workOffer内部封装了每个executor的资源信息。
然后调用taskScheduler的resourceOffers从上面封装的workOffer信息为每个task分配合适的executor。
最后调用launchTasks启动task
// Make fake resource offers on all executors
def makeOffers() {
//TODO 调用launchTask向Executor提交Task
launchTasks(scheduler.resourceOffers(executorDataMap.map { case (id, executorData) =>
new WorkerOffer(id, executorData.executorHost, executorData.freeCores)
}.toSeq))
}
下面看一下launchTasks这个方法。
这个方法主要做了这些操作:
1.遍历每个task,然后将每个task信息序列化
2.判断序列化后的task信息,如果大于rpc发送消息的最大值,则停止,建议调整rpc的spark.akka.frameSize,如果小于rpc发送消息的最大值,则找到task对应的executor,然后更新executor对应的一些内存资源信息
3.DriverActor向executor发送LaunchTask消息。
// Launch tasks returned by a set of resource offers
def launchTasks(tasks: Seq[Seq[TaskDescription]]) {
for (task <- tasks.flatten) {
//拿到序列化器
val ser = SparkEnv.get.closureSerializer.newInstance()
//TODO 序列化Task
val serializedTask = ser.serialize(task)
if (serializedTask.limit >= akkaFrameSize - AkkaUtils.reservedSizeBytes) {
val taskSetId = scheduler.taskIdToTaskSetId(task.taskId)
scheduler.activeTaskSets.get(taskSetId).foreach { taskSet =>
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)
taskSet.abort(msg)
} catch {
case e: Exception => logError("Exception in error callback", e)
}
}
}
else {
//TODO 找到task对应的executor
val executorData = executorDataMap(task.executorId)
executorData.freeCores -= scheduler.CPUS_PER_TASK
//TODO 向Executor发送序列化好的Task
executorData.executorActor ! LaunchTask(new SerializableBuffer(serializedTask))
}
}
}
CoarseGrainedExecutorBackend
下面就看Executor收到消息后做了哪些操作,这里executorData.executorActor实际上就是在创建Executor守护进程时候创建的那个CoarseGrainedExecutorBackend
所以找到CoarseGrainedExecutorBackend处理接收到LaunchTask消息后做了哪些操作。
首先判断当前的executor是不是为空,如果不为空就会反序列化task的信息,然后调用executor的launchTask方法。
//TODO DirverActor发送给Executor的消息,让Executor启动计算任务
case LaunchTask(data) =>
if (executor == null) {
logError("Received LaunchTask command but executor was null")
System.exit(1)
} else {
//获得序列化器
val ser = env.closureSerializer.newInstance()
//TODO 反序列化Task
val taskDesc = ser.deserialize[TaskDescription](data.value)
logInfo("Got assigned task " + taskDesc.taskId)
//TODO 将反序列化后的Task放到线程池里面
executor.launchTask(this, taskId = taskDesc.taskId, attemptNumber = taskDesc.attemptNumber,
taskDesc.name, taskDesc.serializedTask)
}
下面就看下executor的launchTask都做了哪些操作
首先executor会为每个task创建一个TaskRunner(实现了Runnable),然后会将task添加到runningTasks的集合中,并标记其为运行状态,最后将taskRunner放到一个线程池中执行
//TODO 启动Task
def launchTask(
context: ExecutorBackend,
taskId: Long,
attemptNumber: Int,
taskName: String,
serializedTask: ByteBuffer) {
//TODO 创建一个TaskRunner对象,把Task的信息封装到TaskRunner里面
val tr = new TaskRunner(context, taskId = taskId, attemptNumber = attemptNumber, taskName,
serializedTask)
runningTasks.put(taskId, tr)
//TODO 把TaskRunner丢到线程池中
threadPool.execute(tr)
}
在taskRunner的run方法中会去执行每个task,并输出一系列的日志。task运行完成后会向driver发送消息,driver会更新executor的一些资源数据,并标记task已完成。
至此task启动完成
微信公众号:喜讯Xicent