文章目录
-
- 1、功能描述
-
- 2、相关组件
- 3、代码分析
- 3.1初始设置
- 3.2 创建执行环境SparkEnv
- 3.3 创建SparkUI
- 3.4 Hadoop 相关配置
- 3.5 Executor 环境变量
- 3.6 注册HeartbeatR`eceiver心跳接收器
- 3.7 创建任务调度器TaskScheduler
- 3.8 创建和启动DAGScheduler
- 3.9 TaskScheduler启动
- 3.10 启动测量系统MetricsSystem
- 3.11 创建时间日志监听器
- 3.12 创建和启动 ExecutorAllocationManager
- 3.13 ContextCleaner的创建与启动
- 3.14 额外的SparkKistener与启动事件
- 3.15 Spark环境更新
- 3.16 投递应用程序启动事件
- 3.17 创建`DAGSchedulerSource`、`BlockManagerSource`、和`ExecutorAllocationManagerSource`
- 3.18 将SparkContext添加进 终止处理程序ShutdownHookManager
- 3.19 提示是否创建成功
- 3.19 将SparkContext标记为激活
文章目录
- 1、功能描述
- 2、相关组件
- 3、代码分析
- 3.1初始设置
- 3.2 创建执行环境SparkEnv
- 3.3 创建SparkUI
- 3.4 Hadoop 相关配置
- 3.5 Executor 环境变量
- 3.6 注册HeartbeatR`eceiver心跳接收器
- 3.7 创建任务调度器TaskScheduler
- 3.8 创建和启动DAGScheduler
- 3.9 TaskScheduler启动
- 3.10 启动测量系统MetricsSystem
- 3.11 创建时间日志监听器
- 3.12 创建和启动 ExecutorAllocationManager
- 3.13 ContextCleaner的创建与启动
- 3.14 额外的SparkKistener与启动事件
- 3.15 Spark环境更新
- 3.16 投递应用程序启动事件
- 3.17 创建`DAGSchedulerSource`、`BlockManagerSource`、和`ExecutorAllocationManagerSource`
- 3.18 将SparkContext添加进 终止处理程序ShutdownHookManager
- 3.19 提示是否创建成功
- 3.19 将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官网,可以看到SparkContext处于DriverProgram核心位置,所有与Cluster、Worker Node交互的操作都需要SparkContext来完成。
图2 SparkContext 在 Spark 应用程序中的扮演的主要角色
图3 Driver 上运行的服务组件
2、相关组件
名称 |
|
---|---|
SparkConf | Spark 配置类,配置已键值对形式存储,封装了一个ConcurrentHashMap类实例settings用于存储Spark的配置信息。 |
SparkEnv | SparkContext中非常重要的类,它维护着Spark的执行环境,所有的线程都可以通过SparkContext访问到同一个SparkEnv对象。 |
LiveListenerBus | SparkContext 中的事件总线,可以接收各种使用方的事件,并且异步传递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 进一步处理。 |
ExecutorAllocationManager | Executor 动态分配管理器,根据负载动态的分配与删除Executor,可通过其设置动态分配最小Executor、最大Executor、初始Executor数量等配置。 |
ContextClearner | 上下文清理器,为RDD、shuffle、broadcast状态的异步清理器,清理超出应用范围的RDD、ShuffleDependency、Broadcast对象。 |
SparkStatusTracker | 低级别的状态报告API,只能提供非常脆弱的一致性机制,对Job(作业)、Stage(阶段)的状态进行监控。 |
HadoopConfiguration | Spark默认使用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 | |
9 | Hadoop相关配置及Executor环境变量的设置 | |
10 | 注册HeartbeatReceiver心跳接收器 | |
11 | 创建TaskScheduler | |
12 | 创建DAGScheduler | |
13 | 启动TaskScheduler | |
14 | 初始化块管理器BlockManager | |
15 | 启动测量系统MetricsSystem | |
16 | 创建事件日志监听器 | |
17 | 创建和启动Executor分配管ExecutorAllocationManager | |
18 | 创建和启动ContextCleaner | |
19 | 额外的 SparkListenser 与启动事件总线(setupAndStartListenerBus) | |
20 | Spark环境更新(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
创建 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()
调用的是TaskSchedulerImpl
的start()
方法: - 最后,
TaskSchedulerImpl
的start()
方法中又调用了backend.start()
3.10 启动测量系统MetricsSystem
MetricsSystem中三个概念:
Instance
: 指定了谁在使用测量系统;
Spark按照Instance的不同,区分为Master、Worker、Application、Driver和Executor;Source
: 指定了从哪里收集测量数据;
Source有两种来源:Spark internal source: MasterSource/WorkerSource等; Common source: JvmSourceSink
:指定了往哪里输出测量数据;
Spark目前提供的Sink有ConsoleSink、CsvSink、JmxSink、MetricsServlet、GraphiteSink等;Spark使用MetricsServlet作为默认的Sink。
MetricsSystem的启动过程包括:
- 注册Sources;
- 注册Sinks;
- 将Sinks增加Jetty的ServletContextHandler;
启动准备:
- 驱动程序的度量系统需要
设置spark.app.id
为app 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.enabled
为true
来创建。 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
的执行步骤如下:
- 从
spark.extraListeners
属性中获取用户自定义的 SparkListener的类名。用户可以通过逗号分割多个自定义SparkListener
。 - 通过发射生成每一个自定义
SparkListener
的实例,并添加到事件总线的监听器列表中。 - 启动事件总线,并将
_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
方法加入httpFileServer
的jarDir
变量指定的路径下。每加入一个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
方法处理步骤:
- 通过调用
SparkEnv
的方法environmentDetails
,将环境的JVM 参数
、Spark 属性
、系统属性
、classPath
等信息设置为环境明细信息
。 - 生成事件
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 创建DAGSchedulerSource
、BlockManagerSource
、和ExecutorAllocationManagerSource
首先要调用taskScheduler
的postStartHook
方法,其目的是为了等待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
程序挂掉的时候,处理一些清理工作
创建流程:
- 先强制创建记录器
- 调用
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)