先验知识
接之前文章 Spark源码分析之任务提交流程 介绍了Client提交Spark任务的源码分析过程。本文继续分析ApplicationMaster的启动流程(源码Hadoop2.7.1),首先给出Client的提交的一些先决条件如下:
提交命令:
spark-submit --master yarn \
--deploy-mode cluster \
--class org.apache.spark.examples.SparkPi \
/usr/local/spark-2.4.3-bin-hadoop2.7/examples/jars/spark-examples_2.11-2.4.3.jar 100000
由之前文章可知,最终是由SparkSubmit类向Yarn集群提交任务,首先会把任务依赖的文件上传hdfs,然后生成Yarn提交上下文参数,通过RPC方式向Yarn的Resource Manager提交任务,其中提交上下文参数如下:
提交给HDFS的文件如下图
Yarn启动AM流程
SparkSubmit最终会调用Yarn Client通过RPC方式提交给Yarn的RM,提交给Yarn的RM后会首先申请一个Container(其实质就是ApplicationMaster),并与之NodeManager建立联系,NodeManager先进行资源本地化,然后在工作目录下生成并调用 default_container_executor.sh -> default_container_executor_session.sh -> launch_container.sh
启动JVM进程用于执行AM(Yarn集群的调度原理见 Yarn源码分析之集群启动流程 、Yarn源码分析之事件模型 和 Yarn源码分析之状态机机制 )。其中NodeManager进行资源本地化后的磁盘目录如下图(假设yarn配置的参数 yarn.nodemanager.local-dirs=/var/lib/hadoop-yarn/cache/yarn/
):
于测试集群是单节点的,所以上图包含三个容器container_1587104773637_0002_01_000001
、container_1587104773637_0002_01_000002
和container_1587104773637_0002_01_000003
,第一个容器用于运行Driver容器,后两个容器用于运行Executor容器。NodeManager的工作目录为 cd /tmp/hadoop-root/nm-local-dir/usercache/root/appcache/application_1587104773637_0002/container_1587104773637_0002_01_000001
。
重点关注两个文件launch_container.sh
和__spark_conf__.properties
:
launch_container.sh
文件的主要作用为设置AM启动环境变量和NM通过sh启动JVM运行AM;__spark_conf__.properties
则是AM启动Driver和Executor的配置参数。
上图给出文件供参考(由于jar文件过多过大,删除了解压到nm-local-dir/usercache/root/filecache/13/__spark_libs__3004195479466524435.zip目录中的所有jar包)。
单独列出launch_container.sh
供参考,可以看出AM的入口类 org.apache.spark.deploy.yarn.ApplicationMaster
。
AM启动Driver流程
由上节可知,NodeManager启动AM的入口类是org.apache.spark.deploy.yarn.ApplicationMaster,其启动过程会通过伴生类的main
作为入口,代码分析直接见注释,如下图:
然后代码继续执行 ApplicationMaster.run() -> ApplicationMaster.runDriver()
,最终在runDiver()
做Driver的初始化工作,代码分析见注释如下:
如上图,我们也重点分析两部分:AM如何启动Driver程序和AM主线程如何获得子线程中SparkContext的初始化。Driver程序的启动是在userClassThread = startUserApplication()
函数完成的,如下图,首先获得提交参数中定义的--class
,反射并实例化运行初始化SparkContext(即Driver),如下图:
然后我们回到runDriver()
看AM主线程如何获得子线程中SparkContext的初始化,答案就是采用了Promise机制,下面截取关键处代码:
private[spark] class ApplicationMaster(
...
// In cluster mode, used to tell the AM when the user's SparkContext has been initialized.
private val sparkContextPromise = Promise[SparkContext]()
private def sparkContextInitialized(sc: SparkContext) = {
sparkContextPromise.synchronized {
// Notify runDriver function that SparkContext is available
sparkContextPromise.success(sc)
// Pause the user class thread in order to make proper initialization in runDriver function.
sparkContextPromise.wait()
}
}
private def startUserApplication(): Thread = {
logInfo("Starting the user application in a separate Thread")
...
val mainMethod = userClassLoader.loadClass(args.userClass)
.getMethod("main", classOf[Array[String]])
val userThread = new Thread {
override def run(): Unit = {
try {
if (!Modifier.isStatic(mainMethod.getModifiers)) {
...
} else {
mainMethod.invoke(null, userArgs.toArray)
...
}
} catch {
...
sparkContextPromise.tryFailure(e.getCause())
} finally {
sparkContextPromise.trySuccess(null)
}
}
}
userThread.setContextClassLoader(userClassLoader)
userThread.setName("Driver")
userThread.start()
userThread
}
private def runDriver(): Unit = {
//开辟线程,加载用户定义--class函数,即Driver
userClassThread = startUserApplication()
...
try {
//等待用户定义Driver完成SparkContext的初始化完成
val sc = ThreadUtils.awaitResult(sparkContextPromise.future,
Duration(totalWaitTime, TimeUnit.MILLISECONDS))
...
}
如上代码,关键就是val sc = ThreadUtils.awaitResult(sparkContextPromise.future, Duration(totalWaitTime, TimeUnit.MILLISECONDS))
,此函数会阻塞在超时时间内等待sparkContextPromise的Future对象返回SparkContext实例。其原理是先在ApplicationMaster类中定义了变量private val sparkContextPromise = Promise[SparkContext]()
,这样在userClassThread = startUserApplication()
中开辟的子线程中在SparkContext的初始化后会调用hook,进而通知主线程完成SparkContext的初始化并赋值返回,过程如下图:
AM申请Executors流程
在runDriver()
函数中完成sparkContext的初始化后,紧接着就将AM注册到RM,并根据driver的host、port和rpc server名称YarnSchedulerBackend.ENDPOINT_NAME获取到driver的EndpointRef对象driverRef,用于AM与Driver通信;同时传递driverRef给Executor用于executor与driver之间进行rpc通信,最后通过调用createAllocator(driverRef, userConf, rpcEnv, appAttemptId, distCacheConf)
,向RM申请Container资源,并启动Executors,代码如下:
下面给出打印上下文参数示例:
如果申请分配了多个executor容器,提交上下文参数类似,如果区别是--executor-id
参数不同。
如图可以看出向Yarn申请Executors与申请Driver容器的过程类似,区别在于Executor的入口类为org.apache.spark.executor.CoarseGrainedExecutorBackend
。