一文搞定Spark的DAG調度器(DAGScheduler)

1. DAG定義

RDD DAG還 構建了基於數據流之上的操作算子流, 即RDD的各個分區的數據總共會經過哪些 Transformation和 Action這兩種類型的一系列操作的調度運行, 從而RDD先被Trans­formation操作轉換爲新的RDD, 然後被Action操作 將結果反饋到Driver或存儲到外部存儲系統上 。
上面提到的一系列操作的調度運行其實是DAG提交給DAGScheduler來解析完成的。 DAGScheduler是面向Stage的高層級的調度器,DAGScheduler把DAG拆分成很多Tasks,每組Tasks都是一個Stage,解析時是以Shuffle爲邊界反向解析構建Stage,每當遇到Shuffle就會產生新的Stage,然後以一個個TaskSet(每個Stage封裝一個TaskSet)的形式提交給底層調度器TaskScheduler。另外, DAGScheduler需要記錄哪些RDD被存入磁盤等物化動作, 同時要尋求Task的最優化調度, 如在Stage 內部數據的本地性等。 DAGScheduler還需要監視因爲Shuffle跨結點輸出可能導致的失敗, 如果發現這個Stage失敗, 可能就要重新提交該Stage 。由此可見,爲了更好地理解Spark高層調度器DAGScheduler,我們需要了解一些基本概念:

  1. RDD:(Resilient Distributed Datasets, 彈性分佈式數據集)是分佈式內存的一個抽象概念,是一種高度受限的共享內存模型,即RDD是隻讀的記錄分區的集合, 能橫跨集羣的所有結點進行並行計算, 是一種基於工作集的應用抽象。
  2. Application:application(應用)其實就是用spark-submit提交的程序。比方說spark examples中的計算pi的SparkPi。一個application通常包含三部分:從數據源(比方說HDFS)取數據形成RDD,通過RDD的transformation和action進行計算,將結果輸出到console或者外部存儲(比方說collect收集輸出到console)。
  3. Driver:Spark中的driver感覺其實和yarn中Application Master的功能相類似。主要完成任務的調度以及和executor和cluster manager進行協調。有client和cluster聯衆模式。client模式driver在任務提交的機器上運行,而cluster模式會隨機選擇機器中的一臺機器啓動driver。從spark官網截圖的一張圖可以大致瞭解driver的功能。
  4. Job:Spark中的Job和MR中Job不一樣不一樣。MR中Job主要是Map或者Reduce Job。而Spark的Job其實很好區別,一個action算子就算一個Job,比方說count,first等。
  5. Stage:一個Joh需要拆分成多組任務來 完成 , 每組任務由Stage封裝。 跟一個Joh的所有涉及的 PartitionRDD類似, Stage之間也有依賴關係。
  6. TaskSet: 一組任務就是一個TaskSet, 對應一個Stage。 其中, 一個TaskSet的所有Task之間沒有Shuffle 依賴, 因 此互相之間可以並行運行。
  7. Task: 一個獨立的工作單元, 由Driver發送到Executor 上去執行。 通常情況下, 一個Task處理 RDD的一個Partition的數據 。根據 Task返回類型的不同, Task又分爲ShuffleMapTask和ResultTask。

2. DAG實例化

在Spark 源代碼中, DAGScheduler是在整個Spark Application的入口即 SparkContext中聲明並實例化的。在實例化DAGScheduler之前,巳經實例化了SchedulerBackend和底層調度器 TaskScheduler, 而SchedulerBackend和TaskScheduler是通過SparkContaxt的方法createTask­SchedulerSpark 實例化的。DAGScheduler在提交TaskSet給底層調度器時是面TaskScheduler接口的,這符合面向對象中依賴抽象而不依賴具體實現的原則,帶來底層資源調度器的可插拔性,使得Spark可以運行在衆多資源部署模式上。
SparkContext的完整源代碼可以看博客,在這裏只給出與DAGScheduler相關的核心代碼介紹:

 @volatile private var _dagScheduler: DAGScheduler = _//DAG調度器
  ...
   private[spark] def dagScheduler: DAGScheduler = _dagScheduler
  private[spark] def dagScheduler_=(ds: DAGScheduler): Unit = {
    _dagScheduler = ds
  }
  ....
      // Create and start the scheduler
    val (sched, ts) = SparkContext.createTaskScheduler(this, master, deployMode)
    _schedulerBackend = sched
    _taskScheduler = ts
      _dagScheduler = new DAGScheduler(this)
      .....
  
    //啓動任務調度器,等待DAGScheduler分配task
    _taskScheduler.start()

DAGScheduler源代碼中相關的代碼如下:

private[spark] class DAGScheduler(
    private[scheduler] val sc: SparkContext,
    private[scheduler] val taskScheduler: TaskScheduler,
    listenerBus: LiveListenerBus,
    mapOutputTracker: MapOutputTrackerMaster,
    blockManagerMaster: BlockManagerMaster,
    env: SparkEnv,
    clock: Clock = new SystemClock())
  extends Logging {

  def this(sc: SparkContext, taskScheduler: TaskScheduler) = {
    this(
      sc,
      taskScheduler,
      sc.listenerBus,
    sc.env.mapOutputTracker.asInstanceOf[MapOutputTrackerMaster],
      sc.env.blockManager.master,
      sc.env)
  }
 //SparkContext實例化DAGScheduler的人口,利用傳入的SparkContext實例對象來設置將要提交TaskSet的TaskScheduler實例對象的引用
  def this(sc: SparkContext) = this(sc, sc.taskScheduler)
...
//TaskScheduler實例對象也要設置提交給它的TaskSet的DAGScheduler實例對象的引用
  taskScheduler.setDAGScheduler(this)
...
}

3. DAGScheduler劃分Stage的原理

關於Spark中的Stage的劃分,在博客中其實已經有一部分的介紹,在本節中,我們在深入進行介紹。

Spark 在分佈式環境下將數據分區, 然後將作業轉化爲 DAG, 並分階段進行 DAG 的調度和任務的分佈式並行處理。 DAG 將調度提交給 DAGScheduler, DAGScheduler 調度時會根據是否需要經過 Shuffle過程將 Job 劃分爲多個 Stage。
爲了方便理解 DAGScheduler 劃分 Stage 的原理, 下面來回顧一下博客中的一個典型的 DAG 劃分 Stage 示意圖, 如圖 所示。
在這裏插入圖片描述
在上圖中,RDD a 到 ShuffledRDD之間, 以及 UnionRDD到 CoGroupedRDD 之間的數據需要經過 Shuf­fle 過程, 因此 ROD a 和 UnionRDD分別是 Stage 1 跟 Stage 3 和 Stage 2 跟 Stage 3 的劃分點。 而ShuffledRDD 到 CoGroupedRDD 之間, 以及 RDD b到MappedRDD 到 UnionRDD 和 RDDc 到UnionRDD 之間的數據不需要經過Shuffle過程。因此,ShuffledRDD和CoGroupedRDD的依賴是窄依賴,兩個RDD屬於同一個Stage3,其餘RDD劃分爲2個Stage。Stage1和Stage2是相對獨立的,可以並行運行。Stage3則依賴於Stage1和Stage2的運行結果,所以Stage3最後執行。
由此可見,在DAGScheduler調度過程中,Stage階段換份是依據作業是否有Shuffle過程,也就是存在ShuffleDependency的寬依賴時,需要進行Shuffle,此時纔會將作業劃分爲多個Stage。

4. DAGScheduler劃分Stage的源代碼

上一節介紹了DAGScheduler劃分Stage的基本原理,本節結合源碼來看Spark如何具體實現Stage的劃分。
Spark的Action算子會觸發一個job(如,count),其本質是RDD的count方法調用了Spark Context的runJob方法(博客有相關介紹)。然後,在SparkContext的runJob方法中調用3次重載的runJob方法。 最後被調用的重載runJob方法中調用了DAGScheduler的runJob方法,從而進入DAG­Scheduler的源代碼。在DAGScheduler的源代碼中,DAGScheduler的runJob方法中調用submitJob方法。sub­rnitJob方法中最重要的是創建一個JobWaiter對象。

/**
   * 在給定的RDD上運行操作作業,並將所有結果在到達時傳遞給resultHandler函數。
   */
  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
    //創建一個JobWaiter對象
    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
    }
  }
  

其中submitJob的主要功能是具體創建JobWaiter和創建JobSubmitted事件對象把Job­Waiter對象發送給DAGScheduler的內嵌類DAGSchedulerEventProcessLoop對象實例,具體代碼如下:

/**
   * 將Action作業提交給調度程序。
   */
  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 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)
    }
    //獲取JobID,本質上是一個AtomicInteger類對象的自加操作,保證線程安全
    val jobId = nextJobId.getAndIncrement()
    //如果partitions爲空,則說明沒有task任務,則直接返回jobWaiter並且發送監聽事件表明任務結束
    if (partitions.isEmpty) {
      val clonedProperties = Utils.cloneProperties(properties)
      if (sc.getLocalProperty(SparkContext.SPARK_JOB_DESCRIPTION) == null) {
        clonedProperties.setProperty(SparkContext.SPARK_JOB_DESCRIPTION, callSite.shortForm)
      }
      val time = clock.getTimeMillis()
      listenerBus.post(
        SparkListenerJobStart(jobId, time, Seq.empty, clonedProperties))
      listenerBus.post(
        SparkListenerJobEnd(jobId, time, JobSucceeded))
      // Return immediately if the job is running 0 tasks
      return new JobWaiter[U](this, jobId, 0, resultHandler)
    }
	//再次判斷partitions是否爲空
    assert(partitions.nonEmpty)
    val func2 = func.asInstanceOf[(TaskContext, Iterator[_]) => _]
    //創建JobWaiter對象
    val waiter = new JobWaiter[U](this, jobId, partitions.size, resultHandler)
    //創建JobSubmitted事件對象把Job­Waiter對象發送給DAGScheduler的內嵌類DAGSchedulerEventProcessLoop對象實例
    eventProcessLoop.post(JobSubmitted(
      jobId, rdd, func2, partitions.toArray, callSite, waiter,
      Utils.cloneProperties(properties)))
    waiter
  }

DAG­SchedulerEventProcessLoop 在doOnReceive方法中反過來調用了 DAGScheduler 中實現劃分Stage算法很關鍵的handleJobSubrnitted方法。DAGScheduler的handleJobSubmitted方法中調用createResultStage方法,createResultStage方 法根據finalRDD創建finalStage , 這時便真正開始了Stage的劃分。DAGScheduler的handle­JobSubmitted方法中最後調用submitStage方法, 根據RDD的依賴關係,遞歸提交所有的 Stage。

private[scheduler] def handleJobSubmitted(jobId: Int,
      finalRDD: RDD[_],
      func: (TaskContext, Iterator[_]) => _,
      partitions: Array[Int],
      callSite: CallSite,
      listener: JobListener,
      properties: Properties): Unit = {
    var finalStage: ResultStage = null
    try {
      // 例如,如果作業在其基礎HDFS文件已被刪除的HadoopRDD上運行,則創建新階段可能會引發異常。
      finalStage = createResultStage(finalRDD, func, partitions, jobId, callSite)
    } catch {
     。。。。
    }
    // 提交作業,清除內部數據。
    barrierJobIdToNumTasksCheckFailures.remove(jobId)
	//創建Job
    val job = new ActiveJob(jobId, finalStage, callSite, listener, properties)
    .....
    //從finalStage開始,根據RDD的依賴關係,遞歸提交所有的Stage
    submitStage(finalStage)
  }

DAGScheduler 的 createResultStage方法中調用 getShuffleDependenciesAndResourceProfiles方法得到 RDD所有的shuffle依賴 列表, 以及 此階段與RDD關聯的ResourceProfiles。createResultStage源代碼如下所示:

/**
   * 創建與提供的jobId關聯的ResultStage。
   */
  private def createResultStage(
      rdd: RDD[_],
      func: (TaskContext, Iterator[_]) => _,
      partitions: Array[Int],
      jobId: Int,
      callSite: CallSite): ResultStage = {
      //返回Shueele依賴關係,它們是給定RDD的直接父級,並且是此階段與RDD關聯的ResourceProfiles。
       //此功能不會返回隨機的祖先。 例如,如果C對B具有Shueele依賴性,而B對A具有Shueele依賴性:A <-B <-C
    //用rdd C調用此函數只會返回B <-C依賴項。
    val (shuffleDeps, resourceProfiles) = getShuffleDependenciesAndResourceProfiles(rdd)
    //下面4行分別是合併資源和檢查stage資源狀態
    val resourceProfile = mergeResourceProfilesForStage(resourceProfiles)
    checkBarrierStageWithDynamicAllocation(rdd)
    checkBarrierStageWithNumSlots(rdd, resourceProfile)
    checkBarrierStageWithRDDChainPattern(rdd, partitions.toSet.size)
    //獲取或創建給定Shueele依賴項的父級列表。 將使用提供的firstJobId創建新的階段。
    val parents = getOrCreateParentStages(shuffleDeps, jobId)
    val id = nextStageId.getAndIncrement()
    val stage = new ResultStage(id, rdd, func, partitions, parents, jobId,
      callSite, resourceProfile.id)
    stageIdToStage(id) = stage
    updateJobIdStageIdMaps(jobId, stage)
    stage
  }

createResultStage中最重要的就是調用getShuffleDependenciesAndResourceProfiles來獲取RDD的所有直接依賴的shuffle依賴並且獲取與本階段該RDD相關的資源。getOrCreateParentStages方法通過獲取的shuffle依賴來獲取該RDD在Job中的所有Parent Stage列表,因此getShuffleDependenciesAndResourceProfiles方法也是Stage劃分的重點,其源代碼如下所示:

 private[scheduler] def getShuffleDependenciesAndResourceProfiles(
      rdd: RDD[_]): (HashSet[ShuffleDependency[_, _, _]], HashSet[ResourceProfile]) = {
    val parents = new HashSet[ShuffleDependency[_, _, _]]
    val resourceProfiles = new HashSet[ResourceProfile]
    val visited = new HashSet[RDD[_]]//標識已經訪問過的RDD,從後往前回溯
    val waitingForVisit = new ListBuffer[RDD[_]]//存儲需要被訪問的RDD
    waitingForVisit += rdd
    while (waitingForVisit.nonEmpty) {
      val toVisit = waitingForVisit.remove(0)
      if (!visited(toVisit)) {
        visited += toVisit//加入訪問列表
        Option(toVisit.getResourceProfile).foreach(resourceProfiles += _)
        toVisit.dependencies.foreach {
          case shuffleDep: ShuffleDependency[_, _, _] =>
            parents += shuffleDep//如果是Shuffle依賴則加入父級shuffle依賴列表
          case dependency =>
            waitingForVisit.prepend(dependency.rdd)//否則將其依賴的RDD加入待訪問列表,以便於後期繼續向前回溯
        }
      }
    }
    (parents, resourceProfiles)
  }

getOrCreateParentStages方法根據上面獲取的shuffle依賴來獲取父級stage列表,源代碼如下:

private def getOrCreateParentStages(shuffleDeps: HashSet[ShuffleDependency[_, _, _]],
      firstJobId: Int): List[Stage] = {
    shuffleDeps.map { shuffleDep =>
      getOrCreateShuffleMapStage(shuffleDep, firstJobId)
    }.toList
  }

該方法主要是對Shuffle依賴列表進行遍歷(map),對於每個shuffle依賴,調用getOrCreateShuffleMapStage獲取或創建其Stage,

//如果shuffleIdToMapStage中存在一個shuffle映射階段,則獲取該階段。 
//否則,如果不存在隨機映射階段,則此方法將在缺少任何祖先隨機映射階段的同時創建隨機映射階段。
 private def getOrCreateShuffleMapStage(
      shuffleDep: ShuffleDependency[_, _, _],
      firstJobId: Int): ShuffleMapStage = {
    shuffleIdToMapStage.get(shuffleDep.shuffleId) match {
      case Some(stage) =>
        stage
      case None =>
        // 爲所有缺少的祖先shuffle 依賴創建階段     
        getMissingAncestorShuffleDependencies(shuffleDep.rdd).foreach { dep =>
         if (!shuffleIdToMapStage.contains(dep.shuffleId)) {
            createShuffleMapStage(dep, firstJobId)
          }
        }
        // Finally, create a stage for the given shuffle dependency.
        createShuffleMapStage(shuffleDep, firstJobId)
    }
  }

getMissingAncestorShuffleDependencies是查找尚未在shuffleToMapStage中註冊的祖先shuffle依賴項,具體代碼如下:

private def getMissingAncestorShuffleDependencies(
      rdd: RDD[_]): ListBuffer[ShuffleDependency[_, _, _]] = {
    val ancestors = new ListBuffer[ShuffleDependency[_, _, _]]
    val visited = new HashSet[RDD[_]]
    // 我們在此處手動維護堆棧,以防止遞歸訪問引起的StackOverflowError
    val waitingForVisit = new ListBuffer[RDD[_]]
    waitingForVisit += rdd
    while (waitingForVisit.nonEmpty) {
      val toVisit = waitingForVisit.remove(0)
      if (!visited(toVisit)) {
        visited += toVisit
        //getShuffleDependenciesAndResourceProfiles方法遞歸向前回溯發現依賴
        val (shuffleDeps, _) = getShuffleDependenciesAndResourceProfiles(toVisit)
        shuffleDeps.foreach { shuffleDep =>
          if (!shuffleIdToMapStage.contains(shuffleDep.shuffleId)) {
          //不存在Map列表中就將其加入祖先列表
            ancestors.prepend(shuffleDep)
            waitingForVisit.prepend(shuffleDep.rdd)
          } 
        }
      }
    }
    ancestors
  }

createShuffleMapStage會根據給定的Shuffle依賴來創建ShuffleMapStage,其內容基本與創建ResultStage相同。

def createShuffleMapStage[K, V, C](
      shuffleDep: ShuffleDependency[K, V, C], jobId: Int): ShuffleMapStage = {
    val rdd = shuffleDep.rdd
    val (shuffleDeps, resourceProfiles) = getShuffleDependenciesAndResourceProfiles(rdd)
    val resourceProfile = mergeResourceProfilesForStage(resourceProfiles)
    checkBarrierStageWithDynamicAllocation(rdd)
    checkBarrierStageWithNumSlots(rdd, resourceProfile)
    checkBarrierStageWithRDDChainPattern(rdd, rdd.getNumPartitions)
    val numTasks = rdd.partitions.length
    val parents = getOrCreateParentStages(shuffleDeps, jobId)
    val id = nextStageId.getAndIncrement()
    val stage = new ShuffleMapStage(
      id, rdd, numTasks, parents, jobId, rdd.creationSite, shuffleDep, mapOutputTracker,
      resourceProfile.id)

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

    if (!mapOutputTracker.containsShuffle(shuffleDep.shuffleId)) {
      // 這裏需要在mapOutputTracker中註冊RDD,因爲分區的數目未知,無法在RDD構造函數中進行註冊
      logInfo(s"Registering RDD ${rdd.id} (${rdd.getCreationSite}) as input to " +
        s"shuffle ${shuffleDep.shuffleId}")
      mapOutputTracker.registerShuffle(shuffleDep.shuffleId, rdd.partitions.length)
    }
    stage
  }

至此, 整個 DAGScheduler 劃分 Stage 的過程已經介紹完畢。 Stage 劃分完成後, DAG­Scheduler 就會回到 HandleJobSubmitted 方法中調用 submitStage 方法。 在 submitStage 方法中,從 finalStage (ResultStage 對象實例)開始回溯, 直到沒有 Parent Stage 爲止, 提交整個 Job 的所有 Stage, 在某一個 Stage, DAGScheduler 會調用 submitMissingTasks 方法把 Tasks 提交給 TaskScheduler 進行細粒度的 Task 調度。
下面介紹在 Stage 內部 Task 獲取最佳位置的算法。

5.Stage內部Task獲取最佳位置的源代碼

數據本地性是指:確定數據在哪個結點上,就到哪個結點的Executor上去運行。

在DAGScheudler的submitMissingTasks方法中體現了利用RDD的本地性來得到Task的本地性,從而獲取Stage內部Task的最佳位置。DAGScheudler的submitMissingTasks方法會通過調用getPreferredLocs方法得到Task最佳位置,並把結果存放到taskldToLocationsMap中以便使用。
DAGScheudler的submitMissingTasks方法的相關部分關鍵代碼如下:

 private def submitMissingTasks(stage: Stage, jobId: Int): Unit = {
    
    // 找出要計算的分區ID的索引。
    val partitionsToCompute: Seq[Int] = stage.findMissingPartitions()
    ......
    val taskIdToLocations: Map[Int, Seq[TaskLocation]] = try {
      stage match {
        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
      }
    } catch {
      case NonFatal(e) =>
        stage.makeNewStageAttempt(partitionsToCompute.size)
        listenerBus.post(SparkListenerStageSubmitted(stage.latestInfo, properties))
        abortStage(stage, s"Task creation failed: $e\n${Utils.exceptionString(e)}", Some(e))
        runningStages -= stage
        return
    }
    ......
    stage.makeNewStageAttempt(partitionsToCompute.size, taskIdToLocations.values.toSeq)
    .......
    val tasks: Seq[Task[_]] = try {
      val serializedTaskMetrics = closureSerializer.serialize(stage.latestInfo.taskMetrics).array()
      stage match {
        case stage: ShuffleMapStage =>
          stage.pendingPartitions.clear()
          partitionsToCompute.map { id =>
            val locs = taskIdToLocations(id)
            val part = partitions(id)
            stage.pendingPartitions += id
            new ShuffleMapTask(stage.id, stage.latestInfo.attemptNumber,
              taskBinary, part, locs, properties, serializedTaskMetrics, Option(jobId),
              Option(sc.applicationId), sc.applicationAttemptId, stage.rdd.isBarrier())
          }

        case stage: ResultStage =>
          partitionsToCompute.map { id =>
            val p: Int = stage.partitions(id)
            val part = partitions(p)
            val locs = taskIdToLocations(id)
            new ResultTask(stage.id, stage.latestInfo.attemptNumber,
              taskBinary, part, locs, id, properties, serializedTaskMetrics,
              Option(jobId), Option(sc.applicationId), sc.applicationAttemptId,
              stage.rdd.isBarrier())
          }
      }
    } catch {
    }
  }

DAGScheudler的getPreferredLocs方法只是調用getPreferredLocslntemal方法。 DAGScheudler的getPreferredLocs方法代碼如下。

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

DAGScheudler的getPreferredLocslntemal方法具體實現了一個Partition的數據本地性的算法。在具體算法實現時,首先查詢DAGScheduler的內存數據結構中是否存在當前Paritition 的數據本地性的信息,如果有則直接返回,如果沒有首先會調用rdd.getPreferedLocations, 例如,想讓Spark運行在HBase上或一種現在還沒有直接支持的數據庫上面,此時開發者需要自定義ROD,爲了保證Task計算的數據本地性,最爲關鍵的方式是必須實現RDD的get­PreferedLocations, 數據本地性在底層運行之前就完成了。

/**
   *getPreferredLocs的遞歸實現。
    *
    *此方法是線程安全的,因爲它僅通過線程安全的方法(getCacheLocs())訪問DAGScheduler狀態。 修改此方法時請小心,因爲它訪問的任何新DAGScheduler狀態都可能需要附加同步。
   */
  private def getPreferredLocsInternal(
      rdd: RDD[_],
      partition: Int,
      visited: HashSet[(RDD[_], Int)]): Seq[TaskLocation] = {
    // 如果已經訪問過該分區,則無需重新訪問.
    if (!visited.add((rdd, partition))) {
      // 對於以前訪問的分區,已經返回Nil。
      return Nil
    }
    // 如果已緩存分區,則返回緩存位置
    val cached = getCacheLocs(rdd)(partition)
    if (cached.nonEmpty) {
      return cached
    }
    // 如果RDD有首選位置(如輸入RDD的情況),則獲取並構造TaskLocation返回
    val rddPrefs = rdd.preferredLocations(rdd.partitions(partition)).toList
    if (rddPrefs.nonEmpty) {
      return rddPrefs.map(TaskLocation(_))
    }

    // 如果RDD具有窄依賴,請選擇具有任何放置首選項的第一個窄依賴的第一個分區。
    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
  }

DAGScheduler計算數據本地性時巧妙地藉助了RDD自身的getPreferedLocations中的數據, 最大化地優化效率, 因爲getPreferedLocations中表明瞭每個Partition的數據本地性, 雖然當前Partition可能被persist或者checkpoint, 但是persist或者checkpoint默認情況下肯定是 和getPreferedLocations中的Partition 數據本地性一致, 所以這就極大地簡化了Task數據本地性算法的實現和效率的優化。

參考數據和網站
https://www.cnblogs.com/superhedantou/p/5699201.html
《Spark內核機制解析及性能調優》

如果喜歡的話希望點贊收藏,關注我,將不間斷更新博客。

希望熱愛技術的小夥伴私聊,一起學習進步

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