SparkCore — stage划分算法源码分析

DAGscheduler中的stage划分源码分析

  在之前的文章中,已经分析了stage的划分算法,这里我们到源码里面去看划分算法是怎么实现的。
  首先找到提交Job的入口(从action操作开始,找到action操作的runJob -> dagScheduler.runJob -> submitJob -> eventProcessLoop.JobSubmitted -> handleJobSubmitted)handleJobSubmitted()这个方法,下面我们具体分析源码

 private[scheduler] def handleJobSubmitted(jobId: Int,
      finalRDD: RDD[_],
      func: (TaskContext, Iterator[_]) => _,
      partitions: Array[Int],
      callSite: CallSite,
      listener: JobListener,
      properties: Properties) {
    // 使用触发Job的最后一个RDD,创建finalStage,它的类型是ResultStage
    var finalStage: ResultStage = null
    try {
      // 创建finalStage,并且将stage加入DAGScheduler内部的缓存中
      finalStage = newResultStage(finalRDD, func, partitions, jobId, callSite)
    } catch {
      case e: Exception =>
        logWarning("Creating new stage failed due to exception - job: " + jobId, e)
        listener.jobFailed(e)
        return
    }

    // 用finalStage创建一个Job,里面封装了Job的一些信息(比如partition的数量,ResultStage和ShuffleMapStage是不一样的,这里在性能调优的时候再讲)
    val job = new ActiveJob(jobId, finalStage, callSite, listener, properties)
    // 清除RDD缓存
    clearCacheLocs()
    logInfo("Got job %s (%s) with %d output partitions".format(
      job.jobId, callSite.shortForm, partitions.length))
    logInfo("Final stage: " + finalStage + " (" + finalStage.name + ")")
    logInfo("Parents of final stage: " + finalStage.parents)
    logInfo("Missing parents: " + getMissingParentStages(finalStage))

    val jobSubmissionTime = clock.getTimeMillis()
    // 将Job加入内存缓存中
    jobIdToActiveJob(jobId) = job
    activeJobs += job
    finalStage.setActiveJob(job)
    val stageIds = jobIdToStageIds(jobId).toArray
    val stageInfos = stageIds.flatMap(id => stageIdToStage.get(id).map(_.latestInfo))
    listenerBus.post(
      SparkListenerJobStart(job.jobId, jobSubmissionTime, stageInfos, properties))

    // 提交finalStage
    // 这个方法会导致第一个stage被提交,并且其他stage,都放入了waitingStages里了。
    submitStage(finalStage)

    // 提交完第一个stage0后,剩余的stage,通过这个函数提交
    submitWaitingStages()
  }

  上面的代码中,首先使用触发Job的最后一个RDD,创建一个finalStage,这个stage是ResultStage(一个Job里面只有最后一个stage是ResultStage,其余的都是ShuffleMapStage),然后创建Job(里面包含了Job的一些信息),并将Job的相关信息放入缓存中,接着就创建了比较重要的submitStage(finalStage)方法,这个方法里面就包含了stage的划分和提交;而submitWaitingStages()则是提交剩余的stage,下面我们分析一下submitStage(finalStage)方法。

private def submitStage(stage: Stage) {
    // 获取JobId
    val jobId = activeJobForStage(stage)
    // job存在,开始进行任务分配
    if (jobId.isDefined) {
      logDebug("submitStage(" + stage + ")")
      // 这个stage还没有被提交过,或者正在运行,失败等
      if (!waitingStages(stage) && !runningStages(stage) && !failedStages(stage)) {
        // 使用getMissingParentStages()方法,获取这个stage的父stage
        val missing = getMissingParentStages(stage).sortBy(_.id)
        logDebug("missing: " + missing)
        // 这里会反复的递归调用,直到最初的stage,它没有父stage,此时就会提交第一个stage,
        // 也就是stage0
        if (missing.isEmpty) {
          logInfo("Submitting " + stage + " (" + stage.rdd + "), which has no missing parents")
          // 如果父stage没有父stage,那么就提交stage,这里的递归,就是stage划分算法的推动者。
          submitMissingTasks(stage, jobId.get)
        } else {
          for (parent <- missing) {
            // 递归调用submitStage()方法,去提交父Stage
            submitStage(parent)
          }
          // 并且将当前stage,放入waitingStages等待队列中
          waitingStages += stage
        }
      }
    } else {
      abortStage(stage, "No active job for stage " + stage.id, None)
    }
  }

  下面简单说是如何进行stage的划分的,看注释,首先就是根据当前这个stage找到它的父stage,假如父stage的RDD与当前stage的RDD是宽依赖的关系,那么就用这个宽依赖的RDD创建一个ShuffleMapStage,并返回;假如不存在宽依赖,那么就一直遍历下去,直到第一个RDD为止。我们看一下getMissingParentStages()方法。

 private def getMissingParentStages(stage: Stage): List[Stage] = {
	// 创建存储ShuffleMapStage的集合,以及已经遍历RDD的集合
    val missing = new HashSet[Stage]
    val visited = new HashSet[RDD[_]]
    // We are manually maintaining a stack here to prevent StackOverflowError
    // caused by recursively visiting
    // 创建待遍历RDD的栈,防止递归调用栈溢出
    val waitingForVisit = new Stack[RDD[_]]
    def visit(rdd: RDD[_]) {
      if (!visited(rdd)) {
        visited += rdd
        val rddHasUncachedPartitions = getCacheLocs(rdd).contains(Nil)
        if (rddHasUncachedPartitions) {
          // 遍历RDD的依赖
          for (dep <- rdd.dependencies) {
            dep match {
              // 如果是宽依赖,使用宽依赖的RDD,创建一个stage.
              // 默认最后一个stage是ResultStage,之前所有的stage都是ShuffleMapStage
              case shufDep: ShuffleDependency[_, _, _] =>
                val mapStage = getShuffleMapStage(shufDep, stage.firstJobId)
                if (!mapStage.isAvailable) {
                  missing += mapStage
                }
              // 如果是窄依赖,那么将RDD放入栈中
              case narrowDep: NarrowDependency[_] =>
                waitingForVisit.push(narrowDep.rdd)
            }
          }
        }
      }
    }
    // 首先往栈中推入stage最后的一个RDD
    waitingForVisit.push(stage.rdd)
    // 进行while循环
    while (waitingForVisit.nonEmpty) {
      // 对stage的最后一个RDD,调用内部函数visit()
      // 对窄依赖会一直调用,直到出现宽依赖,那么就创建一个stage,并返回
      visit(waitingForVisit.pop())
    }
    missing.toList
  }

  可以看到这个方法也是一个递归调用,为了防止栈溢出,使用了一个stack结构waitingForVisit,我们看内部函数visit()方法,它会遍历当前RDD的依赖,假如存在shuffle依赖,那么就创建一个ShuffleMapStage,并返回,否则就一直执行下去,直到没有父RDD为止,这也就意味着整个Job只创建了一个Stage(ResultStage)。
  下面接着看submitStage的源码,通过getMissingParentStages()获取当前Stage的父Stage,假如存在,就执行到下面这块代码,这块代码就是stage划分算法的推动者:

if (missing.isEmpty) {
          logInfo("Submitting " + stage + " (" + stage.rdd + "), which has no missing parents")
          // 如果父stage没有父stage,那么就提交stage,这里的递归,就是stage划分算法的推动者。
          submitMissingTasks(stage, jobId.get)
        } else {
          for (parent <- missing) {
            // 递归调用submitStage()方法,去提交父Stage
            submitStage(parent)
          }
          // 并且将当前stage,放入waitingStages等待队列中
          waitingStages += stage
        }

如果存在父Stage,那么遍历父Stage,并递归调用submitStage();我们以下面这幅经典的图来说明:
Stage划分
  如上面图所示,首先以最后一个RDDG创建finalStage(Stage3),接着通过**getMissingParentStages()**找它的父RDD,它有两个父RDD,分别是RDDF和RDDB。先看RDDB,它与RDDG是窄依赖,接着遍历RDDB的父RDD,也即RDDA,它们之间是groupByKey操作(发生了shuffle),是宽依赖,因此创建一个stage1,这个遍历结束;接着看RDDF,它与RDDG之间的操作是join,也发生了shuffle操作,因此创建了stage2;在遍历完两个父RDD之后,就返回getMissingParentStages()函数。这时候missing列表里面包含了stage1和stage2。
  下面判断missing是否为空,由于包含了stage1和stage2,因此不为空,就接着遍历。先遍历stage1,由于stage1没有依赖,因此它的missing是空,那么这里就调用submitMissingTasks()提交stage1;接着遍历到stage2,stage2的RDDF的父RDD中没有宽依赖,因此它的missing列表也为空,提交stage2。
  接着执行到waitingStages,它将finalStage,也就stage3加入这个等待队列中。到这里整个submitStage()就运行完成,只有最后一个stage被加入了等待队列中。
  submitStage()执行完成之后,接着执行submitWaitingStages(),提交加入等待队列的stage。
  整个stage的划分以及提交就结束了。以上就是stage的划分以及提交过程,主要分析了一下流程,至于细节这块,以后再慢慢研究,更新博客。
  总结一下,stage的划分是以shuffle为界,也即宽依赖,如果RDD之间发生了shuffle,那么就会以shuffle为界创建新的stage,依次内推。而stage的提交是递归提交,最先创建的stage,会最后提交,这刚好符合RDD的处理流程的先后顺序。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值