前言
本文接续上篇接着从源码的角度来分析 Spark 中提交任务到执行计算的流程,推荐如果没有阅读上篇可以从上篇开始阅读不然会觉得本文有些云里雾里。
Application Master
上篇的最后我们说了再我们提交任务后 Spark 会启动一个yarn client
并向 RM 发送一条类似于 command = bin/java org.apache.spark.deploy.yarn.ApplicationMaster
的指令从而启动 Application Master
, 此时在启动了 AM 之后接下来就是在 yarn 上面的操作了,当然我们通过指令的方式启动了 AM 那么它肯定是一个进程所以我们就可以从其 main()
方法入手来看看其做了那些事情,为了方便阅读本文的代码截取会比上一篇更简洁(上篇截的太多了)。
在 main()
方法中首先可以看到以下语句:
val amArgs = new ApplicationMasterArguments(args)
看过上篇的话我们就大概能猜到其中封装了我们通过命令行传入的各种参数比如 jar, class
等,而实际上里面也确实是在封装这些内容具体就不展开了,依照上篇的套路在封装玩参数之后当然是要创建某个对象并允许啦,这里当然也是这样:
master = new ApplicationMaster(amArgs, new YarnRMClient)
System.exit(master.run())
在这里我们就真正的创建了 ApplicationMaster
然后调用了 run()
方法,进到 run()
方法首先会看到很多的 System.setProperty
设置了许多属性比如部署模式、UI 端口等等,这些对本篇内容而言不重要,但是往下看就会发现令我们感兴趣的语句:
if (isClusterMode) {
runDriver(securityMgr)
} else {
runExecutorLauncher(securityMgr)
}
在我们的集群部署模式下会执行 runDriver()
操作,还记得 Spark 自身的架构吗, driver 负责分发任务与调度,而 executor 负责进行计算,而在之前我们都没发现那里启动了 driver
而 driver
的职责与 AM 类似看来这里就是启动 driver
的地方了,进到方法后我们马上就会看到这样一句话 userClassThread = startUserApplication()
,我们接着点进去就会发现 driver
的所在:
val mainMethod = userClassLoader.loadClass(args.userClass)
.getMethod("main", classOf[Array[String]])
val userThread = new Thread {
override def run() {
try {
mainMethod.invoke(null, userArgs.toArray)
finish(FinalApplicationStatus.SUCCEEDED, ApplicationMaster.EXIT_SUCCESS)
logDebug("Done running users class")
} catch {
case e: InvocationTargetException =>
e.getCause match {
case _: InterruptedException =>
// Reporter thread can interrupt to stop user class
case SparkUserAppException(exitCode) =>
val msg = s"User application exited with status $exitCode"
logError(msg)
finish(FinalApplicationStatus.FAILED, exitCode, msg)
case cause: Throwable =>
logError("User class threw exception: " + cause, cause)
finish(FinalApplicationStatus.FAILED,
ApplicationMaster.EXIT_EXCEPTION_USER_CLASS,
"User class threw exception: " + cause)
}
sparkContextPromise.tryFailure(e.getCause())
} finally {
// Notify the thread waiting for the SparkContext, in case the application did not
// instantiate one. This will do nothing when the user code instantiates a SparkContext
// (with the correct master), or when the user code throws an exception (due to the
// tryFailure above).
sparkContextPromise.trySuccess(null)
}
}
}
userThread.setContextClassLoader(userClassLoader)
// Driver 作为一个执行我们 user App 的线程启动,或者说执行我们 user APP 的线程被取名为 Driver
userThread.setName("Driver")
userThread.start()
userThread
在这里我们就可以看到很熟悉的代码,首先通过反射的方式获取了我们传入的 user App
的主方法然后在线程中对其进行调用,而这段代码最值得注意的是这个执行我们主方法的线程被命名为了 driver
, 至此我们知道了至此我们知道了在 yarn
模式下 driver
是 AM 启动的一个线程而其主要作用就是执行并调度我们的应用。搞清楚了 driver
让我们回到 runDriver()
方法其中还有一个我们感兴趣的内容
registerAM(sc.getConf, rpcEnv, driverRef, sc.ui.map(_.appUIAddress).getOrElse(""), securityMgr)
从方法名就可以看到这里 AM 在向 RM 注册自己,而注册自己的目的就是为了向 RM 申请资源,因此进入到方法后我们可以看到其确实是在这么做:
allocator = client.register(driverUrl,
driverRef,
yarnConf,
_sparkConf,
uiAddress,
historyAddress,
securityMgr,
localResources)
allocator.allocateResources()
这两段代码就很好的对应了我们 Spark 提交任务流程图中的第三步,而在申请完资源后就要创建实际的运算对象也就是我们的 container
了,而在 allocateResources()
这个方法中我们就能找到 handleAllocatedContainers()
继续点到这个方法中我们就能看到启动 contain
的语句 :runAllocatedContainers(containersToUse)
我们再点到这个方法中就能看到启动 executor
的语句了
new ExecutorRunnable(
Some(container),
conf,
sparkConf,
driverUrl,
executorId,
executorHostname,
executorMemory,
executorCores,
appAttemptId.getApplicationId.toString,
securityMgr,
localResources
).run()
def run(): Unit = {
logDebug("Starting Executor Container")
nmClient = NMClient.createNMClient()
nmClient.init(conf)
nmClient.start()
// 启动容器
startContainer()
}
可以看到在这里我们再次启动了一个线程,而在线程中我们可以看到 yarn 中非常熟悉的 NM,而我们都知道 NM 是实际工作的节点其与我们 Spark 中的 executor 相对应,在 run()
方法的最后我们启动了一个容器,当然我们点到这个方法中看看其做了什么,根据之前的经验我们通过 yarn client 启动了一个 AM 而其启动方式是通过 command 命令的,那么我们在这里通过 nm client 启动一个 container 是不是也类似呢,果不其然我们进入方法后马上就可以看到这句话 val commands = prepareCommand()
这了也封装了一条 command,具体的内容就不展开了基本的形式与之前相同而特别需要注意的是其启动的主类org.apache.spark.executor.CoarseGrainedExecutorBackend
,而这也意味着至此我们在 AM 中的操作基本完成接下来该去这个和 executor 相关的类中看看了。
Executor
进入到我们的 CoarseGrainedExecutorBackend
其作为一个进程被启动那么第一件事情就是去看看它的 main()
方法,在经过一堆参数的模式匹配后我们就可以看到 run(driverUrl, executorId, hostname, cores, appId, workerUrl, userClassPath)
这个 run 方法,其从其传入的这些参数我们也可以大概猜到它是在启动我们的 executor
了,但熟悉我们的流程图的话就会发现不对劲的地方我们还没有向 AM 进行注册呢,那我们就得找一找了最终我们在 onStart()
方法中找到了注册相关语句:
override def onStart() {
logInfo("Connecting to driver: " + driverUrl)
rpcEnv.asyncSetupEndpointRefByURI(driverUrl).flatMap { ref =>
// This is a very fast action so we can use "ThreadUtils.sameThread"
driver = Some(ref)
// executor 向 driver 注册自己
ref.ask[Boolean](RegisterExecutor(executorId, self, hostname, cores, extractLogUrls))
}(ThreadUtils.sameThread).onComplete {
// This is a very fast action so we can use "ThreadUtils.sameThread"
case Success(msg) =>
// Always receive `true`. Just ignore it
case Failure(e) =>
exitExecutor(1, s"Cannot register with driver: $driverUrl", e, notifyDriver = false)
}
这里便是在启动进程时就向 dirver
进行注册,而 dirver
在接受到注册后会进行应答,这个应答就会被另一个 receiver()
方法接受:
override def receive: PartialFunction[Any, Unit] = {
case RegisteredExecutor =>
logInfo("Successfully registered with driver")
try {
// 我们的 executor 作为一个对象被启动
executor = new Executor(executorId, hostname, env, userClassPath, isLocal = false)
} catch {
case NonFatal(e) =>
exitExecutor(1, "Unable to create executor due to " + e.getMessage, e)
}
.....
}
最终,在 receiver()
方法中我们的 executor
作为一个对象被启动,至此我们流程图上所有的步骤从我们在 Spark 上submit 一个 job 到达我们的 executor
为止都串联起来了。
结语
本文上下两篇通过源码的角度详细分析了 Spark 在 yarn 集群模式下的任务部署流程,笔者是第一次写这样的文章,可能在阅读体验上不是很好,但是如果可以跟着思路一路跟下来相信不仅能更好的理解 Spark 也能对阅读源码的能力起到提升作用,谢谢你的阅读,完。