Spark TaskScheduler调度算法与资源分配源码分析

目录

  • TaskScheduler调度算法与调度池
  • TaskScheduler任务提交与资源分配
    • 提交Task
    • 资源分配

Spark TaskScheduler调度算法与资源分配源码分析_第1张图片

TaskScheduler调度算法与调度池

  在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先入先出调度算法的实现

  1. 先获取两个Schedulable s1,s2的优先级
  2. 对两个优先级Schedulable 进行比较
  3. 如果优先级相同,则对两个Schedulable stageId进行比较
  4. 如果结果小于0,则优先调度s1,否则优先调度s2

FAIR公平调度算法的实现

  1. 获取两个Schedulable s1,s2的minShare,runningTasks,Needy,minShareRatio,taskToWeightRatio的信息
  2. 如果s1的正在运行task数小于minShare,并且s2的正在运行task数大于等于minShare,则说明s1的分配的资源不足,优先调度s1
  3. 反之,如果s1的正在运行task数大于等于minShare,并且s2的正在运行task数小于minShare,则说明s2的分配的资源不足,优先调度s2
  4. 如果s1和s2的正在运行task数都小于minShare,那么对minShareRatio进行比较,如果s1的minShareRatio小于s2的minShareRatio,那么优先调度s1,反之优先调度s2
  5. 如果s1和s2的正在运行task数都大于等于minShare,则对taskToWeightRatio进行比较,如果s1的taskToWeightRatio小于s2的taskToWeightRatio,那么优先调度s1,反之优先调度s2
  6. 如果minShareRatio或taskToWeightRatio比值相等,则比较s1和s2的name,如果s1小于s2,则优先调度s1,反之优先调度s2

接下面来看看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两种调度算法有两种实现FIFOSchedulableBuilderFairSchedulableBuilder

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添加到父调度池中

TaskScheduler任务提交与资源分配

提交Task

  在DAGScheduer对Stage划分,并对Task最佳位置进行了计算后。在DAGScheduer将Task提交给TaskScheduler之前,将多个Task打包为TaskSet后,调用taskScheduler的submitTasks方法,实则是调用了TaskSchedulerImpl的submitTasks方法。查看submitTasks方法,做了如下事情:

  1. 获取TaskSet中的task数组
  2. 为每个taskSet创建了一个TaskSetManager,用于管理TaskSet,包括任务推断,Task本地性,并对Task的资源进行分配
  3. 向调度池中添加刚才创建TaskSetManager
  4. 判断应用程序是否为local模式并且有没有接收到Task,如果是,则创建一个定时器,通过指定时间检查TaskSchedulerImpl的饥饿状况。如果接收到Task,则取消这个定时器
  5. 同步块中最后判断TaskSchedulerImpl是否接收到Task。如果接收到Task,则调用backend.reviveOffers()方法,为Task分配资源
  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分配资源,该方法内容如下:

  1. 筛选掉死去的executors
  2. 将这个application所有可用的executor,将其封装为workOffers,每个workOffers代表了每个executor可用的cpu资源数量
  3. 调用scheduler的resourceOffers方法,将workOffers传入,实际调用的是TaskSchedulerImpl的resourceOffers方法
  4. 获取scheduler.resourceOffers的返回值,调用launchTasks方法,启动task

在启动task之前还有重要的一步,就是TaskSchedulerImpl的resourceOffers方法,该方法负责进行资源分配,步骤如下:
【1】遍历WorkerOffer

  1. 更新host与executor的映射关系
  2. 标记添加了新的executor
  3. 更新host与机架之间的关系

【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列表

你可能感兴趣的:(Spark)