文章目录
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行~528行)
// 进行一些应用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 }
- 动态启动Executor(第531行~545行)
// 是否启动动态申请,默认关闭 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是有各种不同的实现的。 - 另外,需要注意的是 SchedulerBackend 中完成了Spark应用的注册(方便后面使用,例如启动Executor)
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时被调用,留在后面单独写一篇来讲吧 ^_^