SparkContext
简要介绍
SparkDriver 的初始化始终围绕着 SparkContext 的初始化 。SparkContext 可以算得上是 Spark 应用程序的发动机引擎,SparkContext 初始化完毕,才能向 Spark 集群提交应用程序,而 SparkContext 的配置参数则由 SparkConf 负责
SparkContext 主要由以下内部组件组成
- SparkEnv : Spark 运行时环境,Executor是处理任务的执行器,它依赖于SparkEnv提供的运行时环境。此外,在Driver中也包含了SparkEnv,这是为了保证local模式下任务的执行
- LiveListenerBus : SparkContext 中的事件总线,可以接收各个使用方的事件,并且通过异步方式对事件进行匹配后调用SparkListener 的不同方法
- SparkUI: Spark 的用户界面,SparkUI 间接依赖于计算引擎、调度系统、存储体系,作业(Job)、阶段(Stage)、存储、执行器(Executor)等组件的监控数据都会以 SparkListenerEvent 的形式投递到 LiveListenerBus 中,SparkUI 将从各个 SparkListener 中读取数据并显示到 Web 界面
- SparkStatusTracker : 提供对作业、Stage(阶段)等的监控信息
- ConsoleProgressBar : 利用 SparkStatusTracker 的API,在控制台展示 Stage 的进度
- DAGScheduler : DAG 调度器,是调度系统中的重要组件之一,负责创建 Job,将 DAG 中的 RDD 划分到不同的 Stage、提交 Stage等
- Taskscheduler : 任务调度器,是调度系统中的重要组件之一。TaskScheduler 按照调度算法对集群管理器已经分配给应用程序的资源进行二次调度后分配给任务。 TaskScheduler 调度的 Task 是由 DAGScheduler 创建的,所以 DAGScheduler 是 TaskScheduler 的前置调度
- HeartbeatReceiver : 心跳接收器。所有 Executor 都会向 HeartbeatReceiver 发送心跳信息,HeartbeatReceiver 接收到 Executor 的心跳信息后,首先更新 Executor 的最后可见时间,然后将此信息交给 TaskScheduler 作进一步处理
- ContextCleaner : 上下文清理器。ContextCleaner 实际用异步方式清理那些超出应用作用域范围的 RDD、ShuffleDependency 和Broadcast 等信息
- EventLoggingListener : 将事件持久化到存储的监听器,是 SparkContext 中的可选组件。当 spark.eventLog.enabled 属性 true 时启用
- ExecutorAllocationManager : Executor 动态分配管理器。可以根据工作负载动态调整 Executor 的数量。在配置spark.dynamicAllocation.enabled 属性为 true 的前提下,在非 local 模式下或者当 spark.dynamicAllocation.testing 属性为 true 时启用
- ShutdownHookManager : 用于设置关闭钩子的管理器。可以给应用设置关闭钩子, 这样就可以在 JVM 进程退出时,执行一些清理工作
SprkContext 内部有以下属性
- creationSite : 类型为 CallSite,其中保存着线程栈中最靠近栈顶的用户定义的类及最靠近栈底的 Scala 或者 Spark 核心类信息,CallSite 的 shortForm 属性保存着以上信息的简短描述,CallSite 的 longForm 属性则保存着以上信息的完整描述
- allowMultipleContexts : 是否允许多个 SparkContext 实例。默认 false,可以通过属性 spark.driver.allowMultipleContexts 来控制
- startTime : SparkContext 启动的时间戳
- stopped : 标记 SparkContext 是否已经停止的状态,采用原子类型 AtomicBoolean
- addedFiles : 用于每个本地文件的 URL 与添加此文件到 addedFiles 时的时间戳之间的映射缓存
- addedJars : 用于每个本地 Jar 文件的 URL 与添加此文件到 addedJars 时的时间戳之间的映射缓存
- persistentRdds : 用于对所有持久化的 RDD 保持跟踪。
- executorEnvs : 用于存储环境变量。executorEnvs 中环境变量都将传递给执行任务的 Executor 使用
- sparkUser : 当前系统的登录用户,也可以通过系统环境变量 SPARK_USER 进行设置。这里使用的是 Utils 的 getCurrentUserName 方法
- checkpointDir : RDD 计算过程中保存检查点时所需要的目录
- localProperties : 由 InheritableThreadLocal 保护的线程本地变量,其中的属性值可以沿着线程栈传递下去,供用户使用
- conf : SparkContext的配置,通过调用 SparkConf 的 clone 方法的克隆体。在 SparkContext 初始化的过程中会对 conf 中的配置信息做校验
- jars : 用户设置的 Jar 文件。当用户选择的部署模式是 YARN 时,jars 是由 spark.jars 属性指定的 Jar 文件和 spark.yarn.distjars 属性指定的 Jar 文件的并集。其他模式下只来用由 spark.jars 属性指定的 Jar 文件。这里使用了 Utils 的 getUserJars 方法
- _files : 用户设置的文件。可以使用 spark.files 属性进行指定
- _eventLogDir : 事件日志的路径。当 spark.eventLog.enabled 属性为true时启用。默认为 /tmp/spark-events,也可以通过spark.eventLog.dir 属性指定
- _eventLogCodec : 事件日志的压缩算法。当 spark.eventL.og.enabled 属性与 spark.eventLog.compress 属性皆为 true 时启用。压缩算法默认为 Iz4,也可以通过 spark.io.compression.codec 属性指定。Spark 目前支持的压缩算法包括 Izf、snappy 和 Iz4 这3种
- _hadoopConfiguration : Hadoop的配置信息,具体根据 Hadoop(Hadoop2.0之前的版本)和 HadoopYARN(Hadoop2.0+的版本)的环境有所区别。如果系统属性 SPARK_YARN_MODE 为 true 或者环境变量 SPARK_YARN_MODE 为 true,那么将会是 YARN 的配置,否则为Hadoop配置
- _executorMemory : Executor的内存大小。默认值为1024MB。可以通过设置环境变量(SPARK_EXECUTOR_MEMORY)或者spark.executor.memory 属性指定。其中,spark.executor.memory 的优先级最高,SPARK_EXECUTOR_MEMORY 次之
- _applicationld : 当前应用的标识。TaskScheduler 启动后会创建应用标识,SparkContext 中的 _applicationld 就是通过调用TaskScheduler 的 applicationld 方法获得的
- _applicationAttemptld : 当前应用尝试执行的标识。SparkDriver 在执行时会多次尝试执行,每次尝试都将生成一个标识来代表应用尝试执行的身份
- _listenerBusStarted : LiveListenerBus 是否已经启动的标记
Spark 环境的创建简析(SparkEnv)
// 将 LiveListenerBus 通过 SparkEnv 的构造器传递给 SparkEnv 及 SparkEnv 内部的组件
_env = createSparkEnv(_conf, isLocal, listenerBus)
// SparkEnv 实例的引用将通过 SparkEnv 的 set 方法设置到 SparkEnv 伴生对象的 env属性中,这将便于在任何需要 SparkEnv 的地方,直接通过伴生对象的 get 方法获取
SparkEnv.set(_env)
// createDriverEnv 方法用于创建 SparkEnv,此方法将为 Driver 实例创建 SparkEnv
private[spark] def createSparkEnv(
conf: SparkConf,
isLocal: Boolean,
listenerBus: LiveListenerBus): SparkEnv = {
// numDriverCores 方法就定义了 local[*] / yarn 等方式的cpu的核心数的设置
SparkEnv.createDriverEnv(conf, isLocal, listenerBus, SparkContext.numDriverCores(master, conf))
}
从上面的代码说明可以看到实际是调用了 SparkEnv 的 createDriverEnv 方法来创建了 SparkEnv
private[spark] def createDriverEnv(
conf: SparkConf,
isLocal: Boolean,
listenerBus: LiveListenerBus,
numCores: Int,
mockOutputCommitCoordinator: Option[OutputCommitCoordinator] = None): SparkEnv = {
assert(conf.contains(DRIVER_HOST_ADDRESS),
s"${DRIVER_HOST_ADDRESS.key} is not set on the driver!")
assert(conf.contains("spark.driver.port"), "spark.driver.port is not set on the driver!")
// 按照优先级从高到低,通过spark.driver.bindAddress 属性、spark.driver.host 属性及调用 Utils 的 localHostName 方法获得 bindAddress
val bindAddress = conf.get(DRIVER_BIND_ADDRESS)
// Driver 实例对外显示的 host,可以通过 spark.driver.host 属性及调用 Utils 的 localHostName 方法获得
val advertiseAddress = conf.get(DRIVER_HOST_ADDRESS)
// Driver 实例的端口,可以通过 spark.driver.port 属性指定
val port = conf.get("spark.driver.port").toInt
// I/O加密的密钥。当 spark.io.encryption.enabled 属性为 true 时,调用 CryptoStreamUtils 的 createKey 方法创建密钥
val ioEncryptionKey = if (conf.get(IO_ENCRYPTION_ENABLED)) {
Some(CryptoStreamUtils.createKey(conf))
} else {
None
}
// 这里才是真正创建 SparkEnv 的实现
create(
conf,
SparkContext.DRIVER_IDENTIFIER,
bindAddress,
advertiseAddress,
Option(port),
isLocal,
numCores,
ioEncryptionKey,
listenerBus = listenerBus,
mockOutputCommitCoordinator = mockOutputCommitCoordinator
)
}
心跳接收器的实现简析
// 使用了 SparkEnv 的子组件 NettyRpcEnv 的 setupEndpoint 方法,此方法的作用是向 RpcEnv 的 Dispatcher 注册 HeartbeatReceiver,并返回 NettyRpcEndpointRef 引用
_heartbeatReceiver = env.rpcEnv.setupEndpoint(
HeartbeatReceiver.ENDPOINT_NAME, new HeartbeatReceiver(this))
调度系统的创建和启动简析
TaskScheduler 是 SparkContext 的重要组成部分,负责请求集群管理器给应用程序分配并运行 Executor(一级调度)和给任务分配Executor 并运行任务(二级调度) TaskScheduler也可以看做任务调度的客户端
DAGScheduler主要用于在任务正式交给 TaskSchedulerImpl 提交之前做一些准备工作,包括创建 Job、将 DAG 中的 RDD 划分到不同的Stage 、提交 Stage 等
// 此方法用于创建任务调度器,此方法返回 SchedulerBackend 和 TaskScheduler 的 tuple
val (sched, ts) = SparkContext.createTaskScheduler(this, master, deployMode)
_schedulerBackend = sched
_taskScheduler = ts
_dagScheduler = new DAGScheduler(this)
// 向 HeartbeatReceiver 发送 TaskSchedulerIsSet 消息,表示 SparkContext 的 _taskScheduler 属性已经持有了TaskScheduler 的引用,HeartbeatReceiver 接收到 TaskSchedulerIsSet 消息后,将获取 SparkContext 的 _taskScheduler属性设置到自身的 scheduler 属性中
_heartbeatReceiver.ask[Boolean](TaskSchedulerIsSet)
// 创建完 TaskScheduler 后,在这里启动 TaskScheduler,在 TaskScheduler 启动的时候,还会启动 DAGScheduler
_taskScheduler.start()
块管理器(BlockManager)的初始化简析
BlockManager 囊括了存储体系的所有组件和功能,是存储体系中最重要的组件
// _applicationId 是应用程序向 Master 注册时,Master创建的应用标识
_env.blockManager.initialize(_applicationId)
度量系统的启动简析
MetricsSystem 对 Source 和 Sink 进行封装,将 Source 的数据输出到不同的 Sink,Metrics-System 是 SparkEnv 内部的组件之一,是整个 Spark 应用程序的度量系统
_env.metricsSystem.start()
// 调用了 WebUI 的 attachHandler 方法将度量系统的 ServletContextHandlers 添加到了 Spark UI 中
_env.metricsSystem.getServletHandlers.foreach(handler => ui.foreach(_.attachHandler(handler)))
事件日志监听器的创建简析
EventLoggingListener 是将事件持久化到存储的监听器,是 SparkContext 中的可选组 件。当 spark.eventLog.enabled 属性为true时启用
_eventLogger =
if (isEventLogEnabled) {
val logger =
new EventLoggingListener(_applicationId, _applicationAttemptId, _eventLogDir.get,
_conf, _hadoopConfiguration)
logger.start()
// EventLoggingListener 也将参与到对事件总线中事件的监听中,并把感兴趣的事件记录到日志
listenerBus.addToEventLogQueue(logger)
Some(logger)
} else {
None
}
// EventLoggingListener 最为核心的方法,用于将事件转换为 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 的创建和启动简析
ExecutorAllocationManager 是基于工作负载动态分配和删除 Executor 的代理。
ExecutorAllocationManager 内部会定时根据工作负载计算所需的 Executor 数量
- 如果对 Executor 需求数量大于之前向集群管理器申请的 Executor 数量,那么向集群管理器申请添加Executor
- 如果对 Executor 需求数量小于之前向集群管理器申请的 Executor 数量,那么向集群管理器申请取消部分Executor
- ExecutorAllocationManager 内部还会定时向集群管理器申请移除(“杀死”)过期的 Executor
// 非Local模式下或当spark.dynamicAllocation.testing属性为true时启用ExecutorAllocationManager,首先判断是否需要启动 ExecutorAllocationManager
val dynamicAllocationEnabled = Utils.isDynamicAllocationEnabled(_conf)
_executorAllocationManager =
if (dynamicAllocationEnabled) {
schedulerBackend match {
// 同时这里还有一层判断,只有当 SchedulerBackend 的实现类同时实现了特质 ExecutorAllocationClient 的情况下,才会创建 ExecutorAllocationManager,其实这里只有 CoarseGrainedSchedulerBackend 满足条件
case b: ExecutorAllocationClient =>
Some(new ExecutorAllocationManager(
schedulerBackend.asInstanceOf[ExecutorAllocationClient], listenerBus, _conf,
_env.blockManager.master))
case _ =>
None
}
} else {
None
}
// 对所有的 ExecutorAllocationClient 调用 start 方法来启动
_executorAllocationManager.foreach(_.start())
def start(): Unit = {
// 向事件总线添加 ExecutorAllocationListener
listenerBus.addToManagementQueue(listener)
// 创建定时调度任务 scheduleTask,用来调用 schedule 方法
val scheduleTask = new Runnable() {
override def run(): Unit = {
try {
// 这个定时任务会按照固定的时间间隔调用 schedule 方法,以调整待执行 Executor 请求的数量和运行的 Executor 的数量
schedule()
} catch {
case ct: ControlThrowable =>
throw ct
case t: Throwable =>
logWarning(s"Uncaught exception in thread ${Thread.currentThread().getName}", t)
}
}
}
// 将 scheduleTask 提交给 executor (executor是只有一个线程的ScheduledThreadPoolExecutor),以固定的间隔intervalMillis (值为100)进行调度
executor.scheduleWithFixedDelay(scheduleTask, 0, intervalMillis, TimeUnit.MILLISECONDS)
// 用 ExecutorAllocationClient 的 requestTotalExecutors 方法清求所有的 Executor
// 1. numExecutorsTarget是动态分配Executor的总数,取spark.dynamicAllocation.initialExecutors、spark.dynamicAllocation.minExecutors、spark.executor.instances三个属性配置的最大值
// 2.localityAwareTasks 是由本地性偏好的 Task 数量
// 3.hostToLocalTaskCount 是 Host 与想要在此节点上运行的 Task 的效量之间的映射关系
client.requestTotalExecutors(numExecutorsTarget, localityAwareTasks, hostToLocalTaskCount)
}
private def schedule(): Unit = synchronized {
val now = clock.getTimeMillis
val executorIdsToBeRemoved = ArrayBuffer[String]()
removeTimes.retain { case (executorId, expireTime) =>
val expired = now >= expireTime
if (expired) {
initializing = false
executorIdsToBeRemoved += executorId
}
!expired
}
// 调用该方法重新计算所需的 Executor 数量,并更新请求的 Executor数量
updateAndSyncNumExecutorsTarget(now)
if (executorIdsToBeRemoved.nonEmpty) {
// 对过期的 Executor 进行删除,这里会调用 ExecutorAllocationClient 的 killExecutors方法通知集群管理器“杀死”Executor , killExecutors 方法需要 ExecutorAllocationClient 的实现类去实现
removeExecutors(executorIdsToBeRemoved)
}
}
private def updateAndSyncNumExecutorsTarget(now: Long): Int = synchronized {
// 获得实际需要的 Executor 的最大数量 MaxNeeded
val maxNeeded = maxNumExecutorsNeeded
// 如果还在初始化,那么返回0
if (initializing) {
0
/** 如果 Executor 的目标数量(numExecutorsTarget)超过我们实际需要的数量(maxNeeded),那么首先将numExecutorsTarget 设置为 maxNeeded 与最小 Executor 数量(minNumExecutors)之间的最大值,然后调用ExecutorAllocationClient 的 requestTotalExecutors 方法重新请求 numExecutorsTarget 指定的目标 Executor 数量,以此停止添加新的执行程序,并通知集群管理器取消额外的待处理 Executor 的请求,最后返回减少的 Executor 数量*/
} else if (maxNeeded < numExecutorsTarget) {
val oldNumExecutorsTarget = numExecutorsTarget
numExecutorsTarget = math.max(maxNeeded, minNumExecutors)
numExecutorsToAdd = 1
if (numExecutorsTarget < oldNumExecutorsTarget) {
client.requestTotalExecutors(numExecutorsTarget, localityAwareTasks, hostToLocalTaskCount)
logDebug(s"Lowering target number of executors to $numExecutorsTarget (previously " +
s"$oldNumExecutorsTarget) because not all requested executors are actually needed")
}
numExecutorsTarget - oldNumExecutorsTarget
/** 如果 maxNeeded 大于等于 numExecutorsTarget,且当前时间大于上次添加 Executor 的时间,那么首先调用addExecutors 方法通知集群管理器添加额外的 Executor ,然后更新添加 Executor 的时间,最后返回添加的 Executor 数量 */
} else if (addTime != NOT_SET && now >= addTime) {
val delta = addExecutors(maxNeeded)
logDebug(s"Starting timer to add more executors (to " +
s"expire in $sustainedSchedulerBacklogTimeoutS seconds)")
addTime = now + (sustainedSchedulerBacklogTimeoutS * 1000)
delta
} else {
0
}
}
ContextCleaner的创建和启动简析
ContextCleaner 用于清理那些超出应用范围的 RDD、shuffle 对应的 map 任务状态、shuffle 元数据、Broadcast 对象及 RDD 的Checkpoint 数据
ContextCleaner的创建
_cleaner =
// 通过配置属性 spark.cleaner.referenceTracking(默认是true)来决定是否启用 ContextCleaner
if (_conf.getBoolean("spark.cleaner.referenceTracking", true)) {
Some(new ContextCleaner(this))
} else {
None
}
_cleaner.foreach(_.start())
ContextCleaner的组成
- referenceQueue : 缓存顶级的 AnyRef 引用
- referenceBuffer : 缓存 AnyRef 的虚引用
- listeners : 缓存清理工作的监听器数组
- cleaningThread : 用于具体清理工作的线程,此线程为守护线程,名称为SparkContextCleaner
- periodicGCService : 类型为 ScheduledExecutorService,用于执行 GC 的调度线程池,此线程池只包含一个线程,启动的线程名称以 context-cleaner-periodic-ge 开头
- periodicGCInterval : 执行GC的时间间隔,可通过 spark.cleanerperiodicGC.interval 属性进行配置,默认是30分钟
- blockOnCleanupTasks : 清理非 Shuffle 的其他数据是否是阻塞式的。可通过 spark.cleaner.referenceTracking.blocking 属性进行配置,默认是true
- blockOnShuffleCleanupTasks : 清理 Shuffle 数据是否是阻塞式的。可通过 spark.cleaner.referenceTracking.blocking.shuffle 属性进行配置,默认是false。清理Shuffle数据包括清理 MapOutputTracker 中指定 Shuffleld 对应的 map 任务状态和 ShuffleManager 中注册的 Shuffleld 对应的 Shuffle 元数据。
- stopped : ContextCleaner是否停止的状态标记
ContextCleaner的启动
SparkContext 在初始化的过程中会启动 ContextCleaner,只有这样 ContextCleaner 才能够清理那些超出应用范围的 RDD、Shuffle 对应的 map 任务状态、Shuffle 元数据、Broadcast 对象及 RDD 的 Checkpoint 数据
def start(): Unit = {
// 将 cleaningThread 设置为守护线程
cleaningThread.setDaemon(true)
// 指定 cleaningThread 的名称为 Spark Context Cleaner
cleaningThread.setName("Spark Context Cleaner")
// 启动 cleaningThread
cleaningThread.start()
// 给periodicGCService 设置以 periodicGCService 作为时间间隔定时进行 GC 操作的任务
periodicGCService.scheduleAtFixedRate(new Runnable {
override def run(): Unit = System.gc()
}, periodicGCInterval, periodicGCInterval, TimeUnit.SECONDS)
}
ContextCleaner 其余部分的工作原理和 listenerBus 一样,都是采用了监听器模式,由异步线程来处理,在异步线程中实际只是调用 keepCleaning 方法
private val cleaningThread = new Thread() { override def run() { keepCleaning() }}
private def keepCleaning(): Unit = Utils.tryOrStopSparkContext(sc) {
while (!stopped) {
try {
// 获取到引用
val reference = Option(referenceQueue.remove(ContextCleaner.REF_QUEUE_POLL_TIMEOUT))
.map(_.asInstanceOf[CleanupTaskWeakReference])
// Synchronize here to avoid being interrupted on stop()
synchronized {
reference.foreach { ref =>
logDebug("Got cleaning task " + ref.task)
referenceBuffer.remove(ref)
// 匹配到不同的引用,执行相应的方法进行清理
ref.task match {
case CleanRDD(rddId) =>
doCleanupRDD(rddId, blocking = blockOnCleanupTasks)
case CleanShuffle(shuffleId) =>
doCleanupShuffle(shuffleId, blocking = blockOnShuffleCleanupTasks)
case CleanBroadcast(broadcastId) =>
doCleanupBroadcast(broadcastId, blocking = blockOnCleanupTasks)
case CleanAccum(accId) =>
doCleanupAccum(accId, blocking = blockOnCleanupTasks)
case CleanCheckpoint(rddId) =>
doCleanCheckpoint(rddId)
}
}
}
} catch {
case ie: InterruptedException if stopped => // ignore
case e: Exception => logError("Error in cleaning thread", e)
}
}
}
def doCleanupRDD(rddId: Int, blocking: Boolean): Unit = {
try {
logDebug("Cleaning RDD " + rddId)
// 调用 SparkContext 的 unpersistRDDs 方法从内存或磁盘中移除 RDD,从 persistRDDs 中移除 RDD 的跟踪
sc.unpersistRDD(rddId, blocking)
// 调用所有监听器的 rddCleaned 方法
listeners.asScala.foreach(_.rddCleaned(rddId))
logInfo("Cleaned RDD " + rddId)
} catch {
case e: Exception => logError("Error cleaning RDD " + rddId, e)
}
}
额外的SparkListener与事件总线启动简析
SparkContext 中通过 setupAndStartListenerBus() 方法添加用于自定义 SparkListener 的地方
private def setupAndStartListenerBus(): Unit = {
try {
// 获取用户自定义的 SparkListener 的类名后,通过反射生成每一个自定义 SparkListener 的实例,并添加到事件总线的监听器列表中
conf.get(EXTRA_LISTENERS).foreach { classNames =>
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)
}
}
// 启动事件总线并且期将监控也添加进去
listenerBus.start(this, _env.metricsSystem)
// 修改状态
_listenerBusStarted = true
}
Spark环境更新简析
指定用户任务执行所依赖的文件
// 读取用户设置的 Jar 文件
_jars = Utils.getUserJars(_conf)
// 读取用户设置的其他文件
_files = _conf.getOption("spark.files").map(_.split(",")).map(_.filter(_.nonEmpty))
.toSeq.flatten
当用户选择的部署模式是 YARN 时,_jars 是由 spark.jars 属性指定的 Jar 文件和 spark.yarn.dist.jars 属性置顶的 Jar 文件的并集。其他模式则只采用由 spark.jars 属性指定的 Jar 文件
各个节点运行时获取到文件
def jars: Seq[String] = _jars
def files: Seq[String] = _files
if (jars != null) {
// 遍历每一个 Jar 文件并调用 addJar 方法
jars.foreach(addJar)
}
if (files != null) {
// 遍历每一个其他文件并调用 addFile 方法
files.foreach(addFile)
}
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")
}
// 在这个方法中实际将 Jar 包放到节点的是这个语句,这里将调用 SparkEnv 的 fileServer 的 addJar 方法把 Jar 文件添加到 Driver 本地 RpcEnv 的 NettyStreamManager 中,并将 Jar 文件添加的时间戳信息缓存到 addedJars 中。
// addFile 也是类似的操作
// 接下来 Executor 节点就可以使用 RPC 从 Driver 将文件下载到本地,以供任务执行
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")
// 因为 addJar 和 addFile 可能对应用的环境产生影响,所以在初始化过程的最后需要调用此方法更新环境
postEnvironmentUpdate()
} else {
logWarning(s"The jar $path has been added already. Overwriting of added jars " +
"is not supported in the current version.")
}
}
}
}
private def postEnvironmentUpdate() {
if (taskScheduler != null) {
val schedulingMode = getSchedulingMode.toString
val addedJarPaths = addedJars.keys.toSeq
val addedFilePaths = addedFiles.keys.toSeq
// 将 JVM 参数、spark 属性、系统属性、classpath 等信息设置为环境明细信息
val environmentDetails = SparkEnv.environmentDetails(conf, schedulingMode, addedJarPaths,
addedFilePaths)
// 生成携带环境明细信息的事件 SparkListenerEnvironmentUpdate
val environmentUpdate = SparkListenerEnvironmentUpdate(environmentDetails)
// 投递到事件总线 listenerBus,此事件最终将被 EnvironmentListener 监听
listenerBus.post(environmentUpdate)
}
}
SparkContext 初始化的收尾简析
// 向事件总线投递 SparkListenerApplicationStart 事件
postApplicationStart()
// 等待 SchedulerBackend 准备完成
_taskScheduler.postStartHook()
// 向度量系统注册 DAGSchedulerSource 、 BlockManagerSource 及 ExecutorAllocationManagerSource
_env.metricsSystem.registerSource(_dagScheduler.metricsSource)
_env.metricsSystem.registerSource(new BlockManagerSource(_env.blockManager))
_executorAllocationManager.foreach { e =>
_env.metricsSystem.registerSource(e.executorAllocationManagerSource)
}
logDebug("Adding shutdown hook") // force eager creation of logger
// 添加 SparkContext 的关闭钩子,使得 JVM 退出之前调用 SparkContext 的 stop 方法进行一些关闭操作
_shutdownHookRef = ShutdownHookManager.addShutdownHook(
ShutdownHookManager.SPARK_CONTEXT_SHUTDOWN_PRIORITY) { () =>
logInfo("Invoking stop() from shutdown hook")
try {
stop()
} catch {
case e: Throwable =>
logWarning("Ignoring Exception while stopping SparkContext from shutdown hook", e)
}
}
/**......*/
// 将 SparkContext 标记为激活
SparkContext.setActiveContext(this, allowMultipleContexts)
SparkContext 提供的一些常用方法简析
broadcast 方法
def broadcast[T: ClassTag](value: T): Broadcast[T] = {
assertNotStopped()
require(!classOf[RDD[_]].isAssignableFrom(classTag[T].runtimeClass),
"Can not directly broadcast RDDs; instead, call collect() and broadcast the result.")
val bc = env.broadcastManager.newBroadcast[T](value, isLocal)
val callSite = getCallSite
logInfo("Created broadcast " + bc.id + " from " + callSite.shortForm)
cleaner.foreach(_.registerBroadcastForCleanup(bc))
bc
}
Broadcast 方法用于广播给定的对象,实质是调用了 SparkEnv 的子组件 BroadcastManager 的 newBroadcast 方法生成了广播对象
runJob 方法
SparkContext 提供了多个重载的 runJob 方法,这些方法最终都将调用如下的 runJob 方法
def runJob[T, U: ClassTag](
rdd: RDD[T],
func: (TaskContext, Iterator[T]) => U,
partitions: Seq[Int],
resultHandler: (Int, U) => Unit): Unit = {
if (stopped.get()) {
throw new IllegalStateException("SparkContext has been shutdown")
}
val callSite = getCallSite
// 清理闭包
val cleanedFunc = clean(func)
logInfo("Starting job: " + callSite.shortForm)
if (conf.getBoolean("spark.logLineage", false)) {
logInfo("RDD's recursive dependencies:\n" + rdd.toDebugString)
}
// 调用 DAGScheduler 的 runJob 方法将已经构建好 DAG 的 RDD 提交给 DAGScheduler 进行调度
dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, resultHandler, localProperties.get)
progressBar.foreach(_.finishAll())
// 保存检查点
rdd.doCheckpoint()
}
setCheckpointDir
SparkContext 的setCheckpointDir方法 用于给作业中的RDD指定检查点保存的目录,指定检查点目录是启用检查点机制的前提
def setCheckpointDir(directory: String) {
if (!isLocal && Utils.nonLocalPaths(directory).isEmpty) {
logWarning("Spark is not running in local mode, therefore the checkpoint directory " +
s"must not be on the local filesystem. Directory '$directory' " +
"appears to be on the local filesystem.")
}
checkpointDir = Option(directory).map { dir =>
val path = new Path(dir, UUID.randomUUID().toString)
val fs = path.getFileSystem(hadoopConfiguration)
fs.mkdirs(path)
fs.getFileStatus(path).getPath.toString
}
}
SparkContext 的伴生对象简析
属性
- VALID_LOG_LEVELS : 有效的日志级别,VALID_LOG_LEVELS 包括 ALL、DEBUG、ERROR、FATAL、INFO、OFF、TRACE、WARN等
- SPARK_CONTEXT_CONSTRUCTOR_LOCK : 对 SparkContext 进行构造时使用的锁,以此保证构造 SparkContext 的过程是线程安全的
- activeContext : 类型为 AtomicReference[SparkContext] ,用于保存激活的 SparkContext
- contextBeingConstructed : 标记当前正在构造 SparkContext
方法
- setActiveContext :用于将 SparkContext 作为激活的 SpaarkContext,保存到 activeContext 中
private[spark] def setActiveContext(
sc: SparkContext,
allowMultipleContexts: Boolean): Unit = {
SPARK_CONTEXT_CONSTRUCTOR_LOCK.synchronized {
assertNoOtherContextIsRunning(sc, allowMultipleContexts)
contextBeingConstructed = None
activeContext.set(sc)
}
}
- getOrCreate(config: SparkConf) : 如果没有激活的 SparkContext,则构造 SparkContext,并调用 setActiveContext 方法保存到activeContext 中,最后返回激活的 SparkContext , SparkContext 伴生对象中还提供了无参的 getOrCreate 方法,其实现与getOrCreate(config: SparkConf) 方法类似
def getOrCreate(config: SparkConf): SparkContext = {
// Synchronize to ensure that multiple create requests don't trigger an exception
// from assertNoOtherContextIsRunning within setActiveContext
SPARK_CONTEXT_CONSTRUCTOR_LOCK.synchronized {
if (activeContext.get() == null) {
setActiveContext(new SparkContext(config), allowMultipleContexts = false)
} else {
if (config.getAll.nonEmpty) {
logWarning("Using an existing SparkContext; some configuration may not take effect.")
}
}
activeContext.get()
}
}