Spark1.6.3 Driver端 task调度源码分析

本文深入探讨Spark 1.6.3的DAGScheduler如何分析stages并生成TaskSet,然后通过TaskSchedulerImpl提交到SchedulerBackend进行调度。重点分析粗粒度调度,特别是CoarseGrainedSchedulerBackend的DriverEndpoint如何响应调度请求,涉及Executor通信和资源管理。
摘要由CSDN通过智能技术生成

0.背景

当一个Action操作触发了Spark Job提交后:

1. Spark首先会在DAGScheduler.scala 中对其stages进行递归分析

2. 然后针对某个可执行的stage,根据其partitions的大小生产tasks,并封装成TaskSet

3. 最后调用taskSchedulerImpl.scala的submitTasks(taskSet) 方法发送backend.reviveOffers(),请求SchedulerBackend进行调度。


关于SchedulerBackend:

1. 位于在TaskScheduler下层,与各个节点上的Executors直接通信

2. 可以对接不同的资源管理系统,分为粗粒度(e.g., standalone,yarn)和 细粒度(e.g., mesos)

3. 本文主要分析粗粒度下的调度过程,实现位于CoarseGrainedSchedulerBackend.scala



1.CoarseGrainedSchedulerBackend.scala 接收调度请求

1.1 Backend中有一个类叫做DriverEndpoint,本质上是RpcEndpoint,用于接收信息并处理



1.2 当DriverEndpoint接收到以下三种信息时,Backend会执行调度行为,也就是调用makeOffers()或者是makeOffers(executorId: String)

第一种(有新的taskSet提交):

receive函数接收到ReviveOffers,会调度makeOffers()函数
第二种(task完成的状态更新):

receive函数接收到StatusUpdate(executorId, taskId, state,data),并且state是完成,则调度makeOffers(executorId)函数
第三种(有新的Executor注册):

receiveAndReply函数接收到RegisterExecutor(executorId, executorRef, hostPort, cores, logUrls),会调度makeOffers()函数

1.3 分析makeOffers和makeOffers(executorId: String)函数:
makeOffers():
首先,将每一个executor中的主要信息封装成一个WorkerOffer,WorkerOffer包含了三个信息:executor的Id,所在host,拥有的freecores。 然后,将所有的WorkerOffer封装在一起,形成一个Seq形式的列表,叫做WorkerOffers
makeOffers(executorId: String):
类似,首先将这一个executor变成一个WorkerOffer,然后再套上Seq的形式构成WorkerOffers。
最后:
会调用 launchTasks(scheduler.resourceOffers(workOffers))进行下一步调度,其中scheduler.resourceOffers(workOffers)主要完成形式上的调度,返回一系列的Seq[Seq[TaskDescription]];launchTasks主要用于和各个节点的executor进行RPC通信,指挥实际的调度。



2.TaskSchedulerImpl.scala选择TaskSet和Executor(具体代码太长就不贴出来了)
2.1 分析下scheduler.resourceOffers(workOffers)的过程

resourceOffers函数的实现是在TaskSchedulerImpl.scala函数中,该函数的参数是(offers: Seq[WorkerOffer]),返回值是Seq[Seq[TaskDescription]],也就是一个executor对应一个workerOffer,一个workerOffer对应一个Seq[TaskDescription],TaskDescription描述了即将调用的task的信息。
具体分析如下:
(1)val shuffledOffers = Random.shuffle(offers)
因为workOffers形成时,里面executor的顺序是固定的(主要针对makeOffers()),为防止后面遍历workOffers时前面几个executors总获取优先调度,这里提前将现有的workOffers顺序打乱。
(2)val tasks = shuffleOffers.map(o => new ArrayBuffer[TaskDescription](o.cores))
构造Seq[ArrayBuffer[TaskDescription]],用于保存形式上调度成功的task信息。这里将每个makeOffer映射成一个数组,数组类型为TaskDescription,数组的预留长度为freecores的大小(Spark中一个core对应一个task)。此时数组还是空的。
(3)val availableCpus = shuffleOffers.map(o =>o.cores).toArray
构造Array[Int],保存每个executor的freecores
(4)sortedTaskSets = rootPool.getSortedTaskSetQueue
rootPool为Pool类型,负责TaskSet级别的调度,有两个方法:FIFO和Fair,具体位于SchedulerBuilder.scala中。在TaskSet提交时,会将其加入Pool中,这里是获取已经排序好的TaskSets。
(5) for (taskSet <- sortedTaskSets; maxLocality <- taskSet.myLocalityLevels) {
          do{
              launchedTask = resourceOfferSingleTaskSet(taskSet, maxLocalty, shuffleOffers, availableCpus, tasks)
          }while (launchedTask)
      }
这里是针对每一个taskSet,按照locality的从高到低进行调度。taskSet.myLocalityLevels从高到低的保存了这个TaskSet对于Locality的要求,如果taskSet.myLocalityLevels是{PROCESS_LOCAL, NODE_LOCAL, RACK_LOCAL},表示这个TaskSet中存在对PROCESS_LOCAL,NODE_LOCAL,以及RACK_LOCAL有要求的tasks。这样,调度会从高到低满足这些要求,如果高级别的locality调度没有实现,也就是launchedTask=false,则不进行更低级别的调度,当然,maxLocality 在内部还是会根据Delay Scheduling进行调整,变成allowedLocality,具体涉及到每个级别的等待时间等等。
(6)如果调度成功,resourceOfferSingleTaskSet函数会对参数tasks添加TaskDescription,最终该函数返回tasks

2.2 分析下resourceOfferSingleTaskSet(taskSet, maxLocalty, shuffleOffers, availableCpus, tasks) 的过程:

具体分析如下:
(1) for (i <- 0 until shuffledOffers.size) {......}
是依次遍历每个executor,并尝试调度tasks
(2) if (availableCpus(i) >= CPUS_PER_TASK) 
判断executor的freecores是否满足调度条件
(3) for (task <- taskSet.resourceOffer(execId, host, maxLocality)) { 
          tasks(i) += task
          ......
          availableCpus(i) -= CPUS_PER_TASK
          ......
          lanchedTask = true
      }
taskSet.resourceOffer(execId, host, maxLocality)是对每个executor尝试调度,如果调度成功,则将task信息加入tasks(i) 中,freecores数量减少,lanchedTask设置为true。貌似taskSet.resourceOffer(execId, host, maxLocality)只能返回一个TaskDescription,所以这个for循环只运行一轮。



3.TaskSetmanager.scala 根据Locality选择task(具体代码太长就不贴出来了)
3.1 分析下resourceOffer(execId, host, maxLocality) 的过程:

具体分析如下:
(1) if (!isZombie) {......}
首先判断该TaskSet是否已经完成调度
(2) if (maxLocality != TaskLocality.NO_PREF) {
          allowedLocality = getAllowedLocalityLevel(curTime)
          ......
      }
getAllowedLocalityLevel(curTime)会根据DelayScheduling调整合适的Locality,并用allowedLocality替代maxLocality
(3) dequeueTask(execId, host, allowedLocality) match {......}
根据allowedLocality寻找合适的task,如果成功则返回Some((index=Int, taskLocality=TaskLocality.Value, speculative=Boolean)),失败则返回None。参数index表示task在taskSet中的索引,taskLocality表示调度实现的locality,speculative表示是不是speculative task的调度
(4) case Some((index, taskLocality, speculative)) => {
      ......
      if (maxLocality != TaskLocality.NO_PREF) {
          currentLocalityIndex = getLocalityIndex(taskLocality)
          lastLaunchTime = curTime
      }   
      ......
      sched.dagScheduler.taskStarted(task, info)
      return Some(new TaskDescription(taskId = taskId, attemptNumber = attemptNum, execId, taskName, index, serializedTask))
}
调度成功后会调整Delay Scheduling, 然后告知dagScheduler(taskStarted(task, info) 会post一个BeginEvent(task, taskinfo)的事件),最后构建TaskDescription并返回

3.2 分析下dequeueTask(execId: String, host: String, maxLocality: TaskLocality.Value)的过程:

具体分析如下:
(1) 首先介绍下Spark中有关Locality的结果数据结构:
pendingTasksForExecutor = new HashMap[String, ArrayBuffer[Int]]: 存储分别对每个executor有locality倾向的tasks
pendingTasksForHost = new HashMap[String, ArrayBuffer[Int]]: 存储分别对每个Host有locality倾向的tasks
pendingTasksForRack = new HashMap[String, ArrayBuffer[Int]]: 存储分别对每个Rack有locality倾向的tasks
pendingTasksWithNoPrefs = new ArrayBuffer[Int]: 存储没有locality倾向的tasks
allpendingTasks = new ArrayBuffer[Int]: 存储所有的pending tasks
speculatableTasks = new HashSet[Int]: 存储所有的speculatable tasks
(2) for (index <- dequeueTaskFromList(execId, getPendingForExecutor(execId))) {
          return Some((index, TaskLocality.PROCESS_LOCAL, false))
      }
getPendingForExecutor(execId)是获取对该execId有倾向的tasks List,然后dequeueTaskFromList(execId, getPendingForExecutor(execId)) 从List最后端取出一个task进行return,如果List为空,则匹配下一级别的Locality。
(3) 总的来说:
该函数依次匹配满足PROCESS_LOCAL, NODE_LOCAL, NO_PREF, RACK_LOCAL 以及 ANY的task, 使用到的数据结构依次为pendingTasksForExecutor,pendingTasksForHost,pendingTasksWithNoPrefs,pendingTasksForRack,allpendingTasks。最后是使用speculatableTasks 寻找speculative task。
(4) 最终,将对应的task Index, locality 和 是不是speculative 返回。



4. CoarseGrainedSchedulerBackend.scala 发送调度请求
4.1 在完成所有形式上的调度后,launchTasks(tasks: Seq[Seq[TaskDescription]])将进行与Executor通信,指挥实际的调度:

具体分析如下:
(1) for (task <- tasks.flatten) {......} 
将Seq[Seq[TaskDescription]]扁平化,实现对每个TaskDescription的遍历
(2) val serializedTask = ser.serialize(task)
仅为要进行RPC通信,先把task给序列化,借助Akka
(3) if (serializedTask.limit >= akkaFrameSize - AkkaUtils.reservedSizeBytes) {......}
处理序列化大小超出范围的情况
(4) val executorData = executorDatamap(task.executorId)
     executorData.freeCores -= scheduler.CPUS_PER_TASK
     executorData.executorEndpoint.send(LaunchTask(new SerializeableBuffer(serializedTask)))
首先获取executorData, executorData保存了一个executor所在host,totalcores,freecores,RPC通信接口executorEndpoint,RPC通信地址executorAddreess。然后对freecores进行更新,最后利用RPC接口将LaunchTask发送过去。



5.  总体流程图示





  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值