Spark源碼剖析——SparkContext實例化

Spark源碼剖析——SparkContext實例化

當前環境與版本

環境 版本
JDK java version “1.8.0_231” (HotSpot)
Scala Scala-2.11.12
Spark spark-2.4.4

前言

  • 在前面SparkSubmit提交流程一篇中,我們提到無論是哪一種部署模式,最終都會調用用戶編寫的class類的main方法。而在main方法中,顯然我們會對SparkContext進行實例化。本篇主要的關注點就是SparkContext的實例化過程。
  • SparkContext是整個Spark應用的上下文環境,無論是直接利用new實例化SparkContext,還是構建一個SparkSession,整個Spark應用都會實例化一個SparkContext。
  • 查看SparkContext源碼文檔註釋可知,其主要有以下幾個關鍵點
    • SparkContext代表了對於一個集羣的連接
    • 可以用於創建RDDs、accumulators、broadcast variables
    • 一個JVM只能存在一個SparkContext(未來或許會移除該限制)
  • 可以看出,SparkContext是讓用戶編寫的處理邏輯在集羣中運行的關鍵,利用它連接並操作整個集羣,才能夠實現分佈式計算邏輯。
  • 下面我們就來分析其源碼,看看SparkContext是如何被實例化的。

SparkContext實例化的主要邏輯

  • org.apache.spark.SparkContext
  • 我們先直接看SparkContext的class部分,因爲即便調用其伴生對象的getOrCreate方法同樣還是會實例化SparkContext。所以,我們直接看到其構造部分即可。
  • 但是,這部分對於不太懂Scala的朋友其實是難以找到切入點的。因爲Java中實例化對象會直接調用其構造器中的代碼,然而Scala中卻找不到,只能看到在class處傳入一個SparkConf作爲構造器的參數。其問題點在於在Scala中其class 類名 {...}中的代碼都是其構造實例化的一部分,我們需要由上往下查看其代碼。
  • 理解了這點,我們來看其構造的關鍵部分,代碼如下
    class SparkContext(config: SparkConf) extends Logging {
    
      try { // 第363行,Spark版本2.4.4
        // 省略部分代碼
      } catch {
        // 省略部分代碼
      }
    
    }
    
  • 因爲代碼較多,沒全展示,請先找到try這部分的代碼位置,我們一部分一部分來分析。
  • 配置部分代碼如下(第364行~414行)
       // 此處的config就是我們傳入的SparkConfig
        _conf = config.clone()
        // 進行配置校驗,主要針對一些不合法的或者遺留參數
        // 例如內存相關的spark.storage.memoryFraction、spark.shuffle.memoryFraction
        // 例如指定部署模式的參數yarn-client、yarn-cluster
        _conf.validateSettings()
    
        // 如果參數沒有master,拋出異常
        if (!_conf.contains("spark.master")) {
          throw new SparkException("A master URL must be set in your configuration")
        }
        // 如果參數不帶應用名,拋出異常
        if (!_conf.contains("spark.app.name")) {
          throw new SparkException("An application name must be set in your configuration")
        }
    
        // log out spark.app.name in the Spark driver logs
        logInfo(s"Submitted application: $appName")
    
        // 如果應用運行在YARN的ApplicationMaster時,必須擁有其id,否則拋出異常
        if (master == "yarn" && deployMode == "cluster" && !_conf.contains("spark.yarn.app.id")) {
          throw new SparkException("Detected yarn cluster mode, but isn't running on a cluster. " +
            "Deployment to YARN is not supported directly by SparkContext. Please use spark-submit.")
        }
    
        if (_conf.getBoolean("spark.logConf", false)) {
          logInfo("Spark configuration:\n" + _conf.toDebugString)
        }
    
        // 明確的指出Driver的IP和端口,不依賴於默認值
        _conf.set(DRIVER_HOST_ADDRESS, _conf.get(DRIVER_HOST_ADDRESS))
        _conf.setIfMissing("spark.driver.port", "0")
        
        _conf.set("spark.executor.id", SparkContext.DRIVER_IDENTIFIER)
    
    	// 獲取到jar的路徑,由spark.jars指定
        _jars = Utils.getUserJars(_conf)
        // 獲取工作目錄
        _files = _conf.getOption("spark.files").map(_.split(",")).map(_.filter(_.nonEmpty))
          .toSeq.flatten
    
    	// 事件日誌目錄
        _eventLogDir =
          if (isEventLogEnabled) { // 默認關閉,爲false
            val unresolvedDir = conf.get("spark.eventLog.dir", EventLoggingListener.DEFAULT_LOG_DIR)
              .stripSuffix("/")
            Some(Utils.resolveURI(unresolvedDir))
          } else {
            None
          }
    
    	// 事件日誌的壓縮配置,默認關閉
        _eventLogCodec = {
          val compress = _conf.getBoolean("spark.eventLog.compress", false)
          if (compress && isEventLogEnabled) {
            Some(CompressionCodec.getCodecName(_conf)).map(CompressionCodec.getShortName)
          } else {
            None
          }
        }
    
  • 可以看到此部分代碼主要和配置相關,其中有很多我們比較熟悉的點,例如
    • _conf.validateSettings()中對遺留模式進行了校驗、對傳入的參數yarn-client/cluster的方式進行了校驗,併發出了提示(如果你從Spark1轉入2,繼續使用以前的參數,肯定會遇到過這些提示)
    • !_conf.contains("spark.master")!_conf.contains("spark.app.name")所拋出的異常對於初學Spark的朋友一定不會陌生
    • _jars部分解析的其實也就是我們利用spark.jars進行指定一些jar包
  • Spark事件監聽部分代碼如下(第416行~421行)
        // 實例化ListenerBus
        // 由後面第555行調用setupAndStartListenerBus()啓動
        _listenerBus = new LiveListenerBus(_conf)
    
        // 初始化用於所有事件的狀態存儲
        _statusStore = AppStatusStore.createLiveStore(conf)
        listenerBus.addToStatusQueue(_statusStore.listener.get)
    
  • LiveListenerBus實例化內容較多,我們後面再說
  • SparkContext的核心代碼1,SparkEnv的創建,如下(第423行~425行)
        // Create the Spark execution environment (cache, map output tracker, etc)
        _env = createSparkEnv(_conf, isLocal, listenerBus)
        SparkEnv.set(_env)
    
  • SparkEnv創建內容較多,我們後面再說
  • 再是一部分配置項,代碼如下(第427行~485行)
        // REPL模式下(也就是spark-shell),註冊輸出目錄
        _conf.getOption("spark.repl.class.outputDir").foreach { path =>
          val replUri = _env.rpcEnv.fileServer.addDirectory("/classes", new File(path))
          _conf.set("spark.repl.class.uri", replUri)
        }
    	// 實例化狀態追蹤器,用於監控job、stage的進度
        _statusTracker = new SparkStatusTracker(this, _statusStore)
    	
    	// 是否顯示進度條,配置項爲spark.ui.showConsoleProgress
    	// 在client模式下提交應用後,會在當前console顯示應用執行進度,一般會改爲true
        _progressBar =
          if (_conf.get(UI_SHOW_CONSOLE_PROGRESS) && !log.isInfoEnabled) {
            Some(new ConsoleProgressBar(this))
          } else {
            None
          }
    
       // 創建SparkUI,使用的是Jetty
        _ui =
          if (conf.getBoolean("spark.ui.enabled", true)) {
            // 調用工廠方法,創建SparkUI
            Some(SparkUI.create(Some(this), _statusStore, _conf, _env.securityManager, appName, "",
              startTime))
          } else {
            // For tests, do not enable the UI
            None
          }
        // 綁定,正式啓動Jetty服務
        _ui.foreach(_.bind())
    
    	// 獲取到hadoop相關的配置
        _hadoopConfiguration = SparkHadoopUtil.get.newConfiguration(_conf)
    
        // Add each JAR given through the constructor
        if (jars != null) {
          jars.foreach(addJar)
        }
    
        if (files != null) {
          files.foreach(addFile)
        }
    	
    	// 獲取executor的內存
        _executorMemory = _conf.getOption("spark.executor.memory")
          .orElse(Option(System.getenv("SPARK_EXECUTOR_MEMORY")))
          .orElse(Option(System.getenv("SPARK_MEM"))
          .map(warnSparkMem))
          .map(Utils.memoryStringToMb)
          .getOrElse(1024)
    
        // 轉換系統的環境變量爲配置
        for { (envKey, propKey) <- Seq(("SPARK_TESTING", "spark.testing"))
          value <- Option(System.getenv(envKey)).orElse(Option(System.getProperty(propKey)))} {
          executorEnvs(envKey) = value
        }
        Option(System.getenv("SPARK_PREPEND_CLASSES")).foreach { v =>
          executorEnvs("SPARK_PREPEND_CLASSES") = v
        }
        executorEnvs("SPARK_EXECUTOR_MEMORY") = executorMemory + "m"
        executorEnvs ++= _conf.getExecutorEnv
        executorEnvs("SPARK_USER") = sparkUser
    
  • 接下來,是SparkContext的核心代碼2了,如下(第489行~501行)
        // 創建一個HeartbeatReceiver的Endpoint,並註冊至rpcEnv
        // 首先其onStart會被調用,其中啓動了一個定時器,定時向自己發送ExpireDeadHosts消息
        // 自己收到消息後會調用expireDeadHosts()方法,會移除掉心跳超時的executor
        _heartbeatReceiver = env.rpcEnv.setupEndpoint(
          HeartbeatReceiver.ENDPOINT_NAME, new HeartbeatReceiver(this))
    
        // 根據不同的部署模式創建不同的SchedulerBackend、TaskScheduler,後面再來講該部分代碼
        val (sched, ts) = SparkContext.createTaskScheduler(this, master, deployMode)
        _schedulerBackend = sched
        _taskScheduler = ts
        // 創建DAGScheduler
        _dagScheduler = new DAGScheduler(this)
        _heartbeatReceiver.ask[Boolean](TaskSchedulerIsSet)
    
        // 啓動 TaskScheduler
        _taskScheduler.start()
    
  • 我們再接着看一部分代碼(第503行~516行)
        // 進行一些應用Id相關的設置
        _applicationId = _taskScheduler.applicationId()
        _applicationAttemptId = taskScheduler.applicationAttemptId()
        _conf.set("spark.app.id", _applicationId)
        // 檢測是否是反向代理模式,默認false
        if (_conf.getBoolean("spark.ui.reverseProxy", false)) {
          System.setProperty("spark.ui.proxyBase", "/proxy/" + _applicationId)
        }
        // 向SparkUI設置應用id
        _ui.foreach(_.setAppId(_applicationId))
        // 爲應用初始化BlockManager
        _env.blockManager.initialize(_applicationId)
    
        // 爲該id的應用啓用度量系統
        _env.metricsSystem.start()
        _env.metricsSystem.getServletHandlers.foreach(handler => ui.foreach(_.attachHandler(handler)))
    
  • 再往後,主要關注最後三行代碼
        // 啓動事件日誌器
        _eventLogger =
          if (isEventLogEnabled) {
            val logger =
              new EventLoggingListener(_applicationId, _applicationAttemptId, _eventLogDir.get,
                _conf, _hadoopConfiguration)
            logger.start()
            listenerBus.addToEventLogQueue(logger)
            Some(logger)
          } else {
            None
          }
    
        // 是否啓動動態申請,默認關閉
        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())
    
        // 上下文清理器,主要利用了WeakReference
        // ContextCleaner內部會週期性調用System.gc(),因此請不要爲JVM設置 -XX:-DisableExplicitGC
        _cleaner =
          if (_conf.getBoolean("spark.cleaner.referenceTracking", true)) {
            Some(new ContextCleaner(this))
          } else {
            None
          }
        _cleaner.foreach(_.start())
        
        // 註冊監聽器,並啓動ListenerBus
        setupAndStartListenerBus()
        // 利用ListenerBus發送環境更新事件
        postEnvironmentUpdate()
        // 利用ListenerBus發送應用啓動的消息
        postApplicationStart()
    
  • 至此,SparkContext實例化的主要部分邏輯結束。下面我們來看其中的細節,關於LiveListenerBus、SparkEnv、SchedulerBackend、TaskScheduler、DAGScheduler的部分代碼。

LiveListenerBus的作用

  • org.apache.spark.scheduler.LiveListenerBus
  • LiveListenerBus該類主要用於消息的訂閱/發佈,其代碼主要包含以下部分
    private[spark] class LiveListenerBus(conf: SparkConf) {
    
      // 包含多個消息隊列的列表
      private val queues = new CopyOnWriteArrayList[AsyncEventQueue]()
    
      private[spark] def addToQueue(
          listener: SparkListenerInterface,
          queue: String): Unit = synchronized {
        if (stopped.get()) {
          throw new IllegalStateException("LiveListenerBus is stopped.")
        }
    
        queues.asScala.find(_.name == queue) match {
          case Some(queue) => // 添加監聽器到對應name的隊列
            queue.addListener(listener)
    
          case None => // 沒有的話就新建一個AsyncEventQueue,並添加監聽
            val newQueue = new AsyncEventQueue(queue, conf, metrics, this)
            newQueue.addListener(listener)
            if (started.get()) {
              newQueue.start(sparkContext)
            }
            queues.add(newQueue)
        }
      }
    
      private def postToQueues(event: SparkListenerEvent): Unit = {
        // 發送消息到所有隊列
        val it = queues.iterator()
        while (it.hasNext()) {
          it.next().post(event)
        }
      }
    
      def start(sc: SparkContext, metricsSystem: MetricsSystem): Unit = synchronized {
        // 由SparkContext實例化的最後調用(第555行)
        if (!started.compareAndSet(false, true)) {
          throw new IllegalStateException("LiveListenerBus already started.")
        }
    
        this.sparkContext = sc
        queues.asScala.foreach { q =>
          q.start(sc) // 關鍵,調用了隊列的start方法,啓動了隊列內的子線程,輪詢消息
          queuedEvents.foreach(q.post)
        }
        queuedEvents = null
        metricsSystem.registerSource(metrics)
      }
    
    }
    
  • LiveListenerBus實例化後,由start方法進行初始化,其內部包含多個隊列的列表,並且提供了註冊監聽隊列的方法、發送事件到隊列的方法。
  • AsyncEventQueue中由LinkedBlockingQueue封裝了消息事件,並啓動了一個子線程dispatchThread對消息隊列進行輪詢,取出消息並調用super.postToAll(next)將消息發出,最後到達了org.apache.spark.scheduler.SparkListenerBusdoPostEvent(...)方法,最終匹配消息併發送給了對應的監聽器。有興趣的朋友可以看看此部分代碼,其實就是個觀察者設計模式。

createSparkEnv的過程

  • 在SparkContext中調用createSparkEnv(_conf, isLocal, listenerBus)創建了SparkEnv,我們繼續往後追蹤。接着內部調用了SparkEnv.createDriverEnv(...)創建SparkEnv,然後其內部又調用了create(...)方法。
  • 這部分代碼就比較長了,我們來看其中比較關鍵的幾處代碼
      private def create(
          conf: SparkConf,
          executorId: String,
          bindAddress: String,
          advertiseAddress: String,
          port: Option[Int],
          isLocal: Boolean,
          numUsableCores: Int,
          ioEncryptionKey: Option[Array[Byte]],
          listenerBus: LiveListenerBus = null,
          mockOutputCommitCoordinator: Option[OutputCommitCoordinator] = None): SparkEnv = {
    
        // 省略部分代碼
    
    	// 創建RpcEnv
        val rpcEnv = RpcEnv.create(systemName, bindAddress, advertiseAddress, port.getOrElse(-1), conf,
          securityManager, numUsableCores, !isDriver)
    
    	// 省略部分代碼
    
        // 序列化管理器
        val serializerManager = new SerializerManager(serializer, conf, ioEncryptionKey)
    
        // 省略部分代碼
    
        // 廣播管理器
        val broadcastManager = new BroadcastManager(isDriver, conf, securityManager)
    
        val mapOutputTracker = if (isDriver) {
          new MapOutputTrackerMaster(conf, broadcastManager, isLocal)
        } else {
          new MapOutputTrackerWorker(conf)
        }
    
        // 省略部分代碼
    
        // 實例化ShuffleManager
        val shuffleManager = instantiateClass[ShuffleManager](shuffleMgrClass)
    
        // 創建內存管理器
        val useLegacyMemoryManager = conf.getBoolean("spark.memory.useLegacyMode", false)
        val memoryManager: MemoryManager =
          if (useLegacyMemoryManager) {
            new StaticMemoryManager(conf, numUsableCores)
          } else {
            UnifiedMemoryManager(conf, numUsableCores)
          }
    
        // 省略部分代碼
    
    	// BlockManagerMaster
        val blockManagerMaster = new BlockManagerMaster(registerOrLookupEndpoint(
          BlockManagerMaster.DRIVER_ENDPOINT_NAME,
          new BlockManagerMasterEndpoint(rpcEnv, isLocal, conf, listenerBus)),
          conf, isDriver)
    
        // BlockManager
        val blockManager = new BlockManager(executorId, rpcEnv, blockManagerMaster,
          serializerManager, conf, memoryManager, mapOutputTracker, shuffleManager,
          blockTransferService, securityManager, numUsableCores)
    
        // MetricsSystem
        val metricsSystem = if (isDriver) {
          MetricsSystem.createMetricsSystem("driver", conf, securityManager)
        } else {
          conf.set("spark.executor.id", executorId)
          val ms = MetricsSystem.createMetricsSystem("executor", conf, securityManager)
          ms.start()
          ms
        }
    
        // 省略部分代碼
    
    	// 實例化SparkEnv
        val envInstance = new SparkEnv(
          executorId,
          rpcEnv,
          serializer,
          closureSerializer,
          serializerManager,
          mapOutputTracker,
          shuffleManager,
          broadcastManager,
          blockManager,
          securityManager,
          metricsSystem,
          memoryManager,
          outputCommitCoordinator,
          conf)
    
        // 省略部分代碼
    
        envInstance
      }
    
  • 此部分代碼可謂羣英薈萃,Spark中各種重要的組件都在此處進行了創建,包括RpcEnv、SerializerManager、BroadcastManager、ShuffleManager、MemoryManager、BlockManagerMaster、BlockManager、MetricsSystem。
  • 由於我們主要關注SparkEnv,所以還是先看其實例化的代碼吧,不過其實它的構造器中啥都沒做,主要是將經常要用的對象封裝進來(例如前面的幾個管理器),方便使用(例如可以調用SparkEnv.rpcEnv獲取到RpcEnv)

創建不同的SchedulerBackend、TaskScheduler

  • 在SparkContext中調用SparkContext.createTaskScheduler(...)完成了對於SchedulerBackend、TaskScheduler的創建。不過對於不同的部署環境、部署模式,其SchedulerBackend、TaskScheduler是有各種不同的實現的。
  • 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" =>
            // local模式,創建TaskSchedulerImpl、LocalSchedulerBackend
    
          case LOCAL_N_REGEX(threads) =>
            // local[n]模式,創建TaskSchedulerImpl、LocalSchedulerBackend,只不過會先獲取一下指定的線程數
    
          case LOCAL_N_FAILURES_REGEX(threads, maxFailures) =>
            // 同local[n]模式,不過多了失敗最大重試次數
    
          case SPARK_REGEX(sparkUrl) =>
            // Standalone模式,一般傳入spark://+ip,創建TaskSchedulerImpl、StandaloneSchedulerBackend
    
          case LOCAL_CLUSTER_REGEX(numSlaves, coresPerSlave, memoryPerSlave) =>
            // 本地模擬Spark集羣的模式,創建TaskSchedulerImpl、StandaloneSchedulerBackend
    
          case masterUrl =>
            // 其他情況
            // 例如YARN、Mesos、Kubernetes
            // 獲取ClusterManager
    		val cm = getClusterManager(masterUrl) match {
              case Some(clusterMgr) => clusterMgr
              case None => throw new SparkException("Could not parse Master URL: '" + master + "'")
            }
            try {
              // 根據ClusterManager創建對應的TaskScheduler、SchedulerBackend
              val scheduler = cm.createTaskScheduler(sc, masterUrl)
              val backend = cm.createSchedulerBackend(sc, masterUrl, scheduler)
              cm.initialize(scheduler, backend)
              (backend, scheduler)
            } catch {
              // 省略部分代碼
            }
        }
      }
    
  • 我們可以看到該方法會按照master參數的不同,分別使用不同的方式創建SchedulerBackend、TaskScheduler。local和Standalone模式的代碼其實大家一看就懂了,不過最後一分部對於其他模式的創建就有一點麻煩了,因爲直接看不出來到底創建的是哪一個ExternalClusterManager(YarnClusterManager、MesosClusterManager、KubernetesClusterManager)。
  • 我們來看看獲取ClusterManager部分的代碼
      private def getClusterManager(url: String): Option[ExternalClusterManager] = {
        val loader = Utils.getContextOrSparkClassLoader
        // 調用ServiceLoader.load(...),並在最後對url進行了判斷
        val serviceLoaders =
          ServiceLoader.load(classOf[ExternalClusterManager], loader).asScala.filter(_.canCreate(url))
        if (serviceLoaders.size > 1) {
          throw new SparkException(
            s"Multiple external cluster managers registered for the url $url: $serviceLoaders")
        }
        serviceLoaders.headOption
      }
    
  • 其中最關鍵的是ServiceLoader.load(...)代碼,此處就是要實例化一個ExternalClusterManager。不過傳入的Class信息還是ExternalClusterManager,完全不知道最終實例化的是哪一個ClusterManager。
  • 其實這和ServiceLoader.load(...)的原理以及Spark部署包的編譯有關。
  • ServiceLoader.load(...)該方法是Java的方法,需要傳入一個接口,調用該方法後,會到jar包的META-INF中去尋找./services/接口全限定名(例如org.apache.spark.scheduler.ExternalClusterManager)文件的內容。而該文件內容對應的就是接口具體的實現類全限定名(例如org.apache.spark.scheduler.cluster.YarnClusterManager),然後就會利用反射實例化該對象。
  • 可以看到源碼中,YARN、Mesos、Kubernetes的services文件對應的目錄如下
    ExternalClusterManager
  • 不過到底最後使用哪一個ExternalClusterManager呢?這和Spark部署包的編譯有關,例如當你編譯時指定-Pyarn,那麼就會編譯含YARN版本的Spark,運行時再判斷傳入的url是yarn,那麼最終會使用YarnClusterManager。(其他依此類推即可)

DAGScheduler

  • DAGScheduler實例化時其實沒做太多事,主要是實例化了一個DAGSchedulerEventProcessLoop,並啓動。你看名字其實就能知道,這又是一個迴環,它會啓動子線程eventThread,並輪詢事件隊列eventQueue,調用onReceive處理消息。
  • 其他部分需要等待在後面提交Job時被調用,留在後面單獨寫一篇來講吧 ^_^
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章