Spark之Stage的生成及任務的執行

每一個Spark應用都會創建一個sparksession,用來跟Spark集羣交互,如果提交任務的模式爲cluster模式,則Driver進程會被隨機在某個worker結點上啓動,然後真正執行用戶提供的入口類,或是使用Spark內置的入口類,同時會在Driver進程創建SparkContext對象,提供Spark應用生命週期內所涉及到的各種系統角色。

clientmasterworkerdriverdirverexecutorRequestSubmitDriverLaunchDrivernew DriverRunner["deploy.worker.DriverWrapper"]ProcessBuilder.run()waiting driver subprocess to exitsetup new RpcEndpoint workerWatcherinvoke user defined main class or the internal classloop[ WaitFor ]LaunchExecutorExecutorRunner.start()prepare and run subprocessExecutorStateChangedhandle executor state changed,ExecutorStateChangedtry to clean finished executors from worker and app info cachedExecutorUpdatedclientmasterworkerdriverdirverexecutor

SparkContext

使用Spark功能的主入口,用戶可以通過此實例在集羣中創建RDD、求累加各以及廣播變量。
由於Driver進程會真正執行Spark應用的入口程序,因此它只會在driver端被創建。
默認情況下,一個JVM環境只會創建一個SparkContext實例,但用戶可以修改spark.driver.allowMultipleContexts的默認值,來啓用多個實例。SparkContext內部會啓動多個線程來完成不同的工作,比如分發事件到監聽者、動態分配和回收executors、接收executor的心跳等,因此需要用戶主動調用stop()接口來關閉此實例。

當用戶通過Action函數觸發RDD上的計算時,(通常指countsumtake等的返回結果爲非RDD的函數),便會生成一個Job,被提交到Spark集羣運行。而提交Job的入口便是此實例對象,比如RDD中的count()方法的定義如下:

  /**
   * Return the number of elements in the RDD.
   * sc 是SparkContext實例的變量名
   */
  def count(): Long = sc.runJob(this, Utils.getIteratorSize _).sum

可以看到當一個Action方法被調用時,會通過調用SparkContext的runJob方法,並將當前RDD作爲函數參數,觸發計算過程,最終通過一系列地包裝,將Job任務的生成交由DAGScheduler對象。

DAGScheduler

面向Stage的調度器,它會爲每一個Job計算相應的stage,並跟蹤哪個RDD和Stage的輸出結果被物化了,並找到一個最小的調度方案來跑Job。DAGScheduler會把所有的stage封裝成一系列的TaskSet,提交給底層的一個TaskScheduler的實現類,由它來負責任務的執行。TaskSet包含了完整的依賴任務,這些任務都可以基於已經計算出現的數據結果,比如上游Stage的map任務的輸出,完成自己的工作,即使這些任務會由於依賴數據不可用而失敗。
Spark Stage的生成是通過在RDD圖中劃分shuffle邊界而得到的。RDD的窄依賴,比如map()和filter()方法,會被以流水線的方式劃分到一個stage的任務集中(TaskSet),但有shuffle行爲的操作需要依賴多個其它的stage的完成(比如一個stage輸出一組map數據文件,而另外一個stage會在被阻塞之後讀取這些文件)。最終每一個stage都只會與其它stage有shuffle依賴,而且其可能會執行多個操作。
只有當各種類型的RDD之上的RDD.compute()方法被觸發時,纔會真正地開始執行整個流水線。

另外對於一個DAG圖中的所有stage,DAGScheduler也會根據當前集羣的狀態來決定每一個任務應該在哪個地方執行,最終纔會把這些任務發送到底層的TashScheduler。同時DAGScheduler會保證當一個stage的所需要的shuffle數據文件丟失時,同跑上游的stage,而TaskScheduler會保證當一個stage內部產生錯誤時(非數據文件丟失)會嘗試重跑每個任務,在一定次數的嘗試之後,取消整個stage的執行。

在嘗試查看這部分的實現時,有如下一些概念需要明確:

  • Jobs (內部以ActiveJob表示,兩類),上層提交給調度器的工作單元。比如調用count()方法時,一個新的Job就會生成,而這個Job會在執行了多個stage之後生成中間的數據。
  • Stages(內部以Stage表示),包含了一組合任務集(TaskSet),每一個stage會生成當前Job所需的中間結果,而每一個任務都會在同一個RDD數據集的所有Partition(分區)數據塊上執行同一個方法。而由shuffle邊界(Shuffle Boundries,或稱之爲Barrier屏障)所分隔的stage,會所有barrier之前的stage執行完成之後纔開始執行。因此這裏有兩類的Stage,一個是ResultStage,表示最後一個stage,輸出action方法的結果;另一個是ShuffleMapStage,爲每一個shuffle操作輸出數據文件。通常這些Stage產生的結果都是可以跨Job共享的,由於這不同的Job使用了同一份RDD數據。
    每一個Stage都有一個firstJobId域,用於標識第一次提交當前Stage的Job的ID,如果使用的是FIFO的調度方式,通過此域可以提供首先執行前置Job中的stage或是有失敗時快速恢復的能力。
    最終一個Stage能夠在失敗時嘗試恢復執行多次,因此Stage對象必須跟蹤多個StageInfo對象,然後轉發給listeners(監聽器)或是WEB UI。
  • TaskSet,包含一組有依賴關係的任務,同時也表示這些任務都有共同的shuffle依賴,通常構成一組流水線操作,比如map().filter(),或表示一個特定stage丟失的分區。
  • Tasks,最小的執行單元,會被分發到每個機器上執行。
  • Cache tracking,記錄了有哪個RDD已經被緩存了,以避免重複計算;同時也記錄了哪些ShuffleMapStage已經完成並生成了數據文件,以避免重複執行shuffle階段的map任務。
  • Preferred locations: DAGScheduler會根據底層RDD,或是cache或shuffle的數據的位置來計算一個Stage中每個任務最優地執行節點。
  • Cleanup:爲了避免一個長週期運行的應用可能導致內存泄漏的問題,正在運行的Job完成時所依賴的所有數據結構都會被清理。
    爲了失敗時能夠故障恢復,一個stage可能被提交多次,稱爲attempts。如是TaskScheduler報告某個任務由於FetchFailed或是ExeuctorLost事件而失敗時,DAGScheduler會重新提交失敗的stage。

DAGScheduler接收到來自SparkContext對象提交任務的請求後,會根據傳遞過來的RDD的paritions數量、用戶提供的方法函數、JobId等信息,通過逆拓撲序的方法,遞歸地構建StageTaskSet

提交Stage的過程描述如下:

  1. 首先從ResultStage開始,提交Stage執行,首先嚐試創建其父依賴的ShuffleMapStage
  2. 遍歷當前Stage的父依賴,如果是NarrowDependency類型的依賴,則將其綁定的RDD添加到待遍歷隊列,以便繼續回溯查找其父依賴,直到找到一個最近的ShuffleMapStage,否則若爲ShuffleDependency,則跳轉到步驟3
  3. 調用getOrCreateShuffleMapStage()方法,先嚐試從緩存的ShuffleMapStage中,按傳遞進來的ShuffleDependency的shuffleId字段,查找是否已經被創建過,有則返回,沒有跳轉到步驟4
  4. 找到當前父ShuffleDependency所包含的rdd的所有未創建過對應ShuffleMapStage的祖先ShuffleDenpendency,爲這些祖先依賴創建ShuffleMapStage,遞歸重複步驟2,否則如果所有的祖先依賴都已經被創建,由執行步驟5
  5. 創建當前ShuffleDependency對應的ShuffleMapStage,並綁定所有的直接父Stage
  6. ResultStage對應的所有父ShuffleMapStage創建成功後,並將這些父依賴Stage作爲參數創建最後的ResultStage

至此,所有可能的祖先ShuffleMapStage被依次創建完畢,每一個ShuffleMapStage只包含以下其直接父依賴所對應的ShuffleMapStage。

## rdd方法調用圖
rdd1 -- map() --> rdd2 -- filter() -> rdd3 -- groupbykey() --> rdd3 
                   \                                               \
                    \                                               \
                      filter() --> rdd4 -- reducebykey() --> rdd5 -- join() --> rdd6 -- count() --> result
## rdd方法調用圖轉換成依賴圖的結果如下
## ndep == narrow dependency
## sdep == shuffle dependency
rdd1 -- ndep --> rdd2 -- ndep -> rdd3 -- [sdep] --> rdd3
                   \                                    \
                    \                                    \
                      ndep --> rdd4 -- [sdep] --> rdd5 -- ndep --> rdd6 -- action --> result
## 如果result stage所對應的RDD的直接父依賴,不是ShuffleDependency,則繼續回溯父依賴(NarrowDependency)的父依賴,
## 直到找到了每個直接父依賴可能存在的、離result stage最近的依賴,如stage1, stage2,則算完成第一輪創建。
## 然後再遞歸創建stage1、stage2所對應的父ShuffleDependencty。
## 依賴圖換成stage圖的結果如下
stage1 --> stage1  -> stage1 --> stage1 
                                       \
                                          --> stage0 --> stage0
                                       /
stage2 --> stage2 --> stage2 --> stage2

SchedulerBackend

Driver進程在初始化SparkContext的實例時,會創建任務調度相關的組件,它包括集羣管理器SchedulerBackend、任務調度器TaskScheduler的實例,分別用來管理Spark應用關聯的集羣和任務分發和執行。根據用戶的執行環境不同,一共有三種類型的SchedulerBackend(Cluster Manager)可能被創建:

  • LocalSchedulerBackend
    當用戶提交的任務以本地方式執行時,創建此類的實例,它會在當前的JVM環境中創建executor線程,執行任務。
  • StandaloneSchedulerBackend
    當用戶提交的任務以本地集羣的方式執行或是提交到standalone集羣執行時,會創建此類的實例,它繼承自CoarseGrainedSchedulerBackend類,除了基本的分配和回收Executor功能外,它提供了用於和standalone集羣交互的接口,不論是用戶使用的是Spark開發模式(用戶本地會啓動一個常駐的子進程來交互式地提交任務)還是提交模式(spark-submit),都需要通過此實例與集羣間接交互。
  • ExternalClusterManager
    外部集羣管理器,

核心的源碼摘取如下:

  private def createTaskScheduler(
      sc: SparkContext,
      master: String,
      deployMode: String): (SchedulerBackend, TaskScheduler) = {
    import SparkMasterRegex._

    master match {
      case "local" =>
        val scheduler = new TaskSchedulerImpl(sc, MAX_LOCAL_TASK_FAILURES, isLocal = true)
        val backend = new LocalSchedulerBackend(sc.getConf, scheduler, 1)
        scheduler.initialize(backend)
        (backend, scheduler)

      case LOCAL_N_REGEX(threads) =>
        val scheduler = new TaskSchedulerImpl(sc, MAX_LOCAL_TASK_FAILURES, isLocal = true)
        val backend = new LocalSchedulerBackend(sc.getConf, scheduler, threadCount)
        scheduler.initialize(backend)
        (backend, scheduler)

      case LOCAL_N_FAILURES_REGEX(threads, maxFailures) =>
        val scheduler = new TaskSchedulerImpl(sc, maxFailures.toInt, isLocal = true)
        val backend = new LocalSchedulerBackend(sc.getConf, scheduler, threadCount)
        scheduler.initialize(backend)
        (backend, scheduler)

      case SPARK_REGEX(sparkUrl) =>
        val scheduler = new TaskSchedulerImpl(sc)
        val masterUrls = sparkUrl.split(",").map("spark://" + _)
        val backend = new StandaloneSchedulerBackend(scheduler, sc, masterUrls)
        scheduler.initialize(backend)
        (backend, scheduler)

      case LOCAL_CLUSTER_REGEX(numSlaves, coresPerSlave, memoryPerSlave) =>
        val scheduler = new TaskSchedulerImpl(sc)
        val localCluster = new LocalSparkCluster(
          numSlaves.toInt, coresPerSlave.toInt, memoryPerSlaveInt, sc.conf)
        val masterUrls = localCluster.start()
        val backend = new StandaloneSchedulerBackend(scheduler, sc, masterUrls)
        scheduler.initialize(backend)
        backend.shutdownCallback = (backend: StandaloneSchedulerBackend) => {
          localCluster.stop()
        }
        (backend, scheduler)

      case masterUrl =>
        val cm = getClusterManager(masterUrl) match {
          case Some(clusterMgr) => clusterMgr
          case None => throw new SparkException("Could not parse Master URL: '" + master + "'")
        }
        try {
          val scheduler = cm.createTaskScheduler(sc, masterUrl)
          val backend = cm.createSchedulerBackend(sc, masterUrl, scheduler)
          cm.initialize(scheduler, backend)
          (backend, scheduler)
        } catch {
          case se: SparkException => throw se
          case NonFatal(e) =>
            throw new SparkException("External scheduler cannot be instantiated", e)
        }
    }
  }

CoarseGrainedSchedulerBackend(通常所說的driver,StandaloneSchedulerBackend但是它的一個子類)爲該類的一個實現類,用於管理所有的executors(這裏的executor指的是一個在worker節點啓動的後端進程CoarseGrainedExecutorBackend,管理真正的Executor實例),這些executor的生命週期是與Spark Job的綁定的,而非每個Task,這樣就避免了過多的資源的申請、創建、回收等的過程,提高整個集羣的性能。

當一個Spark應用被提交到集羣時,會先傳遞給DAGScheduler進行Stage的劃分及TaskSet的生成,而後續任務的調度和執行由DAGScheduler通過RPCJobSubmitted消息轉發給SchedulerBackend對象。

TaskScheduler

其實現類爲TaskSchedulerImpl,它會接收來自DAGScheduler生成的一系列TaskSet,然後創建對應的TaskSetManager(實際負責調度TaskSet中的每一個任務),並將TaskSetManager添加到自己的等待資源池裏,等待後續的調度(調度方式目前有兩種,一是先進選出調度,一種是公平調度)。

  def initialize(backend: SchedulerBackend) {
    this.backend = backend
    schedulableBuilder = {
      schedulingMode match {
        case SchedulingMode.FIFO =>
          new FIFOSchedulableBuilder(rootPool)
        case SchedulingMode.FAIR =>
          new FairSchedulableBuilder(rootPool, conf)
        case _ =>
          throw new IllegalArgumentException(s"Unsupported $SCHEDULER_MODE_PROPERTY: " +
          s"$schedulingMode")
      }
    }
    schedulableBuilder.buildPools()
  }

當TaskSchedulerImpl接收到可調度的任務集後,就通知driver進程,嘗試調度Task執行。

  override def submitTasks(taskSet: TaskSet) {
    val tasks = taskSet.tasks
    this.synchronized {
      val manager = createTaskSetManager(taskSet, maxTaskFailures)
      val stage = taskSet.stageId
      val stageTaskSets =
        taskSetsByStageIdAndAttempt.getOrElseUpdate(stage, new HashMap[Int, TaskSetManager])

      stageTaskSets.foreach { case (_, ts) =>
        ts.isZombie = true
      }
      stageTaskSets(taskSet.stageAttemptId) = manager
      schedulableBuilder.addTaskSetManager(manager, manager.taskSet.properties)

      if (!isLocal && !hasReceivedTask) {
        starvationTimer.scheduleAtFixedRate(new TimerTask() {
          override def run() {
            if (!hasLaunchedTask) {
            } else {
              this.cancel()
            }
          }
        }, STARVATION_TIMEOUT_MS, STARVATION_TIMEOUT_MS)
      }
      hasReceivedTask = true
    }
    backend.reviveOffers()
  }

TaskSchedulerCoarseGrainedSchedulerBackend接收已經註冊的、可用的空閒executors信息,從自己的等待池中遍歷所有的TaskSet,嘗試調度每一個TaskSet執行,但很多情況下,並不是每一個TaskSet中的所有Task被一次分配資源並執行,因此TaskScheduler完成爲某一個等待中的TaskSet找到一組可用的executors,至於如何在這些executors上分配自己管理的Task,則將由TaskSetManager完成。TaskSetManager嘗試分配任務到時某個executor上的核心代碼如下:

  @throws[TaskNotSerializableException]
  def resourceOffer(
      execId: String,
      host: String,
      maxLocality: TaskLocality.TaskLocality)
    : Option[TaskDescription] =
  {
    val offerBlacklisted = taskSetBlacklistHelperOpt.exists { blacklist =>
      blacklist.isNodeBlacklistedForTaskSet(host) ||
        blacklist.isExecutorBlacklistedForTaskSet(execId)
    }
    if (!isZombie && !offerBlacklisted) {
      var allowedLocality = maxLocality

      if (maxLocality != TaskLocality.NO_PREF) {
        allowedLocality = getAllowedLocalityLevel(curTime)
        if (allowedLocality > maxLocality) {
          // We're not allowed to search for farther-away tasks
          allowedLocality = maxLocality
        }
      }

      dequeueTask(execId, host, allowedLocality).map { case ((index, taskLocality, speculative)) =>
        // Found a task; do some bookkeeping and return a task description
        val task = tasks(index)
        val taskId = sched.newTaskId()
        // Do various bookkeeping
        copiesRunning(index) += 1
        val attemptNum = taskAttempts(index).size
        val info = new TaskInfo(taskId, index, attemptNum, curTime,
          execId, host, taskLocality, speculative)
        taskInfos(taskId) = info
        taskAttempts(index) = info :: taskAttempts(index)
        // Update our locality level for delay scheduling
        // NO_PREF will not affect the variables related to delay scheduling
        if (maxLocality != TaskLocality.NO_PREF) {
          currentLocalityIndex = getLocalityIndex(taskLocality)
          lastLaunchTime = curTime
        }
        // Serialize and return the task
        val serializedTask: ByteBuffer = try {
          ser.serialize(task)
        } catch {

          case NonFatal(e) =>
            throw new TaskNotSerializableException(e)
        }
        if (serializedTask.limit() > TaskSetManager.TASK_SIZE_TO_WARN_KB * 1024 &&
          !emittedTaskSizeWarning) {
          emittedTaskSizeWarning = true

        addRunningTask(taskId)

        sched.dagScheduler.taskStarted(task, info)
        new TaskDescription(
          taskId,
          attemptNum,
          execId,
          taskName,
          index,
          task.partitionId,
          addedFiles,
          addedJars,
          task.localProperties,
          serializedTask)
      }
    } else {
      None
    }
  }

TaskSetManager

每一個該類的實例,都對應於一個TaskSet,它負責調度TaskSet中的任務,並跟蹤、記錄每一個任務的狀態和行爲。

TaskSetManager內部使用本地化可感知的延遲算法,爲自己管理的TaskSet中的每一個任務Task(主要是ShuffleMapTaskResultTask類型的任務)分配executor,同時創建相應的TaskDescription集體,返回給上層角色。

ExecutorAllocationManager

Executor執行器分配管理器,(當用戶開啓了動態資源分配策略時spark.dynamicAllocation.enabled時會在SparkContext內部創建此對象),基於工作負載動態分配和回收executors,它內部維護了一個可變的目標數據變量,表示當前應用需要多少個活動的executor才能解決任務的積壓問題,它的最小值爲配置的初始化值,並跟隨堆積和正在運行的任務的數量而變化。manager會週期性地與cluster manager(SchedulerBackend後端)同步executor的目標數量的值。

如果當前executor的數量大於目前的負載,會被減少至能夠容納當前正在運行和堆積的任務的數量。而需要增加executor數量的情況發生在有任務堆積待執行的時候,並且在N秒內不能夠處理完等待隊列中的任務;或者之前增加executor數量的操作不能夠在M秒內消費完隊列中的任務時候,繼續增加executor數量的操作,然後繼續下一輪判定。在每一輪的判定中,executor數量的增加是指數級的,直到達到一個上界值,而這個上界是基於配置的spark屬性及當前正在運行和等待的任務數量共同決定的。

至於爲何以指數的遞增方式增加executor,這裏有兩個方面的合理性:

  1. 添加executor的行爲在起始階段應當是緩慢的,以防當前應用只需求很小的數量便能夠完成工作,否則才需要增加更多的executor;
  2. 在一定時間以後,增加executor的動作應該是快速的,以花費更多的時間才能達到目標數量的executor。

移除executor的邏輯很簡單,如果某個executor在K秒內沒有運行過任務,那麼就應該移除它。ExecutorAllocationManager沒有重試的邏輯,以增加或是減少executor,因此如何才能保證達到請求的executor數量是由ClusterManager保證的。

與此管理器相關的、可配置的屬性有以下幾個:

 *   spark.dynamicAllocation.enabled - 是否開啓動態分配功能
 *   spark.dynamicAllocation.minExecutors - 活動executor的最小數量
 *   spark.dynamicAllocation.maxExecutors - 活動executor的最大數量
 *   spark.dynamicAllocation.initialExecutors - 應用啓動時請求的executor數量
 *
 *   spark.dynamicAllocation.executorAllocationRatio - 控制executor數量的因子,實際的最大executor數量=(任務運行數+等待任務數)* ration / executor.cores
 *
 *   spark.dynamicAllocation.schedulerBacklogTimeout (M) - 如果在timeout時間後依然有堆積的任務,則嘗試增加executor的數量
 *
 *   spark.dynamicAllocation.sustainedSchedulerBacklogTimeout (N) - 如果
 *
 *   spark.dynamicAllocation.executorIdleTimeout (K) - timeout時間後移除executor

Job / Stage / Task間的關係

  1. 當前用戶調用一個action方法觸發執行時,就會創建一個Job,(比如rdd.take(10)方法),就開始在Spark集羣中計算數據。
  2. 一個Job開始以後,會先經由DAGScheduler進行依賴分析,從最後一個方法調用開始,回溯RDD的依賴圖(當調用一個RDD的方法時,就會新建一個新的RDD實例,如MapPartitionsRDD或是ShuffledRDD等,並封裝調用的RDD爲其父依賴,MapPartitionsRDD對應於OneToOneDependencyShuffledRDD對應於ShuffleDependency),創建Stage圖,而後嘗試提交最後一個Stage(即ResultStage)執行。
  3. 具體到如何計算Partition數據,需要創建具體的Task實例在集羣中執行。因此DAGScheduler創建好Stage圖之後,會採用逆拓撲排序的方法,依次創建爲每一個Stage的每個Partition創建任務實例Task。當回溯到某個Stage沒有父依賴時,意味着當前Stage應該被首先執行,且可以提交執行,因此將當前Stage添加到running隊列中,併爲這個Stage的每一個待生成的Partition創建對應的Task,而後提交執行。
  4. TaskSchedulerImpl接收到任務後,且有worker資源啓動任務時,就會將任務分配到可用的worker執行,例如ShuffleMapTask會執行Reduce/Map操作,即先reduce之前依賴的Partition的數據,再Map輸出reduce之後的Partition數據。
  5. 當前任務執行完成後,不論成功還是失敗,都會返回一個MapStatus的實例,描述當前Task的狀態信息,並序列化後發送給driver端。
  6. driver端收到任務的更新消息後,就會更新任務相關的所有對象,最終如果當前Stage包含的所有任務都已經正常完成,則會嘗試啓動所有的孩子Stage執行。如下圖中表示的依賴關係,Stage_0Stage_1可以同時執行,但只有先完成Stage_0纔會執行Stage_1
    關係圖

Spark App提交過程

Spark應用提交到Standalone集羣過程的時序圖如下所示,一個App提交給master後,最終會在集羣內啓動一個driver進程,通過各種RPC交互與集羣交互,完成應用的運行。

spark submitclientappmasterworkerdriversparksessionsparkcontext封裝DriverDescriptionRequestSubmitDriverLaunchDriver啓動子進程SparkSession.getOrCreate()SparkContext.getOrCreate()SparkContext集羣交互的入口spark submitclientappmasterworkerdriversparksessionsparkcontext

Spark App執行過程

Spark App Cluster構建時序圖

最終一個Spark應用從Driver進程到啓動Executor執行的過程的時序圖如下:

sparkcontextStandaloneSchStandaloneAppClienmasterworkerdriverdirverCoarseGrainedE創建Executor調度器,即Driver RpcEndpoint內部創建ApplicationDescription實例創建App客戶端,用於交互編程即AppClientRpcEndpoint,提供訪問ClusterManager的接口send RegisterApplication,並等待註冊完成註冊App,生成ID並添加到等待隊列,然後返回註冊成功的消息給driver進程receive RegisteredApplication,更新標記位,master地址,appId通知SchedulerBackend完成註冊更新標記位並更新appId調用schedule()方法,爲app分配executors爲每一個executor構建ExecutorDesc,並更新緩存的ApplicationInfosend LaunchExecutor默認情況下,master會把app所需要的executor分散到儘可能多的worker上面,用戶可以修改spark.deploy.spreadOut的值爲false來禁用這個策略send ExecutorAdded打印添加Executor成功的信息ExecutorRunner.start(),內部啓動cores個線程來跑Task創建子進程調用java命令啓動入口類CoarseGrainedExecutorBackend,同時更新資源變量併發送ExecutorStateChanged到masterask RegisterExecutor更新本地緩存的executors信息,如果不存在,則構建ExecutorDataresponse RegisteredExecutorsend ExecutorStateChangedreceive ExecutorStateChanged,schedule() again更新緩存的ApplicationInfo的對象中相應executor的狀態爲最新狀態,如果完成狀態,則從worker和appInfo中移除send ExecutorUpdated打印狀態更新日誌,如果executor完成,則從driver端移除並更新各類變量sparkcontextStandaloneSchStandaloneAppClienmasterworkerdriverdirverCoarseGrainedE

Spark App 執行

生成RDD

one
createDataFrame
rdd
SparkPlan.execute
doExecute
sparksession
DataFrame
QueryExecutor
HiveTableScanExec
RDD

執行Shuffle Map任務的時序圖

RDDsparkcontextdagschedulermapoutputtrtaskschedulerimplschedulerbackenddrivertasksetmanagerexecutorbackendexecutorTaskblockmanagerShuffleMapTaskSortShuffleWriterrunJob執行collect(),結果寫到results數組,對應每個分區的結果runJobsubmitJob() to send JobSubmitted生成Jobid,創建JobWaiter對象,異步等待Job完成handleJobSubmitted() to create shuffle map stages從最後一個rdd向上回溯,構建ShuffleMapStage,如果其有父依賴是Shuffle依賴,則以深度拓撲排序的方法嘗試構建所有的祖先ShuffleMapStage,最後構建ResultStagesubmitStage() to submit stage recursivelynumAvailableOutputs == numPartitions先遞歸提交所有的沒有完成的父stage,如果不存在未完成的,則提交當前stage。submitMissingTasks() to submit stagesfindMissingPartitions()check partitions ofthe Job forResultStage,another ofMapOutputTrackermake new Broadcast for stage.rddBroadcast withshuffledependencies forShuffuleMapStage,or with func forResultStagesubmitTasks() with a TaskSet for the stagenewShuffleMapTask foreach partition ofSMS or newResultTask foreach partition ofRSsubmitTasks() with a TaskSetcreateTaskSetManager() for the TaskSetschedulerBuilder.addTaskSetManager()schedulerBuilder isFIFOSchedulableBuilderorFairSchedulableBuilderreviveOffers()send ReviveOffers with driver endpoint refmakeOffers()make newWorkerOffer foreach aliveexecutorresourceOffers with IndexedSeq[WorkerOffer]嘗試分析等待任務到時可用的worker中的executorresourceOfferSingleTaskSet()resourceOffer(execId, host, maxLocality)update cache and mark launchedTask as trueloop[ ShuffledWorkerOffers ]loop[ SortedTaskSets ]loop[ submitStage() ]launchTasks(tasks: Seq[Seq[TaskDescription]])serialize each launched TaskDescriptionupdate executor free cores in ExecutorDataMapsend LaunchTask(new SerializableBuffer(serializedTask))loop[ LaunchedTasks ]deserialize TaskDescriptionlaunchTask(this, taskDesc)make new TaskRunner with TaskDescriptionExecutors.newCachedThreadPool.execute(task)run()ShuffleMapTask orResultTaskregisterTask(taskAttemptId)make new TaskContextImpl()runTask(context)runTask(context)deserialize RDD & ShuffleDependencyget writer to write partition datagetOrCompute(partition, context)如果沒有對應RDDBlock,則創建對應的RDDBlock,並以迭代器類型存入BlockManagerwrite(records: Iterator[Product2[K, V]])上一步得到的是RDD的全量數據,在寫出Shuffle數時,會創建ExternalSorter,以遍歷迭代器的方式應用用戶定義的聚合函數Aggregator和關鍵字排序函數Orderingmake new ShuffleBlockIdwrite data file andindex file for thisPartition or TaskserializedResult,MapStatus including BlockManager adress & block sizestatusUpdate(taskId, TaskState.FINISHED, serializedResult)send StatusUpdate(executorId, taskId, state, data)statusUpdate(tid: Long, state: TaskState, serializedData: ByteBuffer)handleSuccessfulTask(tid: Long, result: DirectTaskResult[_])taskEnded(tasks(index), Success...)send CompletionEventhandleTaskCompletion(CompletionEvent)RDDsparkcontextdagschedulermapoutputtrtaskschedulerimplschedulerbackenddrivertasksetmanagerexecutorbackendexecutorTaskblockmanagerShuffleMapTaskSortShuffleWriter
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章