目錄
前言
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內還有其他一部分有用的屬性。并且在初始化的主流程做完之后,也有不少善后工作要做。下一篇文章就會分析它們。