Spark调度系统——任务调度器TaskScheduler

任务调度器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()
  }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Apache DolphinScheduler(incubator,原EasyScheduler)是一个大数据分布式工作流任务调度系统,主要解决大数据研发过程中ETL处理错综复杂的依赖关系,而不能直观监控任务健康状态等问题。DolphinScheduler以DAG流式的方式将Task组装起来,可实时监控任务的运行状态,同时支持重试、从指定节点恢复失败、暂停及Kill任务等操作。 设计特点:一个分布式易扩展的可视化DAG工作流任务调度系统。致力于解决数据处理流程中错综复杂的依赖关系,使调度系统在数据处理流程中开箱即用。 其主要目标如下: 1、以DAG图的方式将Task按照任务的依赖关系关联起来,可实时可视化监控任务的运行状态 2、支持丰富的任务类型:Shell、MR、Spark、SQL(mysql、postgresql、hive、sparksql),Python,Sub_Process、Procedure等 3、支持工作流定时调度、依赖调度、手动调度、手动暂停/停止/恢复,同时支持失败重试/告警、从指定节点恢复失败、Kill任务等操作 4、支持工作流优先级、任务优先级及任务的故障转移及任务超时告警/失败 5、支持工作流全局参数及节点自定义参数设置 6、支持资源文件的在线上传/下载,管理等,支持在线文件创建、编辑 7、支持任务日志在线查看及滚动、在线下载日志等 8、实现集群HA,通过Zookeeper实现Master集群和Worker集群去中心化 9、支持对Master/Worker cpu load,memory,cpu在线查看 10、支持工作流运行历史树形/甘特图展示、支持任务状态统计、流程状态统计 11、支持补数 12、支持多租户 13、支持国际化 14、还有更多等待伙伴们探索

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值