Spark学习-2.4.0-源码分析-1-Spark 核心篇-SparkContext


1、功能描述

  本篇文章就要根据源码分析SparkContext所做的一些事情,用过Spark的开发者都知道SparkContext是编写Spark程序用到的第一个类,足以说明SparkContext的重要性;这里先摘抄SparkContext源码注释来简单介绍介绍SparkContext,注释的第一句话就是说SparkContext为Spark的主要入口点,简明扼要,如把Spark集群当作服务端那Spark Driver就是客户端,SparkContext则是客户端的核心;如注释所说 SparkContext用于连接Spark集群、创建RDD、累加器(accumlator)、广播变量(broadcast variables),所以说SparkContext为Spark程序的根本都不为过
在这里插入图片描述

  SparkContext 是 Spark 中元老级的 API,从0.x.x 版本就已经存在。有过 Spark 使用经验会感觉 SparkContext 已经太老了,然后 SparkContext 始终跟随着 Spark 的迭代不断向前。SparkContext 内部虽然已经发生了很大的变化,有些内部组件已经废弃,有些组件已经优化,还有一些新的组件不断加入,不断焕发的强大的魅力,是 Spark 的灵魂。

1.1 源码描述

/**
 * Main entry point for Spark functionality. A SparkContext represents the connection to a Spark
 * cluster, and can be used to create RDDs, accumulators and broadcast variables on that cluster.
 *
 * Only one SparkContext may be active per JVM.  You must `stop()` the active SparkContext before
 * creating a new one.  This limitation may eventually be removed; see SPARK-2243 for more details.
 *
 * @param config a Spark Config object describing the application configuration. Any settings in
 *   this config overr可以ides the default configs as well as system properties.
 * 火花功能的主要入口点。sparkContext表示与spark集群的连接,可用于在该集群上创建RDD、累加器和广播变量。
 * 每个JVM只能有一个SparkContext处于活动状态。
 * 在创建新的SparkContext之前,必须对活动的SparkContext执行'stop()'。
 * 这一限制最终可能会被消除;更多细节见SPARK-2243。
 *
 *@param config描述应用程序配置的spark config对象。此配置中的任何设置都可以标识默认配置和系统属性。
  */

class SparkContext(config: SparkConf) extends Logging {
}

 也就是说SparkContext是Spark的入口,相当于应用程序的main函数。目前在一个JVM进程中可以创建多个SparkContext,但是只能有一个active级别的。如果你需要创建一个新的SparkContext实例,必须先调用stop方法停掉当前active级别的SparkContext实例。
图一 Spark架构图

图一 Spark架构图

图片来自Spark官网,可以看到SparkContext处于DriverProgram核心位置,所有与Cluster、Worker Node交互的操作都需要SparkContext来完成。

图2 SparkContext 在 Spark 应用程序中的扮演的主要角色

图2 SparkContext 在 Spark 应用程序中的扮演的主要角色

图3 Driver上运行的服务组件

图3 Driver 上运行的服务组件

2、相关组件

名称
说明
SparkConfSpark 配置类,配置已键值对形式存储,封装了一个ConcurrentHashMap类实例settings用于存储Spark的配置信息。  
SparkEnvSparkContext中非常重要的类,它维护着Spark的执行环境,所有的线程都可以通过SparkContext访问到同一个SparkEnv对象。  
LiveListenerBusSparkContext 中的事件总线,可以接收各种使用方的事件,并且异步传递Spark事件监听与SparkListeners监听器的注册。  
SparkUI为Spark监控Web平台提供了Spark环境、任务的整个生命周期的监控。  
TaskScheduler为Spark的任务调度器,Spark通过他提交任务并且请求集群调度任务。因其调度的 Task 由 DAGScheduler 创建,所以 DAGScheduler 是 TaskScheduler 的前置调度。  
DAGScheduler为高级的、基于Stage的调度器, 负责创建 Job,将 DAG 中的 RDD 划分到不同的 Stage,并将Stage作为Tasksets提交给底层调度器TaskScheduler执行。  
HeartbeatReceiver心跳接收器,所有 Executor 都会向HeartbeatReceiver 发送心跳,当其接收到 Executor 的心跳信息后,首先更新 Executor 的最后可见时间,然后将此信息交给 TaskScheduler 进一步处理。  
ExecutorAllocationManagerExecutor 动态分配管理器,根据负载动态的分配与删除Executor,可通过其设置动态分配最小Executor、最大Executor、初始Executor数量等配置。  
ContextClearner上下文清理器,为RDD、shuffle、broadcast状态的异步清理器,清理超出应用范围的RDD、ShuffleDependency、Broadcast对象。  
SparkStatusTracker低级别的状态报告API,只能提供非常脆弱的一致性机制,对Job(作业)、Stage(阶段)的状态进行监控。
HadoopConfigurationSpark默认使用HDFS来作为分布式文件系统,用于获取Hadoop配置信息。  
ProgressBar进度列  
SchedulerBackend调度程序后端  
EventLoggingListener事件记录器 

3、代码分析

思维导图版

序号
说明
补充
1构建调用站点并确保唯一的活跃SparkContext
2配置校验并设置Spark Driver 的 Host 和 Port
3初始化事件日志目录和压缩类型
4初始化App状态存储以及事件LiveListenerBus
5创建Spark的执行环境SparkEnv
6初始化状态跟踪器SparkStatusTracker
7根据配置创建ConsoleProgressBar
8创建并初始化Spark UI
9Hadoop相关配置及Executor环境变量的设置
10注册HeartbeatReceiver心跳接收器
11创建TaskScheduler
12创建DAGScheduler
13启动TaskScheduler
14初始化块管理器BlockManager
15启动测量系统MetricsSystem
16创建事件日志监听器
17创建和启动Executor分配管ExecutorAllocationManager
18创建和启动ContextCleaner
19额外的 SparkListenser 与启动事件总线(setupAndStartListenerBus)
20Spark环境更新(postEnvironmentUpdate)
21投递应用程序启动事件(postApplicationStart)
22创建DAGSchedulerSource、BlockManagerSource和ExecutorAllocationManagerSource
23将SparkContext添加进 终止处理程序ShutdownHookManager
24提示是否创建成功
25将SparkContext标记为激活

3.1初始设置

 首先保存了当前的CallSite信息,并且判断是否允许创建多个SparkContext实例,使用的是spark.driver.allowMultipleContexts属性,默认为false。

// 包名:org.apache.spark
// 类名:SparkContext
class SparkContext(config: SparkConf) extends Logging {
 
 // The call site where this SparkContext was constructed.
 // 获取当前SparkContext的当前调用栈。包含了最靠近栈顶的用户类及最靠近栈底的Scala或者Spark核心类信息
 private val creationSite: CallSite = Utils.getCallSite()
 
 // If true, log warnings instead of throwing exceptions when multiple SparkContexts are active
 // SparkContext默认只有一个实例。如果在config(SparkConf)中设置了allowMultipleContexts为true,
 // 当存在多个active级别的SparkContext实例时Spark会发生警告,而不是抛出异常,要特别注意。
 // 如果没有配置,则默认为false
 private val allowMultipleContexts: Boolean =
   config.getBoolean("spark.driver.allowMultipleContexts", false)
 
 // In order to prevent multiple SparkContexts from being active at the same time, mark this
 // context as having started construction.
 // NOTE: this must be placed at the beginning of the SparkContext constructor.
 // 用来确保SparkContext实例的唯一性,并将当前的SparkContext标记为正在构建中,以防止多个SparkContext实例同时成为active级别的。
 SparkContext.markPartiallyConstructed(this, allowMultipleContexts)

 创建了一系列的变量并部分初始化
创建的变量集合

图4 创建的变量集合

创建变量的源代码如下:

   //此值记录开始时间  
  val startTime = System.currentTimeMillis()
  //此值记录停止时间
  //AtomicBoolean 是一个@code布尔值,可以自动更新。{代码AtomicBoolean }用于诸如原子更新的标志的应用程序中
  private[spark] val stopped: AtomicBoolean = new AtomicBoolean(false)
    // log out Spark Version in Spark driver log
    //在Spark Driver日志中注销Spark版本
  logInfo(s"Running Spark version $SPARK_VERSION")
  /* ------------------------------------------------------------------------------------- *
   | Private variables. These variables keep the internal state of the context, and are    |
   | not accessible by the outside world. They're mutable since we want to initialize all  |
   | of them to some neutral(空的) value ahead of time, so that calling "stop()" while the       |
   | constructor is still running is safe.    																						 |
   |私人变量。这些变量维持着背景下的内部状态,而外部世界无法访问。																	 |
   |他们是可变的,因为我们希望他们所有人都从一开始就接受一些空值,														 |
   |因此,当构造器运行时调用stop()仍然是安全的。                                             |
   * ------------------------------------------------------------------------------------- */

  private var _conf: SparkConf = _
  private var _eventLogDir: Option[URI] = None
  private var _eventLogCodec: Option[String] = None
  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 _executorMemory: Int = _
  private var _schedulerBackend: SchedulerBackend = _
  private var _taskScheduler: TaskScheduler = _
  private var _heartbeatReceiver: RpcEndpointRef = _
  @volatile private var _dagScheduler: DAGScheduler = _
  private var _applicationId: String = _
  private var _applicationAttemptId: Option[String] = None
  private var _eventLogger: Option[EventLoggingListener] = None  
  private var _executorAllocationManager: Option[ExecutorAllocationManager] = None
  private var _cleaner: Option[ContextCleaner] = None
  private var _listenerBusStarted: Boolean = false
  private var _jars: Seq[String] = _
  private var _files: Seq[String] = _
  private var _shutdownHookRef: AnyRef = _
  private var _statusStore: AppStatusStore = _
  // Used to store a URL for each static file/jar together with the file's local timestamp
  //用于存储每个静态file/jar的URL以及文件的本地时间戳
  private[spark] val addedFiles = new ConcurrentHashMap[String, Long]().asScala
  private[spark] val addedJars = new ConcurrentHashMap[String, Long]().asScala

  // Keeps track of all persisted RDDs
  //跟踪所有持久的RDD
  private[spark] val persistentRdds = {
    val map: ConcurrentMap[Int, RDD[_]] = new MapMaker().weakValues().makeMap[Int, RDD[_]]()
    map.asScala
  }
  // Environment variables to pass dto our executors.
  //要传递给执行器的环境变量。
  private[spark] val executorEnvs = HashMap[String, String]()

  // Set SPARK_USER for user who is running SparkContext.
  //为运行SparkContext的用户设置Spark用户。这是当前登录的用户,除非它被“ddd35b ”环境变量覆盖。
  val sparkUser = Utils.getCurrentUserName()
  //检查点目录
  private[spark] var checkpointDir: Option[String] = None

  // Thread Local variable that can be used by users to pass information down the stack
  //线程本地变量,用户可以使用该变量将信息传递到堆栈中
  protected[spark] val localProperties = new InheritableThreadLocal[Properties] {
    override protected def childValue(parent: Properties): Properties = {
      // Note: make a clone such that changes in the parent properties aren't reflected in
      // the those of the children threads, which has confusing semantics (SPARK-10563).
      注意:克隆时,父属性的更改不会反映在子线程的更改中,这会导致语义混乱(spark-10563)。
      SerializationUtils.clone(parent)
    }
    override protected def initialValue(): Properties = new Properties()
  }

接下来是对SparkConf进行复制,然后对各种配置信息进行校验,其中最主要的就是SparkConf必须指定 spark.master(用于设置部署模式)spark.app.name(应用程序名称)属性,否则会抛出异常。

_conf = config.clone()  //获取一个克隆的config 命名为_conf
    _conf.validateSettings()  //检查_conf中的设置是否非法或已弃用

    if (!_conf.contains("spark.master")) { //检查是否设置了master URL   即conf.setMaster() 
      throw new SparkException("A master URL must be set in your configuration")
    }
    if (!_conf.contains("spark.app.name")) {//检查是否设置了application name 即conf.setAppName()
      throw new SparkException("An application name must be set in your configuration")
    }

    // log out spark.app.name in the Spark driver logs
    //在Spark Driver日志中记录注销spark.app.name   (这里log out是注销吗?为什么注销?还是记录日志的意思)
    logInfo(s"Submitted application: $appName")

    // System property spark.yarn.app.id must be set if user code ran by AM on a YARN cluster
    //如果用户代码由ApplicationMaster在yarn集群上运行,则必须设置系统属性spark.yarn.app.id。
    if (master == "yarn" && deployMode == "czxluster" && !_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.")
      //检测到yarn Cluster模式,但未在cluster上运行。SparkContext不直接支持对yarn的部署。请使用Spark Submit。“
    }

    //检测参数spark.logConf ,默认未设置返回false
    if (_conf.getBoolean("spark.logConf", false)) {
      logInfo("Spark configuration:\n" + _conf.toDebugString)
    }

    // Set Spark driver host and port system properties. This explicitly sets the configuration
    // instead of relying on the default value of the config constant.
    // 设置Spark Driver主机和端口系统属性。这将显式设置配置,而不是依赖配置常量的默认值。
    _conf.set(DRIVER_HOST_ADDRESS, _conf.get(DRIVER_HOST_ADDRESS))
    _conf.setIfMissing("spark.driver.port", "0") //如果端口未设置,则设置为0

    //设置执行器executorID
    //从设置可以看出,executor本质上也就是一个driver???,只不过在参数设置时,他的名称是spark.executor.id
    _conf.set("spark.executor.id", SparkContext.DRIVER_IDENTIFIER)

    //从_conf获取要发送到集群的JAR集合。这些路径可以是本地文件系统或HDFS、HTTP、HTTPS或FTP URL上的路径
    //jars和files分别是指??
    _jars = Utils.getUserJars(_conf)
    _files = _conf.getOption("spark.files").map(_.split(",")).map(_.filter(_.nonEmpty))
      .toSeq.flatten

    //事件日志目录
    _eventLogDir =
      if (isEventLogEnabled) {  //检查spark.eventLog.enabled是否设置
        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
      }
    }

3.2 创建执行环境SparkEnv

SparkEnv是Spark的执行环境对象,其中包括与众多Executor指向相关的对象。

  • local模式Driver会创建Executor
  • local-cluster部署模式或者Standalone部署模式Worker另起的CoarseGrainedExecutorBackend进程中也会创建Executor
  • 所以SparkEnv存在于Driver或者CoarseGrainedExecutorBackend进程中


创建SparkEnv主要使用

  • SparkEnv的createDriverEnv方法(有四个参数:conf、isLocal、listenerBus )
    以及在本地模式下driver运行executor需要的numberCores
    // 变量声明
   // 包名:org.apache.spark
   // 类名:SparkContext
   private var _env: SparkEnv = _
   
   // 在SparkEnv创建之前 还要先初始化App状态库和监听器 ,从而获得所有的事件
    //Spark事件的异步侦听器总线,新建一个
   _listenerBus = new LiveListenerBus(_conf)
   _statusStore = AppStatusStore.createLiveStore(conf)
   listenerBus.addToStatusQueue(_statusStore.listener.get)
   
   def isLocal: Boolean = Utils.isLocalMaster(_conf)

//创建Spark执行环境(例如 chche ,map输出追踪器 等等)
   _env = createSparkEnv(_conf, isLocal, listenerBus)
   SparkEnv.set(_env) // SparkEnv内部其实也是一个env,但是是private,外界无法访问,所以要通过setter来实现env初始化

 // This function allows components created by SparkEnv to be mocked in unit tests:
// 此函数用来创建SparkEnv 他也允许 在单元测试中 模拟 Sparkenv创建的组件
 private[spark] def createSparkEnv(
     conf: SparkConf,
     isLocal: Boolean,
     listenerBus: LiveListenerBus): SparkEnv = {
   SparkEnv.createDriverEnv(conf, isLocal, listenerBus, SparkContext.numDriverCores(master, conf))
 }/**
  * The number of cores available to the driver to use for tasks such as I/O with Netty
  * 驱动程序可用于任务(如具有 Netty 的 I/O)的内核数
  */
 private[spark] def numDriverCores(master: String, conf: SparkConf): Int = {
   def convertToInt(threads: String): Int = {
     if (threads == "*") Runtime.getRuntime.availableProcessors() else threads.toInt
   }
   master match {
     case "local" => 1
     case SparkMasterRegex.LOCAL_N_REGEX(threads) => convertToInt(threads)
     case SparkMasterRegex.LOCAL_N_FAILURES_REGEX(threads, _) => convertToInt(threads)
     case "yarn" =>
       if (conf != null && conf.getOption("spark.submit.deployMode").contains("cluster")) {
         conf.getInt("spark.driver.cores", 0)
       } else {
         0
       }
     case _ => 0 // Either driver is not being used, or its core count will be interpolated later
   }
 }

3.3 创建SparkUI

  • SparkUI 提供了用浏览器访问具有样式及布局并且提供丰富监控数据的页面。其采用的是时间监听机制
  • 发送的事件会存入缓存,由定时调度器取出后分配给监听此事件的监听器对监控数据进行更新。
  • 如果不需要SparkUI,则可以将spark.ui.enabled置为false。
	// 变量声明
	// 包名:org.apache.spark
	// 类名:SparkContext
	private var _ui: Option[SparkUI] = None

     //初始化SparkUI
    _ui =
      if (conf.getBoolean("spark.ui.enabled", true)) {
        Some(SparkUI.create(Some(this), _statusStore, _conf, _env.securityManager, appName, "",
          startTime))
      } else {
        // For tests, do not enable the UI
        //如果用于测试,不要启用UI
        None
      }
      
    // Bind the UI before starting the task scheduler to communicate
    // 在启动任务计划程序以进行通信之前绑定UI
    // the bound port to the cluster manager properly
    // 正确绑定到群集管理器的端口
    _ui.foreach(_.bind())

3.4 Hadoop 相关配置

 默认情况下,Spark使用HDFS作为分布式文件系统,所以需要获取Hadoop相关的配置信息:

	  private var _hadoopConfiguration: Configuration = _
	
	  //获取hadoop配置信息
 	  _hadoopConfiguration = SparkHadoopUtil.get.newConfiguration(_conf)

获取的配置信息包括:

  • 将Amazon S3文件系统的AWS_ACCESS_KEY_ID和 AWS_SECRET_ACCESS_KEY加载到Hadoop的Configuration;
  • 将SparkConf中所有的以spark.hadoop.开头的属性都赋值到Hadoop的Configuration;
  • 将SparkConf的属性spark.buffer.size复制到Hadoop的Configuration的配置io.file.buffer.size。
// 第一步
// 包名:org.apache.spark
// 类名:SparkContext
def get: SparkHadoopUtil = instance
  
// 第二步
// 包名:org.apache.spark
// 类名:SparkContext
/**
 * Return an appropriate (subclass) of Configuration. Creating config can initializes some Hadoop
 * subsystems.
 */
def newConfiguration(conf: SparkConf): Configuration = {
  val hadoopConf = SparkHadoopUtil.newConfiguration(conf)
  hadoopConf.addResource(SparkHadoopUtil.SPARK_HADOOP_CONF_FILE)
  hadoopConf
}
  
// 第三步
// 包名:org.apache.spark.deploy
// 类名:SparkHadoopUtil
/**
   * Returns a Configuration object with Spark configuration applied on top. Unlike
   * the instance method, this will always return a Configuration instance, and not a
   * cluster manager-specific type.
   * 返回顶部应用 Spark 配置的配置对象。
   * 与实例方法不同,这将始终返回配置实例,而不是特定于群集管理器的类型。
   */
  private[spark] def newConfiguration(conf: SparkConf): Configuration = {
    val hadoopConf = new Configuration()
    appendS3AndSparkHadoopConfigurations(conf, hadoopConf)
    hadoopConf
  }
  
// 第四步
// 包名:org.apache.spark.deploy
// 类名:SparkHadoopUtil
private def appendS3AndSparkHadoopConfigurations(
      conf: SparkConf,
      hadoopConf: Configuration): Unit = {
    // Note: this null check is around more than just access to the "conf" object to maintain
    // the behavior of the old implementation of this code, for backwards compatibility.
    //注意:此空检查不仅仅是访问"conf"对象,以维护此代码的旧实现的行为,以便向后兼容。
    if (conf != null) {
      // 显式检查 S3 环境变量
      val keyId = System.getenv("AWS_ACCESS_KEY_ID")
      val accessKey = System.getenv("AWS_SECRET_ACCESS_KEY")
      if (keyId != null && accessKey != null) {
        hadoopConf.set("fs.s3.awsAccessKeyId", keyId)
        hadoopConf.set("fs.s3n.awsAccessKeyId", keyId)
        hadoopConf.set("fs.s3a.access.key", keyId)
        hadoopConf.set("fs.s3.awsSecretAccessKey", accessKey)
        hadoopConf.set("fs.s3n.awsSecretAccessKey", accessKey)
        hadoopConf.set("fs.s3a.secret.key", accessKey)

        val sessionToken = System.getenv("AWS_SESSION_TOKEN")
        if (sessionToken != null) {
          hadoopConf.set("fs.s3a.session.token", sessionToken)
        }
      }
      appendSparkHadoopConfigs(conf, hadoopConf)
      val bufferSize = conf.get("spark.buffer.size", "65536")
      hadoopConf.set("io.file.buffer.size", bufferSize)
    }
  }
  
// 第五步
// 包名:org.apache.spark.deploy
// 类名:SparkHadoopUtil
private def appendSparkHadoopConfigs(conf: SparkConf, hadoopConf: Configuration): Unit = {
  // Copy any "spark.hadoop.foo=bar" spark properties into conf as "foo=bar"
  //将任何"spark.hadoop.foo_bar"的spark属性复制到"foo_bar"中。
  for ((key, value) <- conf.getAll if key.startsWith("spark.hadoop.")) {
    hadoopConf.set(key.substring("spark.hadoop.".length), value)
  }
}

3.5 Executor 环境变量

  • executorEnvs包含的环境变量将会在注册应用程序的过程中发送给Master,Master给Worker发送调度后,Worker最终使用executorEnvs提供的信息启动Executor
  • 通过配置spark.executor.memory指定Executor占用的内存的大小,也可以配置系统变量SPARK_EXECUTOR_MEMORY或者SPARK_MEM设置其大小。
	// 变量声明
	// 包名:org.apache.spark
	// 类名:SparkContext
	private var _executorMemory: Int = _
    _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)

executorEnvs是由一个HashMap存储:

	// 变量声明
	// 包名:org.apache.spark
	// 类名:SparkContext
  //要传递给执行器的环境变量。
  private[spark] val executorEnvs = HashMap[String, String]()

	// 变量处理
	// 包名:org.apache.spark
	// 类名:SparkContext
     // 将Java选项转换为env vars作为work around, 因为我们不能在sbt中直接设置env vars。
    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
    }
    
    //Mesos计划程序后端scheduler backend依赖此环境变量来设置执行器内存。
    //todo:只在Mesos调度器中设置这一点。
    executorEnvs("SPARK_EXECUTOR_MEMORY") = executorMemory + "m"
    executorEnvs ++= _conf.getExecutorEnv
    executorEnvs("SPARK_USER") = sparkUser
  

3.6 注册HeartbeatR`eceiver心跳接收器

  • 在 Spark 的实际生产环境中,Executor 是运行在不同的节点上的。
  • local 模式下Driver 与 Executor 属于同一个进程,所以 *Dirver 与 Executor 可以直接使用本地调用交互,当 Executor 运行出现问题时,Driver 可以很方便地知道 *,例如,通过捕获异常。
  • 但是在生产环境下Driver 与 Executor 很可能不在同一个进程内,他们也许运行在不同的机器上,甚至在不同的机房里,因此 Driver 对 Executor 失去掌握
  • <font size=>为了能够掌控 Executor,在 Driver 中创建了这个心跳接收器

创建 HeartbearReceiver 的代码:

  • 下面的代码中使用了 SparkEnv 的子组件 NettyRpcEnv 的 setupEndpoint 方法,此方法的作用是想向RpcEnv 的 Dispatcher 注册 HeartbeatReceiver,并返回 HeartbeatReceiver 的 NettyRpcEndpointRef 引用。
	// 变量声明
	// 包名:org.apache.spark
	// 类名:SparkContext
	private var _heartbeatReceiver: RpcEndpointRef = _

	// We need to register "HeartbeatReceiver" before "createTaskScheduler" because Executor will
    // retrieve "HeartbeatReceiver" in the constructor. (SPARK-6640)
    // 我们需要在“createTaskScheduler”之前注册“heartbeatReceiver”,
    // 因为执行器将在构造函数中检索“heartbeatReceiver”。(火花-6640)
    _heartbeatReceiver = env.rpcEnv.setupEndpoint(
      HeartbeatReceiver.ENDPOINT_NAME, new HeartbeatReceiver(this))

	// 变量处理
	// 包名:org.apache.spark.rpc.netty
	// 类名:NettyRpcEnv
	override def setupEndpoint(name: String, endpoint: RpcEndpoint): RpcEndpointRef = {
	  dispatcher.registerRpcEndpoint(name, endpoint)
	}

3.7 创建任务调度器TaskScheduler

TaskScheduler也是SparkContext的重要组成部分,负责任务的提交请求集群管理器对任务调度,并且负责发送任务到集群运行它们任务失败的重试,以及慢任务的在其他节点上重试
 其中给应用程序分配并运行 Executor为一级调度,而给任务分配 Executor 并运行任务则为二级调度另外 TaskScheduler 也可以看做任务调度的客户端 。

  • 为 TaskSet创建和维护一个TaskSetManager并追踪任务的本地性以及错误信息;
  • 遇到Straggle 任务会方到其他的节点进行重试;
  • 向DAGScheduler汇报执行情况, 包括在Shuffle输出lost的时候报告fetch failed 错误等信息;

 TaskScheduler负责任务调度资源分配,SchedulerBackend负责与Master、Worker通信收集Worker上分配给该应用使用的资源情况。
SparkContext 创建 Task Scheduler 和 Scheduler Backend

图四 SparkContext 创建 Task Scheduler 和 Scheduler Backend

创建 TaskScheduler 的代码:

	// 变量声明
	// 包名:org.apache.spark
	// 类名:SparkContext
	private var _schedulerBackend: SchedulerBackend = _
	private var _taskScheduler: TaskScheduler = _
	  
	// 变量处理
	// 包名:org.apache.spark
	// 类名:SparkContext
    // 创建并启动计划程序
      //this是指当前的SparkContext
    val (sched, ts) = SparkContext.createTaskScheduler(this, master, deployMode)
    _schedulerBackend = sched  //获取schedulerBackend
    _taskScheduler = ts //获取taskScheduler

createTaskScheduler方法根据master的配置匹配部署模式,创建TaskSchedulerImpl,并生成不同的SchedulerBackend,因此此时通过ts获得的_taskScheduler实际上是父类引用指向子类对象TaskSchedulerImpl。具体如下:

  • 首先,在这个createTaskScheduler()方法中,返回值是(SchedulerBackend, TaskScheduler)
  • 其次,它创建scheduler是创建的子类TaskSchedulerImpl
    val scheduler = new TaskSchedulerImpl(sc, MAX_LOCAL_TASK_FAILURES, isLocal = true)
  • 最后返回的是(backend, scheduler)
  • 总而言之,就是createTaskScheduler()创建了一个TaskSchedulerImpl,却将它作为一个TaskScheduler返回
	// 包名:org.apache.spark
	// 类名:SparkContext
	  private def createTaskScheduler(
      sc: SparkContext,
      master: String,
      deployMode: String): (SchedulerBackend, TaskScheduler) = {
    import SparkMasterRegex._  
    //里面封装了很多用来识别 master:String 的正则表达式,确定任务运行模式
    //下面的介个case 都是这里面的方法,以便匹配

    // When running locally, don't try to re-execute tasks on failure.
    val MAX_LOCAL_TASK_FAILURES = 1
    
	//在创建conf的时候要设置的的.setMaster("local")   然后才是new SparkContext(conf)
    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)

        //常用的创建方式是SPARK_REGEX,也就是spark的 standalone 模式
      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)
        }
    }
  }

下面看看scheduler.initialize()做了什么:

  • 确定schedulePool中的调度方式,FIFO或者FAIR
  • 创建schedulePool
  def initialize(backend: SchedulerBackend) {
    this.backend = backend
    schedulableBuilder = {
      schedulingMode match {
        case SchedulingMode.FIFO =>
          new FIFOSchedulableBuilder(rootPool)
        case SchedulingMode.FAIR =>
          new FairSchedulableBuilder(rootPool, conf)
        case _ =>
          throw new IllegalArgumentException(s"Unsupported $SCHEDULER_MODE_PROPERTY: " +
          s"$schedulingMode")
      }
    }
    schedulableBuilder.buildPools()
  }

3.8 创建和启动DAGScheduler

 DAGScheduler主要用于在任务正式交给TaskScheduler提交之前做一些准备工作,包括:

  • 创建Job
  • 将DAG中的RDD划分到不同的Stage
  • 提交Stage 等等

DAGScheduler的数据结构主要维护jobId和stageId的关系、Stage、ActiveJob,以及缓存的RDD的Partition的位置信息在这里插入图片描述

图5 任务执行流程

创建 DAGScheduler 的代码:

	// 变量声明
	// 包名:org.apache.spark
	// 类名:SparkContext
	@volatile private var _dagScheduler: DAGScheduler = _

	//创建DAGScheduler ,并在其构造函数中设置DAGScheduler引用
	_dagScheduler = new DAGScheduler(this) 
	
	//接着心跳接收器询问taskScheduler是否已经设置了
	_heartbeatReceiver.ask[Boolean](TaskSchedulerIsSet) 

3.9 TaskScheduler启动

 TaskScheduler在启动的时候实际是调用了backend的start方法:

   //在 taskScheduler在DAGScheduler的构造函数中设置DAGScheduler引用之后 启动taskScheduler
   // 任务调度器TaskScheduler的创建,想要TaskScheduler发挥作用,必须启动它。
  /**
     * 启动taskScheduler,调用的是TaskSchedulerImpl的start()方法
     * _taskScheduler实际上是一个TaskSchedulerImpl
  * */
   _taskScheduler.start()

 从下面的代码可以看出来

  • TaskSchedulerImpl是继承自TaskScheduler
  • TaskSchedulerImpl重写了start()方法
// TaskSchedulerImpl.scala中
	private[spark] class TaskSchedulerImpl(...)extends TaskScheduler with Logging {
		...
		//从这里可以看出,
		override def start() {
	    backend.start()
	
	    if (!isLocal && conf.getBoolean("spark.speculation", false)) {
	      logInfo("Starting speculative execution thread")
	      speculationScheduler.scheduleWithFixedDelay(new Runnable {
	        override def run(): Unit = Utils.tryOrStopSparkContext(sc) {
	          checkSpeculatableTasks()
	        }
	      }, SPECULATION_INTERVAL_MS, SPECULATION_INTERVAL_MS, TimeUnit.MILLISECONDS)
	    }
	  }
	}
  

如何解释TaskScheduler在启动的时候实际是调用了backend的start方法

  • 首先,_taskScheduler是指向一个子类对象TaskSchedulerImpl(原因见TaskScheduler的创建)
  • 因此,_taskScheduler.start()调用的是TaskSchedulerImplstart()方法:
  • 最后,TaskSchedulerImplstart()方法中又调用了backend.start()

3.10 启动测量系统MetricsSystem

MetricsSystem中三个概念:

  • Instance: 指定了谁在使用测量系统;
    Spark按照Instance的不同,区分为Master、Worker、Application、Driver和Executor;
  • Source: 指定了从哪里收集测量数据;
    Source有两种来源:Spark internal source: MasterSource/WorkerSource等; Common source: JvmSource
  • Sink:指定了往哪里输出测量数据;
    Spark目前提供的Sink有ConsoleSink、CsvSink、JmxSink、MetricsServlet、GraphiteSink等;Spark使用MetricsServlet作为默认的Sink。

MetricsSystem的启动过程包括:

  1. 注册Sources;
  2. 注册Sinks;
  3. 将Sinks增加Jetty的ServletContextHandler;

启动准备:

  • 驱动程序的度量系统需要设置spark.app.idapp id
  • 所以应该在从任务调度程序获取app id设置spark.app.id之后开始。
	//Spark应用程序的ID,先生成一个applicationId,然后在_conf中设置这个参数
	 //生成方式: "spark-application-" + System.currentTimeMillis
	 _applicationId = _taskScheduler.applicationId()
	 _applicationAttemptId = taskScheduler.applicationAttemptId()
	 _conf.set("spark.app.id", _applicationId)
	 
	 // sparkUI相关:这里设置了spark.ui.proxyBase,就是yarn模式下uiRoot参数
	 if (_conf.getBoolean("spark.ui.reverseProxy", false)) {
	   System.setProperty("spark.ui.proxyBase", "/proxy/" + _applicationId)
	 }
	 _ui.foreach(_.setAppId(_applicationId))
	 
	 //初始化BlockManager
	 _env.blockManager.initialize(_applicationId)

然后启动。MetricsSystem启动完毕后,会遍历与Sinks有关的ServletContextHandler,并调用attachHandler将它们绑定到Spark UI上。

    _env.metricsSystem.start()
    // Attach the driver metrics servlet handler to the web ui after the metrics system is started.
    //在度量系统启动后,将驱动程序度量servlet处理程序附加到Web UI。
    _env.metricsSystem.getServletHandlers.foreach(handler => ui.foreach(_.attachHandler(handler)))

3.11 创建时间日志监听器

EventLoggingListener将事件持久化到存储的监听器,是 SparkContext 中可选组件。

  • spark.eventLog.enabled属性为 true 时启动,默认为false
  • 它会被添加到listenerBus

创建准备:

	// 变量声明
	// 包名:org.apache.spark
	// 类名:SparkContext
	private var _eventLogDir: Option[URI] = None
	private var _eventLogCodec: Option[String] = None
	private var _eventLogger: Option[EventLoggingListener] = None
	  
	private[spark] def isEventLogEnabled: Boolean = _conf.getBoolean("spark.eventLog.enabled", false)
	private[spark] def eventLogDir: Option[URI] = _eventLogDir
	private[spark] def eventLogCodec: Option[String] = _eventLogCodec
  
	// 变量处理
	// 包名:org.apache.spark
	// 类名:SparkContext
	
	//事件日志目录
    _eventLogDir =
      if (isEventLogEnabled) {  //检查spark.eventLog.enabled是否设置
        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
      }
    }

创建 EventLoggingListener 的代码:

    /**
      * 启动eventLog,因为isEventLogEnabled默认为false,所以这个默认是不启动的
      */
    _eventLogger =
      if (isEventLogEnabled) {
        val logger =
          // 创建一个EventLoggingListener对象
          new EventLoggingListener(_applicationId, _applicationAttemptId, _eventLogDir.get,
            _conf, _hadoopConfiguration)
        // 调用Start方法
        logger.start()
        // 加入到监听Bus
        listenerBus.addToEventLogQueue(logger)
        Some(logger)
      } else {
        None
      }

EventLoggingListenser 也将参与到事件总线中事件的监听中,并把感兴趣的事件记录到日志中。
EventLoggingListenser 最为核心的方法是 logEvent,其代码如下:

  /** Log the event as JSON. */
  private def logEvent(event: SparkListenerEvent, flushLogger: Boolean = false) {
    val eventJson = JsonProtocol.sparkEventToJson(event)
    // scalastyle:off println
    writer.foreach(_.println(compact(render(eventJson))))
    // scalastyle:on println
    if (flushLogger) {
      writer.foreach(_.flush())
      hadoopDataStream.foreach(_.hflush())
    }
    if (testing) {
      loggedEvents += eventJson
    }
  }

3.12 创建和启动 ExecutorAllocationManager

ExecutorAllocationManager用于对以分配的Executor进行管理。

  • 默认情况下不会创建ExecutorAllocationManager,可以修改属性spark.dynamicAllocation.enabledtrue来创建。
  • ExecutorAllocationManager可以:
     动态的分配最小Executor的数量、动态分配最大Executor的数量、每个Executor可以运行的Task数量等配置信息,并对配置信息进行校验`。
  • start方法将ExecutorAllocationListener加入listenerBus中,ExecutorAllocationListener通过监听listenerBus里的事件,动态的添加、删除Executor。并且通过不断添加Executor,遍历Executor,将超时的Executor杀死并移除

创建和启动ExecutorAllocationManager代码:

    /**
      * dynamicAllocationEnabled用于对已经分配的Executor进行管理,创建和启动ExecutorAllocationManager。
      * dynamic动态的   Allocation分配
      * 返回在给定的conf中是否启用了动态分配。默认是没有启动的 false
      * 设置spark.dynamicAllocation.enabled为true 可以启动动态分配
      */
    val dynamicAllocationEnabled = Utils.isDynamicAllocationEnabled(_conf)
    
    _executorAllocationManager =
      // 如果是local模式下,这个内容为_schedulerBackend: LocalSchedulerBackend 无法进行动态分配
      if (dynamicAllocationEnabled) {
        schedulerBackend match { 
          case b: ExecutorAllocationClient =>
            /** 新建一个ExecutorAllocationManager 动态分配executor*/
            Some(new ExecutorAllocationManager(
              schedulerBackend.asInstanceOf[ExecutorAllocationClient], listenerBus, _conf,
              _env.blockManager.master))
          case _ =>
            None
        }
      } else {
        None
      }
    // 调用start方法启动
    _executorAllocationManager.foreach(_.start())

3.13 ContextCleaner的创建与启动

ContextCleaner用于清理超出应用范围的RDD、ShuffleDependency和Broadcast对象。
ContextCleaner的组成:

  • referenceQueue: 缓存顶级的AnyRef引用;
  • referenceBuff:缓存AnyRef的虚引用;
  • listeners:缓存清理工作的监听器数组;
  • cleaningThread:用于具体清理工作的线程。

创建并启动代码:

    /**  启用Spark的ContextCleaner 用于清理功能 */
    _cleaner =
      if (_conf.getBoolean("spark.cleaner.referenceTracking", true)) {
        Some(new ContextCleaner(this))
      } else {
        None
      }
    // 调用start方法
    _cleaner.foreach(_.start())

3.14 额外的SparkKistener与启动事件

SparkContext 中提供了添加用于自定义 SparkListener 的地方。

根据代码描述,setupAndStartListenerBus 的执行步骤如下:

  1. spark.extraListeners 属性中获取用户自定义的 SparkListener的类名。用户可以通过逗号分割多个自定义 SparkListener
  2. 通过发射生成每一个自定义SparkListener的实例,并添加到事件总线的监听器列表中。
  3. 启动事件总线,并将_listenerBusStarted设置为 true

代码如下:

   /*** 这个方法主要执行每个监听器的静态代码块,启动监听总线listenBus*/
   setupAndStartListenerBus()
	
	  /**
   * Registers listeners specified in spark.extraListeners, then starts the listener bus.
   * This should be called after all internal listeners have been registered with the listener bus
   * (e.g. after the web UI and event logging listeners have been registered).
   */
  private def setupAndStartListenerBus(): Unit = {
    try {
      // 获取用户自定义的 SparkListenser 的类名
      conf.get(EXTRA_LISTENERS).foreach { classNames =>
        // 通过发射生成每一个自定义 SparkListenser 的实例,并添加到事件总线的监听列表中
        val listeners = Utils.loadExtensions(classOf[SparkListenerInterface], classNames, conf)
        listeners.foreach { listener =>
          listenerBus.addToSharedQueue(listener)
          logInfo(s"Registered listener ${listener.getClass().getName()}")
        }
      }
    } catch {
      case e: Exception =>
        try {
          stop()
        } finally {
          throw new SparkException(s"Exception when registering SparkListener", e)
        }
    }
    
	// 启动事件总线,并将_listenerBusStarted设置为 true
    listenerBus.start(this, _env.metricsSystem)
    _listenerBusStarted = true
  }

	// 包名:org.apache.spark.internal
	// 类名:package object config
	private[spark] val EXTRA_LISTENERS = ConfigBuilder("spark.extraListeners")
	  .doc("Class names of listeners to add to SparkContext during initialization.")
	  .stringConf
	  .toSequence
	  .createOptional

3.15 Spark环境更新

SparkContext的初始化过程中,可能对其环境造成影响,所以需要更新环境:

    /*** 在SparkContext的初始化过程中,可能对其环境造成影响,所以需要更新环境。就是提交代码后,如果更新了环境*/
    postEnvironmentUpdate()

SparkContext初始化过程中,如果设置了spark.jars属性,spark.jars指定的jar包将由addJar方法加入httpFileServerjarDir变量指定的路径下。每加入一个jar都会调用postEnvironmentUpdate方法更新环境。增加文件与增加jar相同,也会调用postEnvironmentUpdate方法。

	// 变量声明
	// 包名:org.apache.spark
	// 类名:SparkContext
	private var _jars: Seq[String] = _
	private var _files: Seq[String] = _
	
	// 变量处理
	// 第一步 从_conf获取要发送到集群的JAR集合。这些路径可以是本地文件系统或HDFS、HTTP、HTTPS或FTP URL上的路径
	// 包名:org.apache.spark
	// 类名:SparkContext
    	//jars和files分别是指??
    _jars = Utils.getUserJars(_conf)
    _files = _conf.getOption("spark.files").map(_.split(",")).map(_.filter(_.nonEmpty))
      .toSeq.flatten

	/**
	 * Return the jar files pointed by the "spark.jars" property. Spark internally will distribute
	 * these jars through file server. In the YARN mode, it will return an empty list, since YARN
	 * has its own mechanism to distribute jars.
	 */
	def getUserJars(conf: SparkConf): Seq[String] = {
	  val sparkJars = conf.getOption("spark.jars")
	  sparkJars.map(_.split(",")).map(_.filter(_.nonEmpty)).toSeq.flatten
	}


	// 第二步 添加通过构造函数提供的每个jar和file
	// 包名:org.apache.spark
	// 类名:SparkContext
	// Add each JAR given through the constructor
	if (jars != null) {
	  jars.foreach(addJar)
	}
	  
	if (files != null) {
	  files.foreach(addFile)
	}
	
  /**
   • 为将来在此"SparkContext"上执行的所有任务添加 JAR 依赖项。
   *
   • 如果在执行期间添加了 jar,则在下一个任务集启动之前,该 jar 将不可用。
   *
   • @param路径可以是本地文件、HDFS 中的文件(或其他 Hadoop 支持的文件系统),
   • 每个工作节点上文件的 HTTP、HTTPS 或 FTP URI 或本地:/路径。
   *
   • @note路径只能添加一次。将忽略同一路径的后续添加。
   */
  def addJar(path: String) {
    def addJarFile(file: File): String = {
      try {
        if (!file.exists()) {
          throw new FileNotFoundException(s"Jar ${file.getAbsolutePath} not found")
        }
        if (file.isDirectory) {
          throw new IllegalArgumentException(
            s"Directory ${file.getAbsoluteFile} is not allowed for addJar")
        }
        env.rpcEnv.fileServer.addJar(file)
      } catch {
        case NonFatal(e) =>
          logError(s"Failed to add $path to Spark environment", e)
          null
      }
    }

    if (path == null) {
      logWarning("null specified as parameter to addJar")
    } else {
      val key = if (path.contains("\\")) {
        // For local paths with backslashes on Windows, URI throws an exception
        addJarFile(new File(path))
      } else {
        val uri = new URI(path)
        // SPARK-17650: Make sure this is a valid URL before adding it to the list of dependencies
        Utils.validateURL(uri)
        uri.getScheme match {
          // A JAR file which exists only on the driver node
          case null =>
            // SPARK-22585 path without schema is not url encoded
            addJarFile(new File(uri.getRawPath))
          // A JAR file which exists only on the driver node
          case "file" => addJarFile(new File(uri.getPath))
          // A JAR file which exists locally on every worker node
          case "local" => "file:" + uri.getPath
          case _ => path
        }
      }
      if (key != null) {
        val timestamp = System.currentTimeMillis
        if (addedJars.putIfAbsent(key, timestamp).isEmpty) {
          logInfo(s"Added JAR $path at $key with timestamp $timestamp")
          postEnvironmentUpdate()
        } else {
          logWarning(s"The jar $path has been added already. Overwriting of added jars " +
            "is not supported in the current version.")
        }
      }
    }
  }

  /**
   • 在每个节点上添加使用此 Spark 作业下载的文件。
   *
   • 如果在执行期间添加了文件,则在下一个任务集启动之前,该文件将不可用。
   *
   • @param路径可以是本地文件、HDFS 中的文件(或其他支持 Hadoop 的文件)
   • 文件系统),或 HTTP、HTTPS 或 FTP URI。要访问 Spark 作业中的文件,
   • 使用"SparkFiles.get(文件名)"查找其下载位置。
   • @param递归(如果为 true),则可以在"路径"中给出目录。当前目录是
   * 仅支持 Hadoop 支持的文件系统。
   *
   • @note路径只能添加一次。将忽略同一路径的后续添加。
   */
  def addFile(path: String): Unit = {
    addFile(path, false)
  }
  def addFile(path: String, recursive: Boolean): Unit = {
    val uri = new Path(path).toUri
    val schemeCorrectedPath = uri.getScheme match {
      case null => new File(path).getCanonicalFile.toURI.toString
      case "local" =>
        logWarning("File with 'local' scheme is not supported to add to file server, since " +
          "it is already available on every node.")
        return
      case _ => path
    }

    val hadoopPath = new Path(schemeCorrectedPath)
    val scheme = new URI(schemeCorrectedPath).getScheme
    if (!Array("http", "https", "ftp").contains(scheme)) {
      val fs = hadoopPath.getFileSystem(hadoopConfiguration)
      val isDir = fs.getFileStatus(hadoopPath).isDirectory
      if (!isLocal && scheme == "file" && isDir) {
        throw new SparkException(s"addFile does not support local directories when not running " +
          "local mode.")
      }
      if (!recursive && isDir) {
        throw new SparkException(s"Added file $hadoopPath is a directory and recursive is not " +
          "turned on.")
      }
    } else {
      // SPARK-17650: Make sure this is a valid URL before adding it to the list of dependencies
      Utils.validateURL(uri)
    }

    val key = if (!isLocal && scheme == "file") {
      env.rpcEnv.fileServer.addFile(new File(uri.getPath))
    } else {
      schemeCorrectedPath
    }
    val timestamp = System.currentTimeMillis
    if (addedFiles.putIfAbsent(key, timestamp).isEmpty) {
      logInfo(s"Added file $path at $key with timestamp $timestamp")
      // Fetch the file locally so that closures which are run on the driver can still use the
      // SparkFiles API to access files.
      Utils.fetchFile(uri.toString, new File(SparkFiles.getRootDirectory()), conf,
        env.securityManager, hadoopConfiguration, timestamp, useCache = false)
      postEnvironmentUpdate()
    } else {
      logWarning(s"The path $path has been added already. Overwriting of added paths " +
       "is not supported in the current version.")
    }
  }

根据代码描述,postEnvironmentUpdate方法处理步骤:

  1. 通过调用SparkEnv 的方法 environmentDetails,将环境的 JVM 参数Spark 属性系统属性classPath 等信息设置为环境明细信息
  2. 生成事件SparkListenerEnvironmentUpdate(此事件携带环境明细信息),并投递到事件总线 listenerBus,此事件最终被 EnvironmentListener 监听,并影响 EnvironmentPage 页面中的输出内容。
// 第一步
// 包名:org.apache.spark
// 类名:SparkContext
  /** Post the environment update event once the task scheduler is ready */
  private def postEnvironmentUpdate() {
    if (taskScheduler != null) {
      val schedulingMode = getSchedulingMode.toString
      val addedJarPaths = addedJars.keys.toSeq
      val addedFilePaths = addedFiles.keys.toSeq
      val environmentDetails = SparkEnv.environmentDetails(conf, schedulingMode, addedJarPaths,
        addedFilePaths)
      val environmentUpdate = SparkListenerEnvironmentUpdate(environmentDetails)
      listenerBus.post(environmentUpdate)
    }
  }

// 第二步
// 包名:org.apache.spark
// 类名:SparkEnv
  /**
   • 返回 jvm 信息、Spark 属性、系统属性和
   • 类路径。地图键定义类别,映射值表示相应的
   * 属性作为 KV 对序列。这主要用于 SparkListener 环境更新。
   */
  private[spark]
  def environmentDetails(
      conf: SparkConf,
      schedulingMode: String,
      addedJars: Seq[String],
      addedFiles: Seq[String]): Map[String, Seq[(String, String)]] = {

    import Properties._
    val jvmInformation = Seq(
      ("Java Version", s"$javaVersion ($javaVendor)"),
      ("Java Home", javaHome),
      ("Scala Version", versionString)
    ).sorted

    // Spark properties
    // This includes the scheduling mode whether or not it is configured (used by SparkUI)
    val schedulerMode =
      if (!conf.contains("spark.scheduler.mode")) {
        Seq(("spark.scheduler.mode", schedulingMode))
      } else {
        Seq.empty[(String, String)]
      }
    val sparkProperties = (conf.getAll ++ schedulerMode).sorted

    // System properties that are not java classpaths
    val systemProperties = Utils.getSystemProperties.toSeq
    val otherProperties = systemProperties.filter { case (k, _) =>
      k != "java.class.path" && !k.startsWith("spark.")
    }.sorted

    // Class paths including all added jars and files
    val classPathEntries = javaClassPath
      .split(File.pathSeparator)
      .filterNot(_.isEmpty)
      .map((_, "System Classpath"))
    val addedJarsAndFiles = (addedJars ++ addedFiles).map((_, "Added By User"))
    val classPaths = (addedJarsAndFiles ++ classPathEntries).sorted

    Map[String, Seq[(String, String)]](
      "JVM Information" -> jvmInformation,
      "Spark Properties" -> sparkProperties,
      "System Properties" -> otherProperties,
      "Classpath Entries" -> classPaths)
  }

3.16 投递应用程序启动事件

postApplicationStart方法只是向listenerBus发送了SparkListenerApplicationStart事件:

// 第一步
// 包名:org.apache.spark
// 类名:SparkContext
postApplicationStart()

// 第二步
// 包名:org.apache.spark
// 类名:SparkContext
  /** Post the application start event */
  private def postApplicationStart() {
    //注意:此代码假定任务计划程序已初始化并已联系
    //群集管理器获取应用程序 ID(在群集管理器提供时)。
    listenerBus.post(SparkListenerApplicationStart(appName, Some(applicationId),
      startTime, sparkUser, applicationAttemptId, schedulerBackend.getDriverLogUrls))
  }

3.17 创建DAGSchedulerSourceBlockManagerSource、和ExecutorAllocationManagerSource

首先要调用taskSchedulerpostStartHook方法,其目的是为了等待backend就绪。

// 包名:org.apache.spark
// 类名:SparkContext
// Post init

    // Post init 等待SchedulerBackend准备好
    _taskScheduler.postStartHook()
    // DAGSchedulerSource的测量信息是job和Satge相关的信息
    _env.metricsSystem.registerSource(_dagScheduler.metricsSource)
    // 注册BlockManagerSource
    _env.metricsSystem.registerSource(new BlockManagerSource(_env.blockManager))
    // 动态分配的executor信息
    _executorAllocationManager.foreach { e =>
      _env.metricsSystem.registerSource(e.executorAllocationManagerSource)
    }

3.18 将SparkContext添加进 终止处理程序ShutdownHookManager

ShutdownHookManager的创建,为了在Spark程序挂掉的时候,处理一些清理工作
创建流程:

  1. 先强制创建记录器
  2. 调用addShutdownHook()方法,传入参数SPARK_CONTEXT_SHUTDOWN_PRIORITY
     addShutdownHook实现如下:

他是先调用shutdownHooks获得一个已注册的manager

  • 先创建一个 manager = new SparkShutdownHookManager()
  • 调用 manager.install() 安装要在关机时运行的挂钩,并按顺序运行所有已注册的挂钩
  • 返回 manager

然后调用 shutdownHooks.add(priority, hook) ,也就成了 manager.add(priority, hook)
add 方法主要代码如下:

  • val hookRef = new SparkShutdownHook(priority, hook)
  • hooks.add(hookRef)  //将指定的元素插入到此优先级队列中。
  • 返回 hookRef (也就成了addShutdownHook()的返回值)

3.返回的hookRef,赋值给_shutdownHookRef

   //如果用户忘记了context,请确保该context已停止。
    //这样可以避免在JVM干净退出后留下未完成的事件日志。不过,如果JVM被杀了也没用。
    logDebug("Adding shutdown hook") // 强制创建记录器
    
    /** ShutdownHookManager的创建,为了在Spark程序挂掉的时候,处理一些清理工作  
	    *hook  钩子
	    */
    _shutdownHookRef = ShutdownHookManager.addShutdownHook(
      ShutdownHookManager.SPARK_CONTEXT_SHUTDOWN_PRIORITY) { () =>
      logInfo("Invoking(调用) stop() from shutdown hook")
      try {
        // 这调用停止方法。关闭SparkContext,我就搞不懂了
        stop()
      } catch {
        case e: Throwable =>
          logWarning("Ignoring Exception while stopping SparkContext from shutdown hook", e)
      }
    }
    
    // 包名:org.apache.spark.util
	// 类名:ShutdownHookManager
	  /**
	   * SparkContext 实例的关闭优先级。这低于默认值(100)
	   * 优先级,以便默认情况下在关闭上下文之前运行挂钩。
	   */
	  val SPARK_CONTEXT_SHUTDOWN_PRIORITY = 50

	 /**
	   • 添加具有给定优先级的关机钩。先运行具有较高优先级值的挂钩
	   * 
	   • @param挂接要在关机期间运行的代码。
	   • @return可用于取消登记关机钩的句柄。
	   */
	  def addShutdownHook(priority: Int)(hook: () => Unit): AnyRef = {
	    shutdownHooks.add(priority, hook)
	  }
	  
	 //上一个函数中用到的shutdownHooks
	 private lazy val shutdownHooks = {
	    val manager = new SparkShutdownHookManager()
	    manager.install()
	    manager
	  }
	  
		  /**
		   * shutdownHooks用到的install
		   * 安装要在关机时运行的挂钩,并按顺序运行所有已注册的挂钩.
		   */
		  def install(): Unit = {
		    val hookTask = new Runnable() {
		      override def run(): Unit = runAll()
		    }
		    org.apache.hadoop.util.ShutdownHookManager.get().addShutdownHook(
		      hookTask, FileSystem.SHUTDOWN_HOOK_PRIORITY + 30)
		  }
	  //addShutdownHook用到的add方法
	  def add(priority: Int, hook: () => Unit): AnyRef = {
	    hooks.synchronized {
	      if (shuttingDown) {
	        throw new IllegalStateException("Shutdown hooks cannot be modified during shutdown.")
	      }
	      val hookRef = new SparkShutdownHook(priority, hook)
	      hooks.add(hookRef)//将指定的元素插入到此优先级队列中。
	      hookRef
	    }
	  }

3.19 提示是否创建成功

    //提示你SparkContext是否创建成功
    //默认 没有消息就是最好的消息
    case NonFatal(e) =>
      logError("Error initializing SparkContext.", e)
      try {
        stop()
      } catch {
        case NonFatal(inner) =>
          logError("Error stopping SparkContext after init error.", inner)
      } finally {
        throw e
      }

3.19 将SparkContext标记为激活

如果创建成功,上一步没有抛异常,那么SparkContext初始化的最后会将当前SparkContext的状态从contextBeingConstructed(正在构建中)改为activeContext(已激活)

  // 包名:org.apache.spark
  // 类名:SparkContext
  //目的是为了防止多个SparkContext同时处于活动状态,因此请将此上下文标记为已完成构造。
  //注意:这必须放在SparkContext构造函数的末尾。
  SparkContext.setActiveContext(this, allowMultipleContexts)
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Spark submit任务提交是指将用户编写的Spark应用程序提交到集群中运行的过程。在Spark中,用户可以通过命令行工具或API方式提交任务。 Spark submit命令的基本语法如下: ``` ./bin/spark-submit \ --class <main-class> \ --master <master-url> \ --deploy-mode <deploy-mode> \ --conf <key>=<value> \ <application-jar> \ [application-arguments] ``` 其中,`--class`指定应用程序的主类,`--master`指定集群的URL,`--deploy-mode`指定应用程序的部署模式,`--conf`指定应用程序的配置参数,`<application-jar>`指定应用程序的jar包路径,`[application-arguments]`指定应用程序的命令行参数。 在Spark中,任务提交的过程主要包括以下几个步骤: 1. 创建SparkConf对象,设置应用程序的配置参数; 2. 创建SparkContext对象,连接到集群; 3. 加载应用程序的主类; 4. 运行应用程序的main方法; 5. 关闭SparkContext对象,释放资源。 在任务提交的过程中,Spark会自动将应用程序的jar包和依赖的库文件上传到集群中,并在集群中启动Executor进程来执行任务。任务执行完成后,Spark会将结果返回给Driver进程,并将Executor进程关闭。 总之,Spark submit任务提交是Spark应用程序运行的关键步骤,掌握任务提交的原理和方法对于开发和调试Spark应用程序非常重要。 ### 回答2: Spark 作为一款强大的分布式计算框架,提供了很多提交任务的方式,其中最常用的方法就是通过 spark-submit 命令来提交任务。spark-submit 是 Spark 提供的一个命令行工具,用于在集群上提交 Spark 应用程序,并在集群上运行。 spark-submit 命令的语法如下: ``` ./bin/spark-submit [options] <app jar | python file> [app arguments] ``` 其中,[options] 为可选的参数,包括了执行模式、执行资源等等,<app jar | python file> 为提交的应用程序的文件路径,[app arguments] 为应用程序运行时的参数。 spark-submit 命令会将应用程序的 jar 文件以及所有的依赖打包成一个 zip 文件,然后将 zip 文件提交到集群上运行。在运行时,Spark 会根据指定的主类(或者 Python 脚本文件)启动应用程序。 在提交任务时,可以通过设置一些参数来控制提交任务的方式。例如: ``` --master:指定该任务运行的模式,默认为 local 模式,可设置为 Spark Standalone、YARN、Mesos、Kubernetes 等模式。 --deploy-mode:指定该任务的部署模式,默认为 client,表示该应用程序会在提交任务的机器上运行,可设置为 cluster,表示该应用程序会在集群中一台节点上运行。 --num-executors:指定该任务需要的 executor 数量,每个 executor 会占用一个计算节点,因此需要根据集群配置与任务要求确定该参数的值。 --executor-memory:指定每个 executor 可用的内存量,默认为 1g,可以适当调整该值以达到更好的任务运行效果。 ``` 此外,还有一些参数可以用来指定应用程序运行时需要传递的参数: ``` --conf:指定应用程序运行时需要的一些配置参数,比如 input 文件路径等。 --class:指定要运行的类名或 Python 脚本文件名。 --jars:指定需要使用的 Jar 包文件路径。 --py-files:指定要打包的 python 脚本,通常用于将依赖的 python 包打包成 zip 文件上传。 ``` 总之,spark-submit 是 Spark 提交任务最常用的方法之一,通过该命令能够方便地将应用程序提交到集群上运行。在提交任务时,需要根据实际场景调整一些参数,以达到更好的任务运行效果。 ### 回答3: Spark是一个高效的分布式计算框架,其中比较重要的组成部分就是任务提交。在Spark中,任务提交主要通过spark-submit来实现。本文将从两方面,即任务提交之前的准备工作和任务提交过程中的细节进行探讨。 一、任务提交之前的准备工作 1.环境配置 在执行任务提交前,需要确保所在的计算机环境已经配置好了SparkSpark的环境配置主要包括JAVA环境、Spark的二进制包、PATH路径配置、SPARK_HOME环境变量配置等。 2.编写代码 Spark的任务提交是基于代码的,因此在任务提交前,需要编写好自己的代码,并上传到集群中的某个路径下,以便后续提交任务时调用。 3.参数设置 在任务提交时,需要对一些关键的参数进行设置。例如,任务名、任务对应的代码路径、任务需要的资源、任务需要的worker节点等。 二、任务提交过程中的细节 1.启动Driver 当使用spark-submit命令提交任务时,Spark会启动一个Driver来运行用户的代码。这个Driver通常需要连接到Spark集群来执行任务。 2.上传文件 Spark支持在任务提交时上传所需的文件。这些文件可以用于设置Spark的环境变量、为任务提供数据源等。 3.资源需求 Spark的任务执行依赖于一定的资源。每个任务可以指定自己的资源需求,例如需要多少内存、需要多少CPU等。这些资源需求通常与提交任务时需要的worker节点数量有关系。 4.监控和日志 在任务执行的过程中,Spark会收集任务的监控数据和日志信息。这些数据可用于后续的调试和性能优化。 总之,在Spark任务提交过程中,需要充分考虑任务的资源需求和监控日志信息的收集,以便更好地完成任务和优化Spark运行效率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值