环境准备(Yarn集群)
源码入手入口就是submit提交,当我们去提交的时候,整个程序就开始运行了。
当我们提交时
bin/spark-submit --class org.apache.spark.examples.SparkPi --master yarn ./examples/jars/spark-examples_2.12-3.0.0.jar 10
它底层实际调用的是bin目录中的spark-submit.cmd的脚本文件,让我们打开看看
spark-submit.cmd它内部调用的是spark-submit2.cmd脚本,打开spark-submit2.cmd脚本
同样它实际调用的是spark-class2.cmd脚本,打开spark-class2.cmd脚本
打开后我们发现了大量的脚本内容,在最后一行有个指令SPARK_CMD是我们真正要执行的指令,我们将他echo打印出来看一看
echo %SPARK_CMD%
终端执行一下submit提交程序
执行结果(java后面有个cp命令不重要这里过滤了)
这里是用java指令执行某一个类相当于打开一个java虚拟机(JVM),实际上等同于启动一个process进程,jps能查看的到
进入submit.doSubmit(args)方法
parseArguments(args)是解析参数方法,把传入的命令行解析,解析完成后应用参数的action动作方法去匹配某个分支(SUBMIT,KILL,REQUEST_STATUS,PRINT_VERSION),我们进入paserArguments(args)方法看看
这里他new了一个SparkSubmitArguments对象,进入看看
因为上一层new了他的实例对象,所以它的类会初始化,这个类里面有大量的属性
下面有一个方法在主体内容初始化的时候进行参数解析,会把我们传入的命令行参数转换解析,进入它看看
进入handle方法看看
name和value传进来和下列枚举类进行匹配,这里的MASTER就是我们传入参数的–master
我们进去看看
对应关系图:
只要设置对应的参数和参数值就可以通过以上方法进行匹配
返回到SparkSubmitArguments方法找到对action的处理
一开始将action赋值为null
这里对action进行操作
所以默认action就是SUBMIT,回到SparkSubmit类,匹配成功
这时候就可以提交了,进入submit提交方法
先声明了一个doRunMain()方法,先不看,往下走
我们用的是yarn集群,继续往下走到else,这里调用了一个最上面声明的doRunMain()方法,这时候我们在点到这个方法
进入runMain(args,uninitLog)方法
记着这个准备提交环境的方法,往下走
总结一下上面两个图:val app: SparkApplication = if (classOf[SparkApplication].isAssignableFrom(mainClass))这里会通过上面mainClass = Utils.classForName(childMainClass)反射获取的childMainClass类信息进行判断是否继承自SparkApplication这个类,如果是它会反射创建mainClass.getConstructor().newInstance().asInstanceOf[SparkApplication]这个类对象,如果不是它会new一个JavaMainApplication(mainClass)对象。所以我们关心的是这个childMainClass类型是什么,它从哪里来的?
在上面提到的准备提交环境方法val (childArgs, childClasspath, sparkConf, childMainClass) = prepareSubmitEnvironment(args),它返回的第四个值就是childMainClass,我们可以进去看看具体实现
进入到prepareSubmitEnvironment(args)方法,一开始childMainClass被赋值为空字符串,我们继续往下走
我们走的不是Standalone集群,只能进入Yarn集群,给了它childMainClass = YARN_CLUSTER_SUBMIT_CLASS这个值,我们进去查看一下这个值具体是什么
private[deploy] val YARN_CLUSTER_SUBMIT_CLASS =
"org.apache.spark.deploy.yarn.YarnClusterApplication"
我们继续看org.apache.spark.deploy.yarn.YarnClusterApplication的源码,一般情况下如果未导入相应的spark-yarn源码包无法看到这里,相关依赖如下:
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-yarn_2.12</artifactId>
<version>3.0.0</version>
</dependency>
我们点进去Client对象需要传入的对象ClientArgument(args)看看
回到上一级点进去Client类(文字描述跟不上的话直接双击shift搜上面类名进入)
这里有个yarnClient客户端,我们点进去createYarnClient
继续进入他的实现类
找到ResourceManager后返回到
new Client(new ClientArguments(args), conf, null).run()
进入他的run()方法
进入submitApplication()方法查看
createApplicationSubmissionContext方法内就是一些配置参数,也没有什么重要的部分,所以接着往上走进入createContainerLaunchContext方法(container在yarn中是起到一个解藕的作用),大概看一下里面有启动环境、本地资源、java虚拟机的一些配置
这里提交就是封装一些指令提交给ResourceManager,ResourceManager得到这个指令后,它会在某个节点(NodeManager)去运行这个指令,启动ApplicationMaster(启动的方式是/bin/java + 一些东西 + org.apache.spark.deploy.yarn.ApplicationMaster)
目前而言,从submit到启动ApplicationMaster的图是这样的:
查看org.apache.spark.deploy.yarn.ApplicationMaster源码,这个类能被/bin/java指令执行变成一个进程那他会有一个main方法,找到他的main()方法
有个ApplicationMasterArguments,它会对main方法传入的命令行参数进行封装,我们进入该方法查看
返回到上一层
往下看,找到ApplicationMaster方法进去查看
进入YarnRMClient类
有个AMRMClient(ApplicationMaster和ResourceManager之间连接的客户端),记住它就行,往后退到master实例对象
// 创建 ApplicationMaster 对象
master = new ApplicationMaster(amArgs, sparkConf, yarnConf)
继续往下走
点进去执行AM对象的run()方法
往下走看到了一个判断是否集群模式的分支:如果是集群模式则执行runDriver()的方法,如果不是集群模式,也就是客户端模式,则启动ExecutorLauncher()方法,进入runDriver()方法查看
等待期间执行的是startUserApplication()方法,那么这个方法在干什么呢,我们进去这个方法查看
进去看看userClass是什么类
后退到
val mainMethod = userClassLoader.loadClass(args.userClass) .getMethod("main", classOf[Array[String]])
getMethod方法要从指定的类中找到main方法,但是并没有什么用途,接着往下走
这一部分总结就是Application根据我们传入的参数来启动driver线程,并初始化SparkContext
恰恰是因为这里初始化SparkContext后,等待结果返回成功,我们整个操作才可以继续往下走,所以我们每个项目部分(例如wordcount案例)
我们在配置好spark上下文初始化完成,我们整个案例才可以开始,继续
进入分配器创建方法查看
进入处理方法handleAllocatedContainers查看
这个方法把它粘贴下来
def handleAllocatedContainers(allocatedContainers: Seq[Container]): Unit = {
val containersToUse = new ArrayBuffer[Container](allocatedContainers.size)
// Match incoming requests by host
// TODO 优先分配到同一台主机
val remainingAfterHostMatches = new ArrayBuffer[Container]
for (allocatedContainer <- allocatedContainers) {
matchContainerToRequest(allocatedContainer, allocatedContainer.getNodeId.getHost,
containersToUse, remainingAfterHostMatches)
}
// Match remaining by rack. Because YARN's RackResolver swallows thread interrupts
// (see SPARK-27094), which can cause this code to miss interrupts from the AM, use
// a separate thread to perform the operation.
// TODO 分配到同一个机架
val remainingAfterRackMatches = new ArrayBuffer[Container]
if (remainingAfterHostMatches.nonEmpty) {
var exception: Option[Throwable] = None
val thread = new Thread("spark-rack-resolver") {
override def run(): Unit = {
try {
for (allocatedContainer <- remainingAfterHostMatches) {
val rack = resolver.resolve(allocatedContainer.getNodeId.getHost)
matchContainerToRequest(allocatedContainer, rack, containersToUse,
remainingAfterRackMatches)
}
} catch {
case e: Throwable =>
exception = Some(e)
}
}
}
thread.setDaemon(true)
thread.start()
try {
thread.join()
} catch {
case e: InterruptedException =>
thread.interrupt()
throw e
}
if (exception.isDefined) {
throw exception.get
}
}
// Assign remaining that are neither node-local nor rack-local
// TODO 不是同一主机,也不是同一机架
val remainingAfterOffRackMatches = new ArrayBuffer[Container]
for (allocatedContainer <- remainingAfterRackMatches) {
matchContainerToRequest(allocatedContainer, ANY_HOST, containersToUse,
remainingAfterOffRackMatches)
}
if (remainingAfterOffRackMatches.nonEmpty) {
logDebug(s"Releasing ${remainingAfterOffRackMatches.size} unneeded containers that were " +
s"allocated to us")
for (container <- remainingAfterOffRackMatches) {
internalReleaseContainer(container)
}
}
// TODO 运行匹配后的资源
runAllocatedContainers(containersToUse)
logInfo("Received %d containers from YARN, launching executors on %d of them."
.format(allocatedContainers.size, containersToUse.size))
}
这个方法内会对节点进行分类(根据主机host和支架rack)
然后决定资源的分配方式和优先顺序(后面详细讲解)
准备工作完成就可以开始运行已分配容器,进入这个方法查看
每个可使用的容器挨个遍历
我们查看一下Executor的run方法
继续查看一下另外一个nm节点启动的container
组件通信
应用程序的执行
Shuffle
内存的管理
未完成,加班ing