Spark Driver Program剖析

|舊市拾荒|發表於2020-09-19

  SparkContext是通往Spark叢集的唯一入口,是整個Application執行排程的核心。

一、Spark Driver Program

  Spark Driver Program(以下簡稱Driver)是執行Application的main函式並且新建SparkContext例項的程式。其實,初始化SparkContext是為了準備Spark應用程式的執行環境,在Spark中,由SparkContext負責與叢集進行通訊、資源的申請、任務的分配和監控等。當Worker節點中的Executor執行完畢Task後,Driver同時負責將SparkContext關閉。通常也可以使用SparkContext來代表驅動程式(Driver)。

  Driver(SparkContext)整體架構圖如下所示。

   Spark Driver Program剖析

二、SparkContext深度剖析

  SparkContext是通往Spark叢集的唯一入口,可以用來在Spark叢集中建立RDDs、累加器(Accumulators)和廣播變數(Broadcast Variables)。SparkContext也是整個Spark應用程式(Application)中至關重要的一個物件,可以說是整個Application執行排程的核心(不是指資源排程)。

  SparkContext的核心作用是初始化Spark應用程式執行所需要的核心元件,包括高層排程器(DAGScheduler)、底層排程器(TaskScheduler)和排程器的通訊終端(SchedulerBackend),同時還會負責Spark程式向Master註冊程式等。

  一般而言,通常為了測試或者學習Spark開發一個Application,在Application的main方法中,最開始幾行編寫的程式碼一般是這樣的:

  首先,建立SparkConf例項,設定SparkConf例項的屬性,以便覆蓋Spark預設配置檔案spark-env.sh,spark-default.sh和log4j.properties中的引數;

  然後,SparkConf例項作為SparkContext類的唯一構造引數來例項化SparkContext例項物件。SparkContext在例項化的過程中會初始化DAGScheduler、TaskScheduler和SchedulerBackend,而當RDD的action觸發了作業(Job)後,SparkContext會呼叫DAGScheduler將整個Job劃分成幾個小的階段(Stage),TaskScheduler會排程每個Stage的任務(Task)進行處理。

  還有,SchedulerBackend管理整個叢集中為這個當前的Application分配的計算資源,即Executor。

  如果用一個車來比喻Spark Application,那麼SparkContext就是車的引擎,而SparkConf是關於引擎的配置引數。說明:只可以有一個SparkContext例項執行在一個JVM記憶體中,所以在建立新的SparkContext例項前,必須呼叫stop方法停止當前JVM唯一執行的SparkContext例項。

  Spark程式在執行時分為Driver和Executor兩部分:Spark程式編寫是基於SparkContext的,具體包含兩方面。

  Spark程式設計的核心基礎RDD是由SparkContext最初建立的(第一個RDD一定是由SparkContext建立的)。

  Spark程式的排程優化也是基於SparkContext,首先進行排程優化。

  Spark程式的註冊是通過SparkContext例項化時生產的物件來完成的(其實是SchedulerBackend來註冊程式)。

  Spark程式在執行時要通過Cluster Manager獲取具體的計算資源,計算資源獲取也是通過SparkContext產生的物件來申請的(其實是SchedulerBackend來獲取計算資源的)。

  SparkContext崩潰或者結束的時候,整個Spark程式也結束。

三、SparkContext原始碼解析

  SparkContext是Spark應用程式的核心。我們執行WordCount程式,通過日誌來深入瞭解SparkContext。

  WordCount.scala的程式碼如下。

import org.apache.spark.SparkConf
import org.apache.spark.SparkContext
import org.apache.spark.rdd.HadoopRDD
import org.apache.log4j.Logger
import org.apache.log4j.Level

object wordcount {
  def main(args: Array[String]): Unit = {
    Logger.getLogger("org").setLevel(Level.ALL);
    
    // 第1步:建立Spark的配置物件SparkConf,設定Spark程式執行時的配置資訊,
    val conf = new SparkConf().setAppName("My First Spark APP").setMaster("local")
    
    // 第2步:建立SparkContext物件
    val sc = new SparkContext(conf)
    
    // 第3步:根據具體的資料來源來建立RDD
    val lines = sc.textFile("helloSpark.txt", 1)
    
    // 第4步:對初始的RDD進行Transformation級別的處理,如通過map、filter等
    val words = lines.flatMap{line=>line.split(" ")}
    val pairs = words.map{word=>(word,1)}
    pairs.cache()
    val wordCountsOdered = pairs.reduceByKey(_+_).saveAsTextFile("wordCountResult.log")
//    val wordCountsOdered = pairs.reduceByKey(_+_).map(
//      pair=>(pair._2,pair._1)    
//    ).sortByKey(false).map(pair=>(pair._2,pair._1))
//    wordCountsOdered.collect.foreach(wordNumberPair=>println(wordNumberPair._1+" : "+wordNumberPair._2))
//    while(true){
//      
//    }
    sc.stop()
    
    
  }
}

  在Eclipse中執行wordcount程式碼,日誌顯示如下:

   Spark Driver Program剖析

  程式一開始,日誌裡顯示的是:INFO SparkContext: Running Spark version 2.0.1,日誌中間部分是一些隨著SparkContext建立而建立的物件,另一條比較重要的日誌資訊,作業啟動了並正在執行:INFO SparkContext: Starting job: saveAsTextFile at WordCountJobRuntime.scala:58。

  在程式執行的過程中會建立TaskScheduler、DAGScheduler和SchedulerBackend,它們有各自的功能。DAGScheduler是面向Job的Stage的高層排程器;TaskScheduler是底層排程器。SchedulerBackend是一個介面,根據具體的ClusterManager的不同會有不同的實現。程式列印結果後便開始結束。日誌顯示:INFO SparkContext: Successfully stopped SparkContext。

   Spark Driver Program剖析

  通過這個例子可以感受到Spark程式的執行到處都可以看到SparkContext的存在,我們將SparkContext作為Spark原始碼閱讀的入口,來理解Spark的所有內部機制。

  我們從一個整體去看SparkContext建立的例項物件。首先,SparkContext構建的頂級三大核心為DAGScheduler、TaskScheduler、SchedulerBackend,其中,DAGScheduler是面向Job的Stage的高層排程器;TaskScheduler是一個介面,是底層排程器,根據具體的ClusterManager的不同會有不同的實現,Standalone模式下具體的實現是TaskSchedulerImpl。SchedulerBackend是一個介面,根據具體的ClusterManager的不同會有不同的實現。Standalone模式下具體的實現是StandaloneSchedulerBackend。下圖為SparkContext整體執行圖

   Spark Driver Program剖析

  從整個程式執行的角度講,SparkContext包含四大核心物件:DAGScheduler、TaskScheduler、SchedulerBackend、MapOutputTrackerMaster。StandaloneSchedulerBackend有三大核心功能:負責與Master連線,註冊當前程式RegisterWithMaster;接收叢集中為當前應用程式分配的計算資源Executor的註冊並管理Executors;負責傳送Task到具體的Executor執行。

  第一步:程式一開始執行時會例項化SparkContext裡的物件,所有不在方法裡的成員都會被例項化!一開始例項化時第一個關鍵的程式碼是createTaskScheduler,它位於SparkContext的PrimaryConstructor中,當它例項化時會直接被呼叫,這個方法返回的是taskScheduler和dagScheduler的例項,然後基於這個內容又構建了DAGScheduler,最後呼叫taskScheduler的start()方法。要先建立taskScheduler,然後再建立dagScheduler,因為taskScheduler是受dagScheduler管理的。

  SparkContext.scala的原始碼如下。

    // Create and start the scheduler
    val (sched, ts) = SparkContext.createTaskScheduler(this, master, deployMode)
    _schedulerBackend = sched
    _taskScheduler = ts
    _dagScheduler = new DAGScheduler(this)
    _heartbeatReceiver.ask[Boolean](TaskSchedulerIsSet)

    // start TaskScheduler after taskScheduler sets DAGScheduler reference in DAGScheduler's
    // constructor
    _taskScheduler.start()

  第二步:呼叫createTaskScheduler,這個方法建立了TaskSchedulerImpl和StandaloneSchedulerBackend,createTaskScheduler方法的第一個入參是SparkContext,傳入的this物件是在應用程式中建立的sc,第二個入參是master的地址。

  以下是wordcount.scala建立SparkConf和SparkContext的上下文資訊

    // 第1步:建立Spark的配置物件SparkConf,設定Spark程式執行時的配置資訊,
    val conf = new SparkConf().setAppName("My First Spark APP").setMaster("local")
    
    // 第2步:建立SparkContext物件
    val sc = new SparkContext(conf)

  當SparkContext呼叫createTaskScheduler方法時,根據叢集的條件建立不同的排程器,例如,createTaskScheduler第二個入參master如傳入local引數,SparkContext將建立TaskSchedulerImpl例項及LocalSchedulerBackend例項,在測試程式碼的時候,可以嘗試傳入local[*]或者是local[2]的引數,然後跟蹤程式碼,看看建立了什麼樣的例項物件。

  SparkContext中的SparkMasterRegex物件定義不同的正規表示式,從master字串中根據正規表示式適配master資訊。

  SparkContext.scala的原始碼如下。

/**
 * A collection of regexes for extracting information from the master string.
 */
private object SparkMasterRegex {
  // Regular expression used for local[N] and local[*] master formats
  val LOCAL_N_REGEX = """local\[([0-9]+|\*)\]""".r
  // Regular expression for local[N, maxRetries], used in tests with failing tasks
  val LOCAL_N_FAILURES_REGEX = """local\[([0-9]+|\*)\s*,\s*([0-9]+)\]""".r
  // Regular expression for simulating a Spark cluster of [N, cores, memory] locally
  val LOCAL_CLUSTER_REGEX = """local-cluster\[\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*]""".r
  // Regular expression for connecting to Spark deploy clusters
  val SPARK_REGEX = """spark://(.*)""".r
  // Regular expression for connection to Mesos cluster by mesos:// or mesos://zk:// url
  val MESOS_REGEX = """mesos://(.*)""".r
}

  這是設計模式中的策略模式,它會根據實際需要建立出不同的SchedulerBackend的子類。

  SparkContext.scala的createTaskScheduler方法的原始碼如下。

  /**
   * Create a task scheduler based on a given master URL.
   * Return a 2-tuple of the scheduler backend and the task scheduler.
   */
  private def createTaskScheduler(
      sc: SparkContext,
      master: String,
      deployMode: String): (SchedulerBackend, TaskScheduler) = {
    import SparkMasterRegex._

    // When running locally, don't try to re-execute tasks on failure.
    val MAX_LOCAL_TASK_FAILURES = 1

    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)

  在實際生產環境下,我們都是用叢集模式,即以spark://開頭,此時在程式執行時,框架會建立一個TaskSchedulerImpl和StandaloneSchedulerBackend的例項,在這個過程中也會初始化taskscheduler,把StandaloneSchedulerBackend的例項物件作為引數傳入。StandaloneSchedulerBackend被TaskSchedulerImpl管理,最後返回TaskScheduler和StandaloneSchdeulerBackend。

  SparkContext.scala的原始碼如下。

      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)

  createTaskScheduler方法執行完畢後,呼叫了taskscheduler.start()方法來正式啟動taskscheduler,這裡雖然呼叫了taskscheduler.start方法,但實際上是呼叫了taskSchedulerImpl的start方法,因為taskSchedulerImpl是taskScheduler的子類。

  Task預設失敗重試次數是4次,如果任務不容許失敗,就可以調大這個引數。調大spark.task.maxFailures引數有助於確保重要的任務失敗後可以重試多次。

  初始化TaskSchedulerImpl:呼叫createTaskScheduler方法時會初始化TaskSchedulerImpl,然後把StandaloneSchedulerBackend當作引數傳進去,初始化TaskSchedulerImpl時首先是建立一個Pool來初定義資源分佈的模式Scheduling Mode,預設是先進先出(FIFO)的模式。

  回到taskScheduler start方法,taskScheduler.start方法呼叫時會再呼叫schedulerbackend的start方法。

  SchedulerBackend包含多個子類,分別是LocalSchedulerBackend、CoarseGrainedScheduler-Backend和StandaloneSchedulerBackend、MesosCoarseGrainedSchedulerBackend、YarnScheduler-Backend。

  StandaloneSchedulerBackend的start方法呼叫了CoarseGraninedSchedulerBackend的start方法,通過StandaloneSchedulerBackend註冊程式把command提交給Master:Command ("org.apache.spark.executor.CoarseGrainedExecutorBackend", args, sc.executorEnvs, classPathEntries ++ testingClassPath, libraryPathEntries, javaOpts)來建立一個StandaloneAppClient的例項。

  Master發指令給Worker去啟動Executor所有的程式時載入的Main方法所在的入口類就是command中的CoarseGrainedExecutorBackend,在CoarseGrainedExecutorBackend中啟動Executor(Executor是先註冊,再例項化),Executor通過執行緒池併發執行Task,然後再呼叫它的run方法。

  回到StandaloneSchedulerBackend.scala的start方法:其中建立了一個很重要的物件,即StandaloneAppClient物件,然後呼叫它的client.start()方法。

  在start方法中建立一個ClientEndpoint物件。

  StandaloneAppClient.scala的star方法的原始碼如下。

  def start() {
    // Just launch an rpcEndpoint; it will call back into the listener.
    endpoint.set(rpcEnv.setupEndpoint("AppClient", new ClientEndpoint(rpcEnv)))
  }

  ClientEndpoint是一個RpcEndPoint,首先呼叫自己的onStart方法,接下來向Master註冊。

  呼叫registerWithMaster方法,從registerWithMaster呼叫tryRegisterAllMasters,開一條新的執行緒來註冊,然後傳送一條資訊(RegisterApplication的case class)給Master。

  Master收到RegisterApplication資訊後便開始註冊,註冊後再次呼叫schedule()方法。

四、總結

  從SparkContext建立taskSchedulerImpl初始化不同的例項物件來完成最終向Master註冊的任務,中間包括呼叫scheduler的start方法和建立StandaloneAppClient來間接建立ClientEndPoint完成註冊工作。

  我們把SparkContext稱為天堂之門,SparkContext開啟天堂之門:Spark程式是通過SparkContext釋出到Spark叢集的;SparkContext導演天堂世界:Spark程式的執行都是在SparkContext為核心的排程器的指揮下進行的;SparkContext關閉天堂之門:SparkContext崩潰或者結束的時候整個Spark程式也結束。 

  

  

  

  

相關文章