在之前我们分析了Spark的内核架构运行机制,其中有一个很重要的组件SparkContext,这里我们就分析SparkContext的相关源码。
我们知道所有的Spark程序,在运行之前都需要创建一个非常重要的组件,那就是SparkContext,它负责读取SparkConf中的相关配置信息,并且初始化一些Spark Application运行时需要用到的两个重要的组件DAGScheduler和TaskScheduler。
下面我们首先看一下整个代码的一个运行流程:
针对流程,我们具体看源码:首先是SparkContext的源码(版本1.6):
// 创建和初始化TaskScheduler 和 DAGScheduler
val (sched, ts) = SparkContext.createTaskScheduler(this, master)
_schedulerBackend = sched
_taskScheduler = ts
_dagScheduler = new DAGScheduler(this)
_heartbeatReceiver.ask[Boolean](TaskSchedulerIsSet)
// start TaskScheduler after taskScheduler sets DAGScheduler reference in DAGScheduler's
// constructor
// TaskScheduler初始化,里面包含了注册Master
_taskScheduler.start()
从上面的源码中可以看出,首先初始化化了两个组件TaskScheduler和DAGScheduler。它调用createTaskScheduler()方法,创建了TaskSchedulerImpl和SparkDepolySchedulerBackend。接着就启动TaskScheduler,调用了它的start()方法。
下面看一下createTaskScheduler()方法:
createTaskScheduler依据不同的提交模式创建对应的TaskScheduler和SparkDepolySchedulerBackend,我们这里看常用的standalone提交模式是怎么创建的:
private def createTaskScheduler(
sc: SparkContext,
master: String): (SchedulerBackend, TaskScheduler) = {
// 省略代码
........
// 这是我们常用的spark提交模式中的standalone方式
case SPARK_REGEX(sparkUrl) =>
// 创建TaskScheduler
val scheduler = new TaskSchedulerImpl(sc)
// 提取Master的URL地址
val masterUrls = sparkUrl.split(",").map("spark://" + _)
// 创建SparkDepolySchedulerBackend
val backend = new SparkDeploySchedulerBackend(scheduler, sc, masterUrls)
// 初始化
scheduler.initialize(backend)
// 将创建的两个组件返回
(backend, scheduler)
// 省略代码
........
}
这里就是初始化两个组件,并将其返回。
接着我们看一下TaskScheduler的initialize()方法:它的主要功能就是创建Task的调度池,有两种调度方式:FIFO和FAIR,默认是FIFO的调度算法,也可以手动设置,只要设置参数:spark.scheduler.mode 为 FAIR即可。这两种算法的不同,这里不做具体分析了。大概就是,FIFO就是比较StageID大小,谁小(优先级高)谁就执行;FAIR就是,先比较task的数量,谁小谁先执行;不行的话,再比较task的权重,谁大谁先执行,假如都一样大,那么就按照名字比较谁大谁执行(感兴趣研究的FairSchedulingAlgorithm和FIFOSchedulingAlgorithm这两个方法)。
// 初始化Task的调度策略,默认是FIFO
def initialize(backend: SchedulerBackend) {
this.backend = backend
// temporarily set rootPool name to empty
rootPool = new Pool("", schedulingMode, 0, 0)
schedulableBuilder = {
schedulingMode match {
case SchedulingMode.FIFO =>
new FIFOSchedulableBuilder(rootPool)
case SchedulingMode.FAIR =>
new FairSchedulableBuilder(rootPool, conf)
}
}
// 创建调度池
schedulableBuilder.buildPools()
}
最后我们看一下taskScheduler的start()方法:
override def start() {
// 调用的是SparkDeploySchedulerBackend的start()方法
backend.start()
if (!isLocal && conf.getBoolean("spark.speculation", false)) {
logInfo("Starting speculative execution thread")
speculationScheduler.scheduleAtFixedRate(new Runnable {
override def run(): Unit = Utils.tryOrStopSparkContext(sc) {
checkSpeculatableTasks()
}
}, SPECULATION_INTERVAL_MS, SPECULATION_INTERVAL_MS, TimeUnit.MILLISECONDS)
}
}
由于它调用的是SparkDeploySchedulerBackend的start方法,下面我们接着看:
override def start() {
// 创建DriverEndPoint,用于与Executor进行通信,用于接收executor的状态信息等
super.start()
// launcher服务
launcherBackend.connect()
// 省略代码,主要是一些参数配置
.............................
// Start executors with a few necessary configs for registering with the scheduler
val sparkJavaOpts = Utils.sparkJavaOpts(conf, SparkConf.isExecutorStartupConf)
val javaOpts = sparkJavaOpts ++ extraJavaOpts
val command = Command("org.apache.spark.executor.CoarseGrainedExecutorBackend",
args, sc.executorEnvs, classPathEntries ++ testingClassPath, libraryPathEntries, javaOpts)
val appUIAddress = sc.ui.map(_.appUIAddress).getOrElse("")
val coresPerExecutor = conf.getOption("spark.executor.cores").map(_.toInt)
// 它代表了当前执行的这个application的一些参数
// 包括 application最大需要多少cpu core,每个worker上需要多少内存
// 对应spark-submit的脚本中的
// --num-executors --executor-memory --executor-cores
val appDesc = new ApplicationDescription(sc.appName, maxCores, sc.executorMemory,
command, appUIAddress, sc.eventLogDir, sc.eventLogCodec, coresPerExecutor)
// 创建了AppClient -- ClientEndPoint
client = new AppClient(sc.env.rpcEnv, masters, appDesc, this, conf)
// 连接Master,注册Application
client.start()
// 注册状态更新
launcherBackend.setState(SparkAppHandle.State.SUBMITTED)
// 等待executor注册返回
waitForRegistration()
// 注册成功
launcherBackend.setState(SparkAppHandle.State.RUNNING)
}
这里就创建了一个重要的组件AppClient,接着调用它的start方法,里面创建了一个ClientEndPoint,调用它的onStart()方法,onStart方法中接着调用了registerWithMaster方法,它里面接着调用了tryRegisterAllMasters()方法。我们看一下这个方法:
def start() {
// Just launch an rpcEndpoint; it will call back into the listener.
// 调用RpcEndPoint,里面创建了ClientEndPoint,并调用它的start方法
endpoint.set(rpcEnv.setupEndpoint("AppClient", new ClientEndpoint(rpcEnv)))
}
override def onStart(): Unit = {
try {
// 调用了这个方法,传入的参数是,尝试次数
registerWithMaster(1)
} catch {
case e: Exception =>
logWarning("Failed to connect to master", e)
markDisconnected()
stop()
}
}
// 尝试注册所有的master
private def tryRegisterAllMasters(): Array[JFuture[_]] = {
// 遍历Master
for (masterAddress <- masterRpcAddresses) yield {
registerMasterThreadPool.submit(new Runnable {
override def run(): Unit = try {
if (registered.get) {
return
}
logInfo("Connecting to master " + masterAddress.toSparkURL + "...")
// 连接Master
val masterRef =
rpcEnv.setupEndpointRef(Master.SYSTEM_NAME, masterAddress, Master.ENDPOINT_NAME)
// 向Master注册Application的信息
masterRef.send(RegisterApplication(appDescription, self))
} catch {
case ie: InterruptedException => // Cancelled
case NonFatal(e) => logWarning(s"Failed to connect to master $masterAddress", e)
}
})
}
}
通过上面代码可以看出,通过tryRegisterAllMasters来注册所有的Master,为什么是所有的Master,下篇文章再进行分组,这里它就向master发送Application的注册信息。
当所有的Executor都反向注册到SparkDeploySchedulerBackend上时,整个SparkContext就初始化完成了,就会接着执行我们的Application作业。
上述源码的分析,就对应着我们一开始的流程图。