DAGScheduler任务调度剖析

本文深入剖析了Spark的DAGScheduler的主要功能,包括job的stage划分、任务的首选位置决策、面向stage的调度策略以及stage输出的追踪。在stage划分部分,详细介绍了创建Stage的过程;任务的首选位置决策则在submitMissingTasks方法中得以体现;DAGScheduler不负责具体task调度,而是将任务提交给TaskScheduler。此外,DAGScheduler还需关注ShuffleMapStage的输出,以便优化调度,并在必要时重新提交stage。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、主要功能

  • 对每个job划分stage
  • 决定任务运行的首选位置(preferred locations)
  • 面向stage进行任务调度
  • 跟踪stage的输出
  • 需要时重新提交stage

二、主要功能的展开描述

1、怎么划分stage?

先看这个数据结构,stage划分之后保存在一个HashMap中,stageId作为key,Stage对象作为value

private[scheduler] val stageIdToStage = new HashMap[Int, Stage]

spark在任务提交时才划分stage,我们可以根据任务提交调用栈链条来找到线索跟踪job提交流程我们追踪到handleJobSubmitted

finalStage = createResultStage(finalRDD, func, partitions, jobId, callSite)

我们具体看下createResultStage

  private def createResultStage(
      rdd: RDD[_],
      func: (TaskContext, Iterator[_]) => _,
      partitions: Array[Int],
      jobId: Int,
      callSite: CallSite): ResultStage = {
    ......
    //这里创建祖先stage,这说明stage是根据最终RDD,逐级往源头回溯创建stage,可以看到主要的创建过程就在这里
    val parents = getOrCreateParentStages(rdd, jobId)
    val id = nextStageId.getAndIncrement()
    val stage = new ResultStage(id, rdd, func, partitions, parents, jobId, callSite)
    stageIdToStage(id) = stage
    updateJobIdStageIdMaps(jobId, stage)
    stage
  }

接下来

// 返回 List[Stage] ,这一步操作将祖先stage全部创建完成
private def getOrCreateParentStages(rdd: RDD[_], firstJobId: Int): List[Stage] = {
// 这里也就容易理解是按照宽依赖为界限划分stage,在为RDD的祖先RDD生成stage时,直接获取祖先的shuffle依赖,如果没有
shuffle依赖,那整个job就一个stage
    getShuffleDependencies(rdd).map { shuffleDep =>
    // 某个宽依赖的stage可能已经存在,避免重复生成
      getOrCreateShuffleMapStage(shuffleDep, firstJobId)
    }.toList
  }
  
private def getOrCreateShuffleMapStage(
      shuffleDep: ShuffleDependency[_, _, _],
      firstJobId: Int): ShuffleMapStage = {
    shuffleIdToMapStage.get(shuffleDep.shuffleId) match {
      case Some(stage) =>
        stage

      case None =>
        // 为祖先shuffle依赖创建stage
        getMissingAncestorShuffleDependencies(shuffleDep.rdd).foreach { dep =>
          if (!shuffleIdToMapStage.contains(dep.shuffleId)) {
          // 创建ShuffleMapStage 这一stage生成宽依赖的分区 
            createShuffleMapStage(dep, firstJobId)
          }
        }
        // 为传入参数中给定的宽依赖创建stage
        createShuffleMapStage(shuffleDep, firstJobId)
    }
  }

然后我们再看下createShuffleMapStage

def createShuffleMapStage(shuffleDep: ShuffleDependency[_, _, _], jobId: Int): ShuffleMapStage = {
    val rdd = shuffleDep.rdd
    ......
    val numTasks = rdd.partitions.length
    // 关注这里,很明显开始重复创建stage过程了:最终RDD的生成stage--->最终RDD的祖先的宽依赖的生成stage--->
    再往谱系上游寻找其祖先的宽依赖,确定其宽依赖生成的stage--->直到没有宽依赖,祖先stage划分结束
    val parents = getOrCreateParentStages(rdd, jobId)
    val id = nextStageId.getAndIncrement()
    val stage = new ShuffleMapStage(
      id, rdd, numTasks, parents, jobId, rdd.creationSite, shuffleDep, mapOutputTracker)

    stageIdToStage(id) = stage
    shuffleIdToMapStage(shuffleDep.shuffleId) = stage
    updateJobIdStageIdMaps(jobId, stage)

    //这个地方对于后边说的首选位置有帮助,先提一下,重点在于mapOutputTracker
    if (!mapOutputTracker.containsShuffle(shuffleDep.shuffleId)) {
      // Kind of ugly: need to register RDDs with the cache and map output tracker here
      // since we can't do it in the RDD constructor because # of partitions is unknown
      logInfo("Registering RDD " + rdd.id + " (" + rdd.getCreationSite + ")")
      mapOutputTracker.registerShuffle(shuffleDep.shuffleId, rdd.partitions.length)
    }
    stage
  }

2、怎么决定任务运行的首选位置?

我们还是从任务提交的流程中来确认这一逻辑,定位到submitMissingTasks方法中

// 找到stage需要进行计算的分区
val partitionsToCompute: Seq[Int] = stage.findMissingPartitions()
......
// 可以看到不管是什么类型的stage,都通过getPreferredLocs(stage.rdd, p) 来获取该stage的分区id对应的主机地址
val taskIdToLocations: Map[Int, Seq[TaskLocation]] = try {
      stage match {
      //可见分区id也作为任务id
        case s: ShuffleMapStage =>
          partitionsToCompute.map { id => (id, getPreferredLocs(stage.rdd, id))}.toMap
        case s: ResultStage =>
          partitionsToCompute.map { id =>
            val p = s.partitions(id)
            (id, getPreferredLocs(stage.rdd, p))
          }.toMap
      }

private[spark]
  def getPreferredLocs(rdd: RDD[_], partition: Int): Seq[TaskLocation] = {
    getPreferredLocsInternal(rdd, partition, new HashSet)
  }

private def getPreferredLocsInternal(
      rdd: RDD[_],
      partition: Int,
      visited: HashSet[(RDD[_], Int)]): Seq[TaskLocation] = {
    // 如果分区已经知道在哪里了,不需要再重复去找一次
    // This avoids exponential path exploration.  SPARK-695
    if (!visited.add((rdd, partition))) {
      // Nil has already been returned for previously visited partitions.
      return Nil
    }
    // 如果该分区已经缓存过,直接返回缓存位置
    // 调用次数和分区数一样,即和task数量一致
    // 函数中判断存储级别,如果存储级别为None,说明未缓存直接返回
    // 否则通过blockId从blockManagerMaster获取分区所在的主机和executorId
    val cached = getCacheLocs(rdd)(partition)
    if (cached.nonEmpty) {
      return cached
    }
    // If the RDD has some placement preferences (as is the case for input RDDs), get those
    // 根据输入源的不同,有不同的子类实现,用来获取数据的首选位置,代码先考虑checkpoint位置
    val rddPrefs = rdd.preferredLocations(rdd.partitions(partition)).toList
    if (rddPrefs.nonEmpty) {
      return rddPrefs.map(TaskLocation(_))
    }

    // 对于窄依赖,选择第一个具有首选位置的分区,将其位置当做任务的首选位置
    // 源码中也有说明,理想情况下应该根据需要传送的大小来比较来选出任务的首选位置,却并未这么做
    rdd.dependencies.foreach {
      case n: NarrowDependency[_] =>
        for (inPart <- n.getParents(partition)) {
          val locs = getPreferredLocsInternal(n.rdd, inPart, visited)
          if (locs != Nil) {
            return locs
          }
        }
      case _ =>
    }
    Nil
  }

3、面向stage调度?

面向stage调度是说DAGScheduler只在stage的层面进行调度,而不负责具体task的调度运行。

submitJob --->
JobSubmitted事件Event--->
DAGSchedulerEventProcessLoop事件处理器调用onReceive--->
doOnReceive--->dagScheduler.handleJobSubmitted--->
submitStage(finalStage)--->submitMissingTasks(stage, jobId.get)--->
若任务数大于0执行调用
taskScheduler.submitTasks(new TaskSet( tasks.toArray, stage.id, stage.latestInfo.attemptNumber, jobId, properties))--->
若任务数为0,调用markStageAsFinished(stage, None) ,然后提交子stage:submitWaitingChildStages(stage)

可以看到,在DAGScheduler中,会将stage中的任务作为任务集TaskSet提交给TaskScheduler,具体TaskScheduler的实现负责任务集中每个task的调度。

4、DAGScheduler如何跟踪stage的输出?

DAGScheduler需要追踪stage的输出,确切的讲应该是ShuffleMapStage输出,以便进行最小开销调度

private[scheduler] def markMapStageJobsAsFinished(shuffleStage: ShuffleMapStage): Unit = {
    // Mark any map-stage jobs waiting on this stage as finished
    if (shuffleStage.isAvailable && shuffleStage.mapStageJobs.nonEmpty) {
      // 前边提到过mapOutputTracker,这是一个MapOutputTrackerMaster对象,这一操作获取了给定shuffle输出的所有统计值及
      mapStatus状态信息
      // 在创建ShuffleMapStage的时候,已经将shuffleID,shuffle分区等信息注册给了mapOutputTracker
      val stats = mapOutputTracker.getStatistics(shuffleStage.shuffleDep)
      for (job <- shuffleStage.mapStageJobs) {
        markMapStageJobAsFinished(job, stats)
      }
    }
  }

4、需要时重新提交stage

以下两种情况下DAGScheduler均要重新提交stage

private[scheduler] def handleTaskCompletion(event: CompletionEvent){
......
// 因shuffle output分区lost导致的拉取失败,需要重新提交上一个stage重新计算shuffle输出
case FetchFailed(bmAddress, shuffleId, mapId, _, failureMessage) =>
......
// 因shuffle任务失败,并且stage重试尝试次数未超过最大可连续重试值
case failure: TaskFailedReason if task.isBarrier =>
......
messageScheduler.schedule(new Runnable {
   override def run(): Unit = eventProcessLoop.post(ResubmitFailedStages)
}, DAGScheduler.RESUBMIT_TIMEOUT, TimeUnit.MILLISECONDS)
......
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值