Spark源码 -- Stage的切分

上一节我们介绍了spark作业在集群中通过sparkSubmit脚本提交job,但job提交后作业是如何运行的,这节我们详细来探讨一下作业的运行和stage的切分,下面我们通过一个wordCount代码来看看作业是如何触发的。

import org.apache.spark.{SparkConf, SparkContext}

object WordCount {
  def main(args: Array[String]): Unit = {

    val sparkConf = new SparkConf()
      .setMaster("local[*]")
      .setAppName("wordCount")

    val spark = SparkContext.getOrCreate(sparkConf)

    val sourceRDD = spark.textFile("src/main/resources/word2.txt")

    val flatMapRDD = sourceRDD.flatMap(line => line.split(" "))

    val mapRDD = flatMapRDD.map((_, 1))

    val sortRDD = mapRDD.sortByKey()

    val reduceRDD = sortRDD.reduceByKey(_ + _)
    //行动算子会触发job的执行
    reduceRDD.count()


  }
}

我们都知道spark算子分为转换算子和行动算子,spark代码只有在遇到行动算子的时候才会触发代码的执行,这里的count算子就是行动算子。我们点击count算子进入算子内部

1.SparkContext

可以看到count算子底层调用sc.runJob方法,这里的sc就是sparkContext.然后点击runJob方法,底层调用了异构方法runjob

 内部调用了好几个异构方法,最终通过调用dagScheduler.runJob方法

2.DAGScheduler

1.runJob方法

此方法中通过submitJob方法提交job,并且会异步获取job的提交结果,并执行对应操作

 def runJob[T, U](
      rdd: RDD[T],
      func: (TaskContext, Iterator[T]) => U,
      partitions: Seq[Int],
      callSite: CallSite,
      resultHandler: (Int, U) => Unit,
      properties: Properties): Unit = {
    val start = System.nanoTime
    //通过submitJob方法提交job
    val waiter = submitJob(rdd, func, partitions, callSite, resultHandler, properties)
    //异步获取job的提交结果,根据结果执行对应操作
    ThreadUtils.awaitReady(waiter.completionFuture, Duration.Inf)
    waiter.completionFuture.value.get match {
      case scala.util.Success(_) =>
        logInfo("Job %d finished: %s, took %f s".format
          (waiter.jobId, callSite.shortForm, (System.nanoTime - start) / 1e9))
      case scala.util.Failure(exception) =>
        logInfo("Job %d failed: %s, took %f s".format
          (waiter.jobId, callSite.shortForm, (System.nanoTime - start) / 1e9))
        // SPARK-8644: Include user stack trace in exceptions coming from DAGScheduler.
        val callerStackTrace = Thread.currentThread().getStackTrace.tail
        exception.setStackTrace(exception.getStackTrace ++ callerStackTrace)
        throw exception
    }
  }

 2.submitJob方法

该方法中主要做了两件事:

1.创建jobWaiter,用于异步获取job的提交结果

2.往eventProcessLoop队列中发送一条jobSubmitted消息

def submitJob[T, U](
      rdd: RDD[T],
      func: (TaskContext, Iterator[T]) => U,
      partitions: Seq[Int],
      callSite: CallSite,
      resultHandler: (Int, U) => Unit,
      properties: Properties): JobWaiter[U] = {
    // Check to make sure we are not launching a task on a partition that does not exist.
    
    //获取该rdd最大分区数
    val maxPartitions = rdd.partitions.length
    partitions.find(p => p >= maxPartitions || p < 0).foreach { p =>
      throw new IllegalArgumentException(
        "Attempting to access a non-existent partition: " + p + ". " +
          "Total number of partitions: " + maxPartitions)
    }
    
    //获取JobJd
    val jobId = nextJobId.getAndIncrement()
    if (partitions.size == 0) {
      // Return immediately if the job is running 0 tasks
      return new JobWaiter[U](this, jobId, 0, resultHandler)
    }

    assert(partitions.size > 0)
    val func2 = func.asInstanceOf[(TaskContext, Iterator[_]) => _]
    //创建jobWaiter
    val waiter = new JobWaiter(this, jobId, partitions.size, resultHandler)
    //往队列中发送一条jobSubmitted消息
    eventProcessLoop.post(JobSubmitted(
      jobId, rdd, func2, partitions.toArray, callSite, waiter,
      SerializationUtils.clone(properties)))
    waiter
  }

这里的eventProcessLoop是一个基于事件的消息队列,该队列的具体类型是DAGSchedulerEventProcessLoop,该消息队列启动后会去拉取收到的消息,并调用OnReceive方法去处理 

 我们点进DAGSchedulerEventProcessLoop这个类中,去找一下他的OnReceive方法

可以看到OnReceive方法中调用了doOnReceive方法 ,这个方法中会根据不同消息类型调用dagScheduler的不同方法,jobSubmitted类型的消息会调用它的handleJobSubmitted方法

private def doOnReceive(event: DAGSchedulerEvent): Unit = event match {
    case JobSubmitted(jobId, rdd, func, partitions, callSite, listener, properties) =>
      dagScheduler.handleJobSubmitted(jobId, rdd, func, partitions, callSite, listener, properties)

    case MapStageSubmitted(jobId, dependency, callSite, listener, properties) =>
      dagScheduler.handleMapStageSubmitted(jobId, dependency, callSite, listener, properties)

    case StageCancelled(stageId, reason) =>
      dagScheduler.handleStageCancellation(stageId, reason)

    case JobCancelled(jobId, reason) =>
      dagScheduler.handleJobCancellation(jobId, reason)

    case JobGroupCancelled(groupId) =>
      dagScheduler.handleJobGroupCancelled(groupId)

    case AllJobsCancelled =>
      dagScheduler.doCancelAllJobs()

    case ExecutorAdded(execId, host) =>
      dagScheduler.handleExecutorAdded(execId, host)

    case ExecutorLost(execId, reason) =>
      val workerLost = reason match {
        case SlaveLost(_, true) => true
        case _ => false
      }
      dagScheduler.handleExecutorLost(execId, workerLost)

    case WorkerRemoved(workerId, host, message) =>
      dagScheduler.handleWorkerRemoved(workerId, host, message)

    case BeginEvent(task, taskInfo) =>
      dagScheduler.handleBeginEvent(task, taskInfo)

    case SpeculativeTaskSubmitted(task) =>
      dagScheduler.handleSpeculativeTaskSubmitted(task)

    case GettingResultEvent(taskInfo) =>
      dagScheduler.handleGetTaskResult(taskInfo)

    case completion: CompletionEvent =>
      dagScheduler.handleTaskCompletion(completion)

    case TaskSetFailed(taskSet, reason, exception) =>
      dagScheduler.handleTaskSetFailed(taskSet, reason, exception)

    case ResubmitFailedStages =>
      dagScheduler.resubmitFailedStages()
  }

3.handleJobSubmitted方法

一个Job有多个Stage,最后一个Stage叫做:ResultStage,前面的就叫做:ShuffleMapStage
ShuffleMapStage 执行完了之后,数据可以被持久化或者shuffle给下一个stage
resultStage 执行完了之后,就按照程序的要求,把数据持久化,除非打印输出

该方法中主要做了一下几件事:

        1.创建resultStage,stage分为resultStage和shuffleMapStage

        2.提交stage

 /**
     * TODO_MA 马中华 https://blog.csdn.net/zhongqi2513
     *   注释: RDD DAG划分Stages:Stage的划分是从最后一个Stage开始逆推的,每遇到一个宽依赖处,就分裂成另外一个Stage
     *   依此类推直到Stage划分完毕为止。并且,只有最后一个Stage的类型是ResultStage类型。
     *   注意Dataset、DataFrame、sparkSession.sql("select ...")经过catalyst代码解析会将代码转化为RDD
     *
     *   做了两件最重要的事情:
     *   1、stage切分
     *   2、stage提交
     */
    private[scheduler] def handleJobSubmitted(jobId: Int, finalRDD: RDD[_], func: (TaskContext, Iterator[_]) => _, partitions: Array[Int],
        callSite: CallSite, listener: JobListener, properties: Properties) {
        var finalStage: ResultStage = null
        try {
            /**
             * TODO_MA 马中华 https://blog.csdn.net/zhongqi2513
             *   注释: Stage 切分
             *   这个 finalRDD 就是 rdd链条中的最后一个 RDD,也就是触发 sc.runJob() 方法执行的 RDD
             *   必然是针对某个 RDD 调用了一个 action 算子才触发执行的,则该 RDD 就是 finallRDD
             *
             *   stage切分的核心方法:createResultStage
             *   返回的是一个 ResultStage对象,但是这个对象中,会包含他的所有的 父stage
             *   这样做的最大好处:容错!
             *   spark DAG引擎:血脉关系  RDD之间的依赖被构建成了 dependency ,也被构建成了 stage 之间的依赖关系
             */
            // New stage creation may throw an exception if, for example, jobs are run on a
            // HadoopRDD whose underlying HDFS files have been deleted.
            finalStage = createResultStage(finalRDD, func, partitions, jobId, callSite)
            
            /**
             * TODO_MA 马中华 https://blog.csdn.net/zhongqi2513
             *   注释:
             *   注意几对概念:
             *   1、ShuffleMapStage  +  ResultStage
             *   2、ShuffleMapTask + ResultTask
             *   3、ShuffleDependency + NarrowDependency
             */
            
            /**
             * TODO_MA 马中华 https://blog.csdn.net/zhongqi2513
             *   注释:先看这四个概念:
             *   1、Application
             *   2、Job
             *   3、Stage
             *      一个Job有多个Stage,最后一个Stage叫做:ResultStage,前面的就叫做:ShuffleMapStage
             *          ShuffleMapStage 执行完了之后,数据可以被持久化或者shuffle给下一个stage
             *          ResultStage 执行完了之后,就按照程序的要求,把数据持久化,除非打印输出
             *   4、Task
             */
            
        } catch {
            case e: BarrierJobSlotsNumberCheckFailed => logWarning(
                s"The job $jobId requires to run a barrier stage that requires more slots " + "than the total number of slots in the cluster currently.")
                // If jobId doesn't exist in the map, Scala coverts its value null to 0: Int automatically.
                val numCheckFailures = barrierJobIdToNumTasksCheckFailures.compute(jobId, new BiFunction[Int, Int, Int] {
                    override def apply(key: Int, value: Int): Int = value + 1
                })
                if (numCheckFailures <= maxFailureNumTasksCheck) {
                    messageScheduler.schedule(new Runnable {
                        override def run(): Unit = eventProcessLoop
                            .post(JobSubmitted(jobId, finalRDD, func, partitions, callSite, listener, properties))
                    }, timeIntervalNumTasksCheck, TimeUnit.SECONDS)
                    return
                } else {
                    // Job failed, clear internal data.
                    barrierJobIdToNumTasksCheckFailures.remove(jobId)
                    listener.jobFailed(e)
                    return
                }
            case e: Exception => logWarning("Creating new stage failed due to exception - job: " + jobId, e)
                listener.jobFailed(e)
                return
        }
        
        // Job submitted, clear internal data.
        barrierJobIdToNumTasksCheckFailures.remove(jobId)
        
        val job = new ActiveJob(jobId, finalStage, callSite, listener, properties)
        
        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()
        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))
        
        /**
         * TODO_MA 马中华 https://blog.csdn.net/zhongqi2513
         *   注释: 提交 Stage, 包含了所有的父stage,这里面也是通过递归来提交执行 stage
         */
        submitStage(finalStage)
    }

4.createResultStage方法

该方法中主要功能:

        1.获取父stage,没有shuffle则会返回一个空list

        2.创建resultStage并返回

 /**
     * TODO_MA Create a ResultStage associated with the provided jobId.
     *  进行Stage切分的详细方法实现
     */
    private def createResultStage(rdd: RDD[_], func: (TaskContext, Iterator[_]) => _, partitions: Array[Int], jobId: Int,
        callSite: CallSite): ResultStage = {
        checkBarrierStageWithDynamicAllocation(rdd)
        checkBarrierStageWithNumSlots(rdd)
        checkBarrierStageWithRDDChainPattern(rdd, partitions.toSet.size)
        
        // TODO_MA 注释:获得父stage,若没有shuffle则返回空List
        // TODO_MA 注释:获取当前Stage的parent Stage,这个方法是划分Stage的核心实现
        val parents = getOrCreateParentStages(rdd, jobId)
        
        // TODO_MA 注释: finalStage=resultStage 的 stageID
        val id = nextStageId.getAndIncrement()
        
        // TODO_MA 注释:创建当前最后的ResultStage
        // TODO_MA 注释:parents 所有的 父 stage
        val stage = new ResultStage(id, rdd, func, partitions, parents, jobId, callSite)
        
        // TODO_MA 注释:将 ResultStage 与 stageId 相关联, 保存在 map 中
        // TODO_MA 注释:stageIdToStage = new HashMap[Int, Stage]
        stageIdToStage(id) = stage
        
        // TODO_MA 注释:更新该job中包含的stage
        updateJobIdStageIdMaps(jobId, stage)
        
        // TODO_MA 注释:返回ResultStage
        stage
    }

5.getOrCreateParentStages方法

主要功能:

        1.获取该rdd的shuffle依赖

        2.获取或者创建shuffleMapStage

private def getOrCreateParentStages(rdd: RDD[_], firstJobId: Int): List[Stage] = {
        
        /**
         * TODO_MA 马中华 https://blog.csdn.net/zhongqi2513
         *   注释:
         *   1、遍历 RDD 的依赖关系,找到第一个 ShuffleDependency (可能多个,也可能没有)然后放入 HashSet 并返回
         *   2、如果获取不到 ShuffleDependency,逻辑在此终止返回空 list
         *   3、里面会创建当前 ShuffleDependency 的所有父 ShuffleMapStage
         */
        /**
         * TODO_MA 马中华 https://blog.csdn.net/zhongqi2513
         *   注释:
         *   1、rdd 就是  finalRDD
         *   2、getShuffleDependencies 获取当前 job 的所有 shuffleDependencies
         *   3、调用 map 函数,给每一个 ShuffleDepedency 构建 ShuffleMapStage
         *   4、构建 Stage 的方法是:getOrCreateShuffleMapStage
         *   5、map 的 参数函数的 参数 shuffleDep 是一个宽依赖
         */
        getShuffleDependencies(rdd).map { shuffleDep => getOrCreateShuffleMapStage(shuffleDep, firstJobId) }.toList
    }

6.getShuffleDepedendices方法

主要功能:

        1.创建一个栈结构,将finalRDD压入栈中

        2.取出栈中的第一个元素注册登记

        3.获取该rdd的血缘关系,如果是宽依赖则放到parents集合中,如果是窄依赖则压入栈中

        4.继续遍历栈中的元素,直至为空,使用深度优先遍历算法,返回parents

 * TODO_MA 注释:采用的是深度优先遍历找到 Action 算子的父依赖中的宽依赖
     * TODO_MA 注释:获得单个rdd的所有宽依赖关系(一般只有一个宽依赖;像CoGroupedRDD有多个)
     */
    private[scheduler] def getShuffleDependencies(rdd: RDD[_]): HashSet[ShuffleDependency[_, _, _]] = {
        
        // TODO_MA 注释:用来存放依赖
        val parents = new HashSet[ShuffleDependency[_, _, _]]
        
        // TODO_MA 注释:遍历过的RDD放入这个里面
        val visited = new HashSet[RDD[_]]
        
        // TODO_MA 注释:创建一个待遍历RDD的栈结构:先进后出的栈
        val waitingForVisit = new ArrayStack[RDD[_]]
        
        // TODO_MA 注释:压入 FinalRDD
        waitingForVisit.push(rdd)
        
        /**
         * TODO_MA 马中华 https://blog.csdn.net/zhongqi2513
         *   注释:循环遍历这个栈结构
         */
        while (waitingForVisit.nonEmpty) {
            // TODO_MA 注释: 取出栈顶的第一个RDD
            val toVisit = waitingForVisit.pop()
            
            // TODO_MA 注释:如果RDD没有被遍历过,则执行if内部的代码
            if (!visited(toVisit)) {
                // TODO_MA 注释:先登记
                visited += toVisit
                
                /**
                 * TODO_MA 马中华 https://blog.csdn.net/zhongqi2513
                 *   注释:
                 *   1、rdd.dependencies:通过 rdd 获得依赖关系
                 *   2、dependency.rdd:通过依赖关系获得 rdd
                 */
                // TODO_MA 注释:得到依赖,我们知道依赖中存放的有父RDD的对象
                toVisit.dependencies.foreach {
                    // TODO_MA 注释:如果这个依赖是shuffle依赖,则放入返回队列中
                    case shuffleDep: ShuffleDependency[_, _, _] => parents += shuffleDep
                    
                    // TODO_MA 注释:如果不是shuffle依赖,把其父RDD压入待访问栈中,从而进行循环
                    case dependency => waitingForVisit.push(dependency.rdd)
                }
            }
        }
        parents
    }

7.getOrCreateShuffleMapStage方法

主要功能:       

        1.根据shuffleDep的shuffleId获取stage,有的话就获取该stage

        2.通过该宽依赖的血缘关系获取他的祖先shuffleDep

        3.创建shuffleMapStage

/**
     * TODO_MA 如果 shuffleIdToMapStage 中存在 shuffle,则获取 shuffle map stage。
     *  否则,如果 shuffle map stage 不存在,该方法将创建 shuffle map stage
     *  以及任何丢失的 parent shuffle map stage。
     *
     *  第一次来到这里传进来的是左到右首个shuffleDep,没有父stage,
     *  shuffleIdToMapStage也没有记录,会调 getMissingAncestorShuffleDependencies 并创建 stage
     *  之后进来可直接根据 shuffleIdToMapStage 提取到 ShuffleMapStage
     *
     * Gets a shuffle map stage if one exists in shuffleIdToMapStage.
     * Otherwise, if the shuffle map stage doesn't already exist,
     * this method will create the shuffle map stage in addition to any missing ancestor shuffle map stages.
     */
    private def getOrCreateShuffleMapStage(shuffleDep: ShuffleDependency[_, _, _], firstJobId: Int): ShuffleMapStage = {
        
        // TODO_MA 注释: 根据每个 ShuffleDep 的 shuffleID 来获取 Stage 对象
        shuffleIdToMapStage.get(shuffleDep.shuffleId) match {
            // TODO_MA 注释:如果有,则直接返回
            case Some(stage) => stage
            
            // TODO_MA 注释:如果还有 ShuffleDependency 没有构建 ShuffleMapStage 则创建一个
                // TODO_MA 注释:如果这个stage没有构建
                // TODO_MA 注释:第一件事:先把这个stage的所有父stage都构建出来
                // TODO_MA 注释:然后再次构建当前stage
            case None =>
                
                // TODO_MA 注释:给当前 stage 的所有 父stage 创建 ShuffleMapStage
                // TODO_MA 注释:举例:C <---B <-- A
                // Create stages for all missing ancestor shuffle dependencies.
                getMissingAncestorShuffleDependencies(shuffleDep.rdd)
                    .foreach { dep => // Even though getMissingAncestorShuffleDependencies only returns shuffle dependencies
                        // that were not already in shuffleIdToMapStage, it's possible that by the time we
                        // get to a particular dependency in the foreach loop, it's been added to
                        // shuffleIdToMapStage by the stage creation process for an earlier dependency. See
                        // SPARK-13902 for more information.
                        if (!shuffleIdToMapStage.contains(dep.shuffleId)) {
                            createShuffleMapStage(dep, firstJobId)
                        }
                    }
                
                /**
                 * TODO_MA 马中华 https://blog.csdn.net/zhongqi2513
                 *   注释:为指定的 ShuffleDependency 创建一个 ShuffleMapStage
                 */
                // Finally, create a stage for the given shuffle dependency.
                createShuffleMapStage(shuffleDep, firstJobId)
        }
    }
    

8.createShuffleMapStage方法

主要功能:

        1.获取它的parentStage

        2.创建shuffleMapStage并返回

  /**
     * TODO_MA 创建一个ShuffleMapStage,它生成给定的shuffle依赖项的分区。
     *  如果先前运行的stage生成了相同的shuffle 数据,则此函数将复制先前shuffle 中仍然可用的输出位置,以避免不必要地重新生成数据。
     *
     * Creates a ShuffleMapStage that generates the given shuffle dependency's partitions. If a
     * previously run stage generated the same shuffle data, this function will copy the output
     * locations that are still available from the previous shuffle to avoid unnecessarily regenerating data.
     */
    def createShuffleMapStage(shuffleDep: ShuffleDependency[_, _, _], jobId: Int): ShuffleMapStage = {
        val rdd = shuffleDep.rdd
        checkBarrierStageWithDynamicAllocation(rdd)
        checkBarrierStageWithNumSlots(rdd)
        checkBarrierStageWithRDDChainPattern(rdd, rdd.getNumPartitions)
        
        val numTasks = rdd.partitions.length
        val parents = getOrCreateParentStages(rdd, jobId)
        
        // TODO_MA 注释: 用来指定一个Stage的ID
        // TODO_MA 注释: 最先构建的stage 他的key=stageID就是最小的
        val id = nextStageId.getAndIncrement()
        
        // TODO_MA 注释:生成一个 ShuffleMapStage
        val stage = new ShuffleMapStage(id, rdd, numTasks, parents, jobId, rdd.creationSite, shuffleDep, mapOutputTracker)
        
        // TODO_MA 注释: 添加
        stageIdToStage(id) = stage
        shuffleIdToMapStage(shuffleDep.shuffleId) = stage
        
        updateJobIdStageIdMaps(jobId, stage)
        
        /**
         * TODO_MA 马中华 https://blog.csdn.net/zhongqi2513
         *   注释: 把 shuffle 信息注册到 Driver上的 MapOutputTrackerMaster 的 shuffleStatuses
         */
        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(s"Registering RDD ${rdd.id} (${rdd.getCreationSite}) as input to " + s"shuffle ${shuffleDep.shuffleId}")
            
            /**
             * TODO_MA 马中华 https://blog.csdn.net/zhongqi2513
             *   注释:
             *   1、把 Shuffle 信息注册到自己 Driver 的 MapOutputTrackerMaster 上,
             *   2、生成的是 shuffleId 和 ShuffleStatus 的映射关系
             *   3、在后面提交 Job 的时候还会根据它来的验证 map stage 是否已经准备好
             */
            mapOutputTracker.registerShuffle(shuffleDep.shuffleId, rdd.partitions.length)
        }
        stage
    }

至此,spark的stage切分源码就看完啦,有兴趣的伙伴可以去debug看一下源码的具体调用过程和顺序。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值