本篇文章就要根据源码分析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 的灵魂。
/**
* 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 上运行的服务组件
名称 |
|
---|---|
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 |
事件记录器 |
|
|
|
思维导图版
序号 |
|
|
---|---|---|
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标记为激活 |
首先保存了当前的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
}
}
SparkEnv是Spark的执行环境对象,其中包括与众多Executor指向相关的对象。
CoarseGrainedExecutorBackend
进程中也会创建Executor
创建SparkEnv主要使用
createDriverEnv
方法(有四个参数:conf、isLocal、listenerBus )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
}
}
// 变量声明
// 包名: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())
默认情况下,Spark使用HDFS作为分布式文件系统,所以需要获取Hadoop相关的配置信息:
private var _hadoopConfiguration: Configuration = _
//获取hadoop配置信息
_hadoopConfiguration = SparkHadoopUtil.get.newConfiguration(_conf)
获取的配置信息包括:
// 第一步
// 包名: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)
}
}
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
实际生产环境
中,Executor 是运行在不同的节点上
的。local 模式下
的 Driver 与 Executor 属于同一个进程
,所以 *Dirver 与 Executor 可以直接使用本地调用交互,当 Executor 运行出现问题时,Driver 可以很方便地知道 *,例如,通过捕获异常。在生产环境下
,Driver 与 Executor 很可能不在同一个进程内
,他们也许运行在不同的机器上,甚至在不同的机房里,因此 Driver 对 Executor 失去掌握。为了能够掌控 Executor,在 Driver 中创建了这个心跳接收器
。创建 HeartbearReceiver 的代码:
// 变量声明
// 包名: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)
}
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)
// 包名: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()
}
DAGScheduler主要用于在任务正式交给TaskScheduler提交之前做一些准备工作,包括:
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)
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()
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的启动过程包括:
启动准备:
设置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)))
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
}
}
ExecutorAllocationManager
用于对以分配的Executor
进行管理。
ExecutorAllocationManager
,可以修改属性spark.dynamicAllocation.enabled
为true
来创建。ExecutorAllocationManager
可以: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())
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())
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
在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)
}
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))
}
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)
}
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
}
}
//提示你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
}
如果创建成功,上一步没有抛异常,那么SparkContext
初始化的最后会将当前SparkContext
的状态从contextBeingConstructed(正在构建中)
改为activeContext(已激活)
:
// 包名:org.apache.spark
// 类名:SparkContext
//目的是为了防止多个SparkContext同时处于活动状态,因此请将此上下文标记为已完成构造。
//注意:这必须放在SparkContext构造函数的末尾。
SparkContext.setActiveContext(this, allowMultipleContexts)