Spark Core DAG生成源碼分析

前提回顧

  之前分析了Spark Streaming關於DAG的生成,對於Spark Streaming而言,先是通過自己代碼中的各種transform方法來構造各個DStream之間的關聯關係,然後再通過最後調用action操作的算子處進行回溯,action算子操作的DStream作爲outputStream存儲到DStreamGraph中的數組中,回溯過程中會找到數據來源處的DStream,這個DStream作爲元素存儲到DStreamGraph中的inputStream數組中。通過以上兩個inputStreamoutputStream數組成功存儲了源頭和結尾的DStream,又依據之前構造的各個DStream之間的關聯,從而成功的構造完畢了DAG關係圖。
  回顧完Spark Streaming中DAG的生成流程,理論來說RDDDAG生成邏輯應該和DStream是類似的,我們直接深入代碼來驗證我們的想法。

transform算子分析

  顯然和DStream類似,RDD中肯定也是通過各種transform操作來提前構造好各個RDD之間關係的,爲了驗證這個想法,需要查看各個算子的代碼,先看用的最多的map算子代碼如下:

> rdd = rdd0.map(...)

--> RDD.scala
def map[U: ClassTag](f: T => U): RDD[U] = withScope {
    val cleanF = sc.clean(f)
    new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))
  }

--> MapPartitionsRDD.scala
private[spark] class MapPartitionsRDD[U: ClassTag, T: ClassTag](
    var prev: RDD[T],
    f: (TaskContext, Int, Iterator[T]) => Iterator[U],  // (TaskContext, partition index, iterator)
    preservesPartitioning: Boolean = false,
    isOrderSensitive: Boolean = false)
  extends RDD[U](prev) {

  override val partitioner = if (preservesPartitioning) firstParent[T].partitioner else None

  override def getPartitions: Array[Partition] = firstParent[T].partitions

  override def compute(split: Partition, context: TaskContext): Iterator[U] =
    f(context, split.index, firstParent[T].iterator(split, context))

  override def clearDependencies() {
    super.clearDependencies()
    prev = null
  }

  override protected def getOutputDeterministicLevel = {
    if (isOrderSensitive && prev.outputDeterministicLevel == DeterministicLevel.UNORDERED) {
      DeterministicLevel.INDETERMINATE
    } else {
      super.getOutputDeterministicLevel
    }
  }
}

  以上代碼分爲三部分,實例用法、底層抽象類RDD.scalamap方法的實現以及map返回類MapPartitionsRDD的實現。可以很明顯的看到在調用map方法後,生成新的RDD的時候會傳入之前的rdd用於構造新的rdd,也就是說和dstream完全類似就是通過transform來關聯不同rdd之間的關係。這樣就可以很好的理解流程就是通過各種transform來關聯rdd,然後再通過action來觸發其它操作,這裏說的其它操作應該包括有依據構造的rdd關係來構造dag、生成job、生成stage等等。

action算子分析

  爲了驗證我們的想法,那麼我們就繼續深入代碼,我們挑選collect算子來作爲入口,來通過collect算子入口看action算子內部主要做了哪些事情。

def collect(): Array[T] = withScope {
    val results = sc.runJob(this, (iter: Iterator[T]) => iter.toArray)
    Array.concat(results: _*)
  }

  如上爲collect的實現,可以看到主要就是調用了SparkContextrunJob方法,那麼很顯然了,這裏就是深入源碼內部的入口。我們來繼續看runJob方法的實現:

def runJob[T, U: ClassTag](
      rdd: RDD[T],
      func: (TaskContext, Iterator[T]) => U,
      partitions: Seq[Int],
      resultHandler: (Int, U) => Unit): Unit = {
    if (stopped.get()) {
      throw new IllegalStateException("SparkContext has been shutdown")
    }
    val callSite = getCallSite
    val cleanedFunc = clean(func)
    logInfo("Starting job: " + callSite.shortForm)
    if (conf.getBoolean("spark.logLineage", false)) {
      logInfo("RDD's recursive dependencies:\n" + rdd.toDebugString)
    }
    dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, resultHandler, localProperties.get)
    progressBar.foreach(_.finishAll())
    rdd.doCheckpoint()
  }

  以上代碼省略了各種重載方法的調用鏈直接到最後的實現方法處,這裏實現也很簡單,主要就是去調用dagSchedulerrunJob方法,我們來繼續查看dagSchedulerrunJob的實現:

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
    val waiter = submitJob(rdd, func, partitions, callSite, resultHandler, properties)
    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
    }
  }

  這裏主要也是爲了調用submitJob,代碼如下:

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] = {

    val jobId = nextJobId.getAndIncrement()
    if (partitions.size == 0) {
      // 任務分區數爲0則直接返回。
      return new JobWaiter[U](this, jobId, 0, resultHandler)
    }

    val func2 = func.asInstanceOf[(TaskContext, Iterator[_]) => _]
     // job任務監聽器,任務完成後的數據交給resultHandle來處理
    val waiter = new JobWaiter(this, jobId, partitions.size, resultHandler)
    // 發佈消息
    eventProcessLoop.post(JobSubmitted(
      jobId, rdd, func2, partitions.toArray, callSite, waiter,
      SerializationUtils.clone(properties)))
    waiter
  }

  如上submitJob主要就是創建一個job監聽器,然後和任務一起發佈到eventProcessLoop中,然後會有對應的事件響應處理,對於任務提交JobSubmitted類型事件的處理方法爲dagScheduler.handleJobSubmitted方法,具體實現如下:

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 {
      // 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)
    } catch {
      case e: Exception =>
        logWarning("Creating new stage failed due to exception - job: " + jobId, e)
        listener.jobFailed(e)
        return
    }

    // 獲取這個stage觸發的job
    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))
    submitStage(finalStage)
  }

  這裏可以發現此處會有一個變量叫finalStage,這個可以理解就是最後一個stage,我們再來思考我們是如何一路走到這裏的,就是從action算子觸發一路走到這裏,所以很顯然可以知道這個finalStage就是DAG中進行stage劃分時最後的一個stage。因爲action算子就已經是觸發計算任務的地方,確實就應該是一個DAG的末尾了。
  以上分析,可以看出這裏應該也是和dstream類似,是通過rddaction算子來進行回溯的方式找到源頭rdd,中間會觸發一些事件暫時用於他處這裏不進行分析,主要圍繞stage的劃分和啓動流程,那麼對應的就直接看到最後的submitStage方法,代碼如下:

private def submitStage(stage: Stage) {
    val jobId = activeJobForStage(stage)
    if (jobId.isDefined) { // 之前已經定義過了jobId
      logDebug("submitStage(" + stage + ")")
      // 本次提交的stage狀態不是等待、運行、失敗狀態,說明應該是還未正式提交啓動運行的
      if (!waitingStages(stage) && !runningStages(stage) && !failedStages(stage)) {
        // 獲取當前stage所依賴的且一樣沒有提交的stage,這個過程就是一個回溯的過程
        val missing = getMissingParentStages(stage).sortBy(_.id)
        logDebug("missing: " + missing)
        if (missing.isEmpty) {
          logInfo("Submitting " + stage + " (" + stage.rdd + "), which has no missing parents")
          submitMissingTasks(stage, jobId.get)
        } else {
          for (parent <- missing) {
            submitStage(parent)
          }
          waitingStages += stage
        }
      }
    } else {
      abortStage(stage, "No active job for stage " + stage.id, None)
    }
  }

  到上面這一步的時候,注意最開始調用submitStage這個方法的時候,傳過來的stage是由action觸發得到的,是finalStage。這裏需要知道的一點就是,每個stage剛開始創建出來的時候都只是對應一個rdd,然後在進行回溯的時候,其實是從最開始創建好的DAG圖譜中最後的finalStage只是一個調用action操作的rdd,通過這個rdd來回溯它所依賴的父類rdd,然後判斷和所依賴的rdd的依賴關係是寬依賴還是窄依賴,如果是窄依賴(即分區之間一對一的依賴)關係,則把依賴的rdd直接吸收到本次的stage中;反之如果是寬依賴,則會依據所依賴的rdd生成一個新的stage,然後這兩個stage之間建立聯繫。如此往復一直沿着之前構建的rdd關係鏈尋找所依賴的rdd和被依賴rdd之間的關係來創建stage,最終會把所有的rdd劃分爲若干個stage

以上介紹的是stage劃分的邏輯,還可以補充的一點是關於task以及job的產生,對於job而言,對應的就是一個action算子,每一個action操作都會觸發任務執行得到結果,對應會有一個小的DAG關係圖,這個action所觸發計算所涉及的計算任務就是一個job。而具體到劃分完stage之後,每個stage所關聯的rdd中會有若干個partition,每個partition計算都會產生一個task,以上所有的stagejob最終都是爲了最終的計算,而最終的計算單位就是以一個一個的partition作爲單位得到一個交給Executor執行的task

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章