文章目錄
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.SparkListenerBus
的doPostEvent(...)
方法,最終匹配消息併發送給了對應的監聽器。有興趣的朋友可以看看此部分代碼,其實就是個觀察者設計模式。
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呢?這和Spark部署包的編譯有關,例如當你編譯時指定-Pyarn,那麼就會編譯含YARN版本的Spark,運行時再判斷傳入的url是yarn,那麼最終會使用YarnClusterManager。(其他依此類推即可)
DAGScheduler
- DAGScheduler實例化時其實沒做太多事,主要是實例化了一個DAGSchedulerEventProcessLoop,並啓動。你看名字其實就能知道,這又是一個迴環,它會啓動子線程eventThread,並輪詢事件隊列eventQueue,調用onReceive處理消息。
- 其他部分需要等待在後面提交Job時被調用,留在後面單獨寫一篇來講吧 ^_^