Spark Core源碼精讀計劃#2:SparkContext組件初始化

目錄

前言

SparkContext在整個Spark Core中的地位毋庸置疑,可以說是核心中的核心。它存在于Driver中,是Spark功能的主要入口,如果沒有SparkContext,我們的應用就無法運行,也就無從享受Spark為我們帶來的種種便利。

由于SparkContext類的內容較多(整個SparkContext.scala文件共有2900多行),因此我們不追求畢其功于一役,而是拆成三篇文章來討論。本文主要研究SparkContext初始化過程中涉及到的那些Spark組件,并對它們進行介紹。

SparkContext類的構造方法

SparkContext類接收SparkConf作為構造參數,并且有多種輔助構造方法的實現,比較簡單,不多廢話了。

代碼#2.1 - o.a.s.SparkContext類的輔助構造方法

class SparkContext(config: SparkConf) extends Logging {
  // ...
  
  def this() = this(new SparkConf())

  def this(master: String, appName: String, conf: SparkConf) =
    this(SparkContext.updatedConf(conf, master, appName))

  def this(
      master: String,
      appName: String,
      sparkHome: String = null,
      jars: Seq[String] = Nil,
      environment: Map[String, String] = Map()) = {
    this(SparkContext.updatedConf(new SparkConf(), master, appName, sparkHome, jars, environment))
  }

  private[spark] def this(master: String, appName: String) =
    this(master, appName, null, Nil, Map())

  private[spark] def this(master: String, appName: String, sparkHome: String) =
    this(master, appName, sparkHome, Nil, Map())

  private[spark] def this(master: String, appName: String, sparkHome: String, jars: Seq[String]) =
    this(master, appName, sparkHome, jars, Map())

  // ...
}

而其主構造方法主要由一個巨大的try-catch塊組成,位于SparkContext.scala的362~586行,它內部包含了很多初始化邏輯。

SparkContext初始化的組件

在上述try-catch塊的前方,有一批預先聲明的私有變量字段,且基本都重新定義了對應的Getter方法。它們用于維護SparkContext需要初始化的所有組件的內部狀態。下面的代碼清單將它們預先整理出來。

代碼#2.2 - SparkContext中的組件字段及Getter

  private var _conf: SparkConf = _
  private var _listenerBus: LiveListenerBus = _
  private var _env: SparkEnv = _
  private var _statusTracker: SparkStatusTracker = _
  private var _progressBar: Option[ConsoleProgressBar] = None
  private var _ui: Option[SparkUI] = None
  private var _hadoopConfiguration: Configuration = _
  private var _schedulerBackend: SchedulerBackend = _
  private var _taskScheduler: TaskScheduler = _
  private var _heartbeatReceiver: RpcEndpointRef = _
  @volatile private var _dagScheduler: DAGScheduler = _
  private var _eventLogger: Option[EventLoggingListener] = None
  private var _executorAllocationManager: Option[ExecutorAllocationManager] = None
  private var _cleaner: Option[ContextCleaner] = None
  private var _statusStore: AppStatusStore = _

  private[spark] def conf: SparkConf = _conf
  private[spark] def listenerBus: LiveListenerBus = _listenerBus
  private[spark] def env: SparkEnv = _env
  def statusTracker: SparkStatusTracker = _statusTracker
  private[spark] def progressBar: Option[ConsoleProgressBar] = _progressBar
  private[spark] def ui: Option[SparkUI] = _ui
  def hadoopConfiguration: Configuration = _hadoopConfiguration
  private[spark] def schedulerBackend: SchedulerBackend = _schedulerBackend
  private[spark] def taskScheduler: TaskScheduler = _taskScheduler
  private[spark] def taskScheduler_=(ts: TaskScheduler): Unit = {
    _taskScheduler = ts
  }
  private[spark] def dagScheduler: DAGScheduler = _dagScheduler
  private[spark] def dagScheduler_=(ds: DAGScheduler): Unit = {
    _dagScheduler = ds
  }
  private[spark] def eventLogger: Option[EventLoggingListener] = _eventLogger
  private[spark] def executorAllocationManager: Option[ExecutorAllocationManager] = _executorAllocationManager
  private[spark] def cleaner: Option[ContextCleaner] = _cleaner
  private[spark] def statusStore: AppStatusStore = _statusStore

下面,我們按照SparkContext初始化的實際順序,依次對這些組件作簡要的了解,并且會附上一部分源碼。

SparkConf

SparkConf在文章#1已經詳細講過。它其實不算初始化的組件,因為它是構造SparkContext時傳進來的參數。SparkContext會先將傳入的SparkConf克隆一份副本,之后在副本上做校驗(主要是應用名和Master的校驗),及添加其他必須的參數(Driver地址、應用ID等)。這樣用戶就不可以再更改配置項,以保證Spark配置在運行期的不變性。

LiveListenerBus

LiveListenerBus是SparkContext中的事件總線。它異步地將事件源產生的事件(SparkListenerEvent)投遞給已注冊的監聽器(SparkListener)。Spark中廣泛運用了監聽器模式,以適應集群狀態下的分布式事件匯報。

除了它之外,Spark中還有多種事件總線,它們都繼承自ListenerBus特征。事件總線是Spark底層的重要支撐組件,之后會專門分析。

AppStatusStore

AppStatusStore提供Spark程序運行中各項監控指標的鍵值對化存儲。Web UI中見到的數據指標基本都存儲在這里。其初始化代碼如下。

代碼#2.3 - 構造方法中AppStatusStore的初始化

    _statusStore = AppStatusStore.createLiveStore(conf)
    listenerBus.addToStatusQueue(_statusStore.listener.get)

代碼#2.4 - o.a.s.status.AppStatusStore.createLiveStore()方法

  def createLiveStore(conf: SparkConf): AppStatusStore = {
    val store = new ElementTrackingStore(new InMemoryStore(), conf)
    val listener = new AppStatusListener(store, conf, true)
    new AppStatusStore(store, listener = Some(listener))
  }

可見,AppStatusStore底層使用了ElementTrackingStore,它是能夠跟蹤元素及其數量的鍵值對存儲結構,因此適合用于監控。另外還會產生一個監聽器AppStatusListener的實例,并注冊到前述LiveListenerBus中,用來收集監控數據。

SparkEnv

SparkEnv是Spark中的執行環境。Driver與Executor的運行都需要SparkEnv提供的各類組件形成的環境來作為基礎。其初始化代碼如下。

代碼#2.5 - 構造方法中SparkEnv的初始化

    _env = createSparkEnv(_conf, isLocal, listenerBus)
    SparkEnv.set(_env)

代碼#2.6 - o.a.s.SparkContext.createSparkEnv()方法

  private[spark] def createSparkEnv(
      conf: SparkConf,
      isLocal: Boolean,
      listenerBus: LiveListenerBus): SparkEnv = {
    SparkEnv.createDriverEnv(conf, isLocal, listenerBus, SparkContext.numDriverCores(master))
  }

可見,SparkEnv的初始化依賴于LiveListenerBus,并且在SparkContext初始化時只會創建Driver的執行環境,Executor的執行環境就是后話了。在創建Driver執行環境后,會調用SparkEnv伴生對象中的set()方法保存它,這樣就可以“一處創建,多處使用”SparkEnv。

通過SparkEnv管理的組件也有多種,比如SparkContext中就會出現的安全性管理器SecurityManager、RPC環境RpcEnv、存儲塊管理器BlockManager、監控度量系統MetricsSystem。在SparkContext構造方法的后方,就會藉由SparkEnv先初始化BlockManager與啟動MetricsSystem。

代碼#2.7 - 構造方法中BlockManager的初始化與MetricsSystem的啟動

    _env.blockManager.initialize(_applicationId)
    _env.metricsSystem.start()
    _env.metricsSystem.getServletHandlers.foreach(handler => ui.foreach(_.attachHandler(handler)))

由于SparkEnv的重要性和復雜性,后面會專門寫文章來講解它,這里只需要有個大致的了解即可。

SparkStatusTracker

SparkStatusTracker提供報告最近作業執行情況的低級API。它的內部只有6個方法,從AppStatusStore中查詢并返回諸如Job/Stage ID、活躍/完成/失敗的Task數、Executor內存用量等基礎數據。它只能保證非常弱的一致性語義,也就是說它報告的信息會有延遲或缺漏。

ConsoleProgressBar

ConsoleProgressBar按行打印Stage的計算進度。它周期性地從AppStatusStore中查詢Stage對應的各狀態的Task數,并格式化成字符串輸出。它可以通過spark.ui.showConsoleProgress參數控制開關,默認值false。

SparkUI

SparkUI維護監控數據在Spark Web UI界面的展示。它的樣子在文章#0的圖中已經出現過,因此不再贅述。其初始化代碼如下。

代碼#2.8 - 構造方法中SparkUI的初始化

    _ui =
      if (conf.getBoolean("spark.ui.enabled", true)) {
        Some(SparkUI.create(Some(this), _statusStore, _conf, _env.securityManager, appName, "",
          startTime))
      } else {
        None
      }
    _ui.foreach(_.bind())

可以通過spark.ui.enabled參數來控制是否啟用Spark UI,默認值true。然后調用SparkUI的父類WebUI的bind()方法,將Spark UI綁定到特定的host:port上,如文章#0中的localhost:4040。

(Hadoop)Configuration

Spark可能會依賴于Hadoop的一些組件運行,如HDFS和YARN,Spark官方也有針對Hadoop 2.6/2.7的預編譯包供下載。

SparkContext會借助工具類SparkHadoopUtil初始化一些與Hadoop有關的配置,存放在Hadoop的Configuration實例中,如Amazon S3相關的配置,和以“spark.hadoop.”為前綴的Spark配置參數。

HeartbeatReceiver

HeartbeatReceiver是心跳接收器。Executor需要向Driver定期發送心跳包來表示自己存活。它本質上也是個監聽器,繼承了SparkListener。其初始化代碼如下。

代碼#2.9 - 構造方法中HeartbeatReceiver的初始化

    _heartbeatReceiver = env.rpcEnv.setupEndpoint(
      HeartbeatReceiver.ENDPOINT_NAME, new HeartbeatReceiver(this))

可見,HeartbeatReceiver通過RpcEnv最終包裝成了一個RPC端點的引用,即代碼#2.2中的RpcEndpointRef。

Spark集群的節點間必然會涉及大量的網絡通信,心跳機制只是其中的一方面而已。因此RPC框架同事件總線一樣,是Spark底層不可或缺的組成部分。

SchedulerBackend

SchedulerBackend負責向等待計算的Task分配計算資源,并在Executor上啟動Task。它是一個Scala特征,有多種部署模式下的SchedulerBackend實現類。它在SparkContext中是和TaskScheduler一起初始化的,作為一個元組返回。

TaskScheduler

TaskScheduler即任務調度器。它也是一個Scala特征,但只有一種實現,即TaskSchedulerImpl類。它負責提供Task的調度算法,并且會持有SchedulerBackend的實例,通過SchedulerBackend發揮作用。它們兩個的初始化代碼如下。

代碼#2.10 - 構造方法中SchedulerBackend與TaskScheduler的初始化

    val (sched, ts) = SparkContext.createTaskScheduler(this, master, deployMode)
    _schedulerBackend = sched
    _taskScheduler = ts

下面這個方法比較長,但就當做是提前知道一下Spark的幾種Master設置方式吧。它包括有三種本地模式、本地集群模式、Standalone模式,以及第三方集群管理器(如YARN)提供的模式。

代碼#2.11 - o.a.s.SparkContext.createTaskScheduler()方法

  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)

      case LOCAL_N_REGEX(threads) =>
        def localCpuCount: Int = Runtime.getRuntime.availableProcessors()
        // local[*] estimates the number of cores on the machine; local[N] uses exactly N threads.
        val threadCount = if (threads == "*") localCpuCount else threads.toInt
        if (threadCount <= 0) {
          throw new SparkException(s"Asked to run locally with $threadCount 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) =>
        def localCpuCount: Int = Runtime.getRuntime.availableProcessors()
        // local[*, M] means the number of cores on the computer with M failures
        // local[N, M] means exactly N threads with M failures
        val threadCount = if (threads == "*") localCpuCount else threads.toInt
        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) =>
        // Check to make sure memory requested <= memoryPerSlave. Otherwise Spark will just hang.
        val memoryPerSlaveInt = memoryPerSlave.toInt
        if (sc.executorMemory > memoryPerSlaveInt) {
          throw new SparkException(
            "Asked to launch cluster with %d MB RAM / worker but requested %d MB/worker".format(
              memoryPerSlaveInt, sc.executorMemory))
        }

        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)
        }
    }
  }

DAGScheduler

DAGScheduler即有向無環圖(DAG)調度器。DAG的概念在文章#0中已經講過,用來表示RDD之間的血緣。DAGScheduler負責生成并提交Job,以及按照DAG將RDD和算子劃分并提交Stage。每個Stage都包含一組Task,稱為TaskSet,它們被傳遞給TaskScheduler。也就是說DAGScheduler需要先于TaskScheduler進行調度。

DAGScheduler初始化是直接new出來的,但在其構造方法里也會將SparkContext中TaskScheduler的引用傳進去。因此要等DAGScheduler創建后,再真正啟動TaskScheduler。

代碼#2.12 - 構造方法中DAGScheduler的初始化和TaskScheduler的啟動

    _dagScheduler = new DAGScheduler(this)
    _heartbeatReceiver.ask[Boolean](TaskSchedulerIsSet)
    _taskScheduler.start()

SchedulerBackend、TaskScheduler與DAGScheduler是Spark調度邏輯的主要組成部分,之后會深入探索它們的細節。

值得注意的是,代碼#2.2中只有TaskScheduler與DAGScheduler還定義了Setter方法,目前只在內部測試方法中調用過。

EventLoggingListener

EventLoggingListener是用于事件持久化的監聽器。它可以通過spark.eventLog.enabled參數控制開關,默認值false。如果開啟,它也會注冊到LiveListenerBus里,并將特定的一部分事件寫到磁盤。

ExecutorAllocationManager

ExecutorAllocationManager即Executor分配管理器。它可以通過spark.dynamicAllocation.enabled參數控制開關,默認值false。如果開啟,并且SchedulerBackend的實現類支持這種機制,Spark就會根據程序運行時的負載動態增減Executor的數量。它的初始化代碼如下。

代碼#2.13 - 構造方法中ExecutorAllocationManager的初始化

    val dynamicAllocationEnabled = Utils.isDynamicAllocationEnabled(_conf)
    _executorAllocationManager =
      if (dynamicAllocationEnabled) {
        schedulerBackend match {
          case b: ExecutorAllocationClient =>
            Some(new ExecutorAllocationManager(
              schedulerBackend.asInstanceOf[ExecutorAllocationClient], listenerBus, _conf,
              _env.blockManager.master))
          case _ =>
            None
        }
      } else {
        None
      }
    _executorAllocationManager.foreach(_.start())

ContextCleaner

ContextCleaner即上下文清理器。它可以通過spark.cleaner.referenceTracking參數控制開關,默認值true。它內部維護著對RDD、Shuffle依賴和廣播變量(之后會提到)的弱引用,如果弱引用的對象超出程序的作用域,就異步地將它們清理掉。

總結

本文從SparkContext的構造方法入手,按順序簡述了十余個Spark內部組件及其初始化邏輯。這些組件覆蓋了Spark機制的多個方面,我們之后在適當的時機還要深入研究其中的一部分,特別重要的如事件總線LiveListenerBus、執行環境SparkEnv、調度器TaskScheduler及DAGScheduler等。

最后用一張圖來概括吧。

SparkContext初始化順序

除了組件初始化之外,SparkContext內還有其他一部分有用的屬性。并且在初始化的主流程做完之后,也有不少善后工作要做。下一篇文章就會分析它們。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容