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
        _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
          }
        // Bind the UI before starting the task scheduler to communicate
        // the bound port to the cluster manager properly
        _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)))
    
  • 再往后都是一些基本处理了,主要关注以下三行代码即可
        setupAndStartListenerBus()
        postEnvironmentUpdate()
        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时被调用,留在后面单独写一篇来讲吧 ^_^
发布了151 篇原创文章 · 获赞 70 · 访问量 19万+

猜你喜欢

转载自blog.csdn.net/alionsss/article/details/105015910