博主博客地址:https://bryce-loski.github.io/
Spark任务运行流程(基于yarn集群模式)源码分析(1)
写在前面的话
本文通过通俗易懂的方式,将以spark的yarn集群模式,通过源码层面去分析spark的任务调度流程。因为源码量巨大,所以只分析调度任务时所经历的主要流程。
注:阅读前需要具备一点点scala基础
1.1 Spark核心组件
- Driver
Spark的驱动器节点,用于执行spark的main方法,负责实际代码的执行工作。
主要责任有:
- 将用户程序转化为作业(job)
- 在Executor之间调度任务(Task)
- 跟踪Executor的执行情况
- 通过UI展示查询运行情况
- Executor
Spark Executor节点是负责在Spark作业中运行具体的任务,任务彼此之间互相独立。Spark应用启动时,Executor加点被同时启动,并且始终伴随着整个Spark用用的生命周期。
Executor自动启动HA机制,当某个节点运行故障,Spark应用会自动将出错节点上的任务调度到其他的节点上继续执行。
Executor的核心功能:
- 负责运行组成Spark的应用任务,并将结果返回Driver端
- 它们通过自身的块管理器(Block Manager)为用户程序中要求缓存的 RDD 提供内存式存储。RDD是直接缓存在Executor进程内的,因此任务可以在运行时充分利用缓存数据加速运算。
- Spark通用运行流程概述
上图体现spark基本的运行流程:
- 在任务提交以后,Saprk会先启动Driver程序
- 随后Driver分出两条线,
2.1 一条从集群管理器去注册应用程序,因为有这条线,Spark才能与yarn结合。实现可插拔。
2.2 另一条执行main函数。 Spark的查询为懒执行,当执行到Action算子时开始反向推算,根据宽依赖进行Stage的划分,随后每一个Stage对于一个Taskset。 - Task分发到执行的Executor去执行。
Spark在yarn的提交流程
准备工作
准备两个依赖
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-yarn_2.12</artifactId>
<version>2.4.5</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.12</artifactId>
<version>2.4.5</version>
</dependency>
2.1 指令执行入口
在yarn运行模式下。spark会通过一个指令,向yarn提交任务。本文以官方案例,spark PI为例:
bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master yarn \
./examples/jars/spark-examples_2.12-2.4.5.jar \
10
从指令中可以看到,这个指令式执行spark-submit,传入参数。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ESvzAT0e-1592550302362)(https://s1.ax1x.com/2020/06/19/NKFNqO.png)]
用文本文件打开这个文件,里面只有不到30行代码。主要带代码在最后一句。这个指令主要执行
文件bin目录下的spark-class的org.apache.spark.deploy.SparkSubmit类
exec "${SPARK_HOME}"/bin/spark-class org.apache.spark.deploy.SparkSubmit "$@"
打开spark-class可以找到
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8BfAnhZ9-1592550302366)(https://s1.ax1x.com/2020/06/19/NKFcsf.png)]
从最后一行开始。这个文件执行了
- CMD参数。
- CMD参数用过while循环,将build_command封装进CMD
- build_command配置了JVM的内存,前面的Runner封装了java的配置。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DZJgE3g2-1592550302370)(https://s1.ax1x.com/2020/06/19/NKFHyV.png)]
所以通过拼接,最后得出的指令为:
bin/java -Xmx128m -cp org.apache.spark.deploy.SparkSubmit
这条指令通过java的指定去启动一个进程,这个进程为spark-submit。
2.2 SparkSubmit
通过上面这条指令。可以得出,sparkSubmit这个类中一定有一个main方法作为方法的入口,去执行。所以在idea中去寻找这个类,的伴生类对象。
override def main(args: Array[String]): Unit = {
val submit = new SparkSubmit() {
self =>
override protected def parseArguments(args: Array[String]): SparkSubmitArguments = {
new SparkSubmitArguments(args) {
override protected def logInfo(msg: => String): Unit = self.logInfo(msg)
override protected def logWarning(msg: => String): Unit = self.logWarning(msg)
}
}
override protected def logInfo(msg: => String): Unit = printMessage(msg)
override protected def logWarning(msg: => String): Unit = printMessage(s"Warning: $msg")
override def doSubmit(args: Array[String]): Unit = {
try {
super.doSubmit(args)
} catch {
case e: SparkUserAppException =>
exitFn(e.exitCode)
}
}
}
submit.doSubmit(args)
}
进入main方法。可以看到main方法中前面所有的方法都是在声明类信息。只有最后一行去调用了SparkSubmit的doSubmit方法,并将args作为参数传入。
目前的结构是:
SparkSubmit
-- main
// 执行提交
// args表示应用程序的命令行参数
-- submit.doSubmit(args)
2.2.1 doSubmit
点击进入doSubmit
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5qvyc5St-1592550302373)(https://s1.ax1x.com/2020/06/19/NKkPOK.png)]
代码量很少,关键的代码只有
val appArgs = parseArguments(args)
从字面意思,这句话对传入的args进行参数的解析。
2.2.1.1 parseArguments
进入 parseArguments -> SparkSubmitArguments
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m6bOHnJn-1592550302376)(https://s1.ax1x.com/2020/06/19/NKkMOf.png)]
可以看到很熟悉的东西。这个类将各种参数进行封装,封装成自己的类。
在下文可以看到处理的过程。但是这个并不是本文的主要内容所以略过。
在本类中可以找到一个handle的方法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jcbNCNAa-1592550302378)(https://s1.ax1x.com/2020/06/19/NKkUlq.png)]
这个方法通过模式匹配对命令行的参数进行了封装。如:
–class org.apache.spark.examples.SparkPi
–master yarn
将 org.apache.spark.examples.SparkPi => master
将yarn -> master
所以目前的结构是:
SparkSubmit
-- main
// 执行提交
// args表示应用程序的命令行参数
-- submit.doSubmit(args)
// 解析命令行参数
// --master : yarn => master
// --class : xxxxx.WordCount => mainClass
-- parseArguments
sparkSubmit执行main -> dosubmit -> parseArguments ->** new SparkSubmitArguments** -> 通过handle方法,将命令行的各个参数传入到了spark程序中。
2.2.1.2 action
返回doSubmit层,往下看可以看到一个action方法:
appArgs.action match {
case SparkSubmitAction.SUBMIT => submit(appArgs, uninitLog)
case SparkSubmitAction.KILL => kill(appArgs)
case SparkSubmitAction.REQUEST_STATUS => requestStatus(appArgs)
case SparkSubmitAction.PRINT_VERSION => printVersion()
}
通过代码可以得知appargs通过模式匹配执行了action,由于没有case _的选项,所以action一定在某个地方被赋值了。
点击进入action所在的SparkSubmitArguments,可以找到这样一行代码:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xjJoImn4-1592550302379)(https://s1.ax1x.com/2020/06/19/NKkB0U.png)]
通过一定的scala基础,可以分析出action在这里被赋值上了SUBMIT
最后action走的是
case SparkSubmitAction.SUBMIT => submit(appArgs, uninitLog)
这一行代码。
2.2.2 submit(appArgs, uninitLog)
进入action的submit函数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BbLnO6ib-1592550302380)(https://s1.ax1x.com/2020/06/19/NKk40O.png)]
里面有一个doRunMain函数,由于没有调用这个函数,所以这个函数不被执行,先跳过,看下面的if-else方法
if (args.isStandaloneCluster && args.useRest) {
try {
logInfo("Running Spark using the REST application submission protocol.")
doRunMain()
} catch {
// Fail over to use the legacy submission gateway
case e: SubmitRestConnectionException =>
logWarning(s"Master endpoint ${args.master} was not a REST server. " +
"Falling back to legacy submission gateway instead.")
args.useRest = false
submit(args, false)
}
// In all other modes, just run the main class as prepared
} else {
doRunMain()
}
由于本文是基于yarn部署环境。所以直接查看else中的doRunMain方法
2.2.2.1 doRunMain
doRunMain方法中,if函数判断是否有代理用户,当前执行命令并没有设定,所以执行else中的runMain方法
2.2.3 runMain
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YgKuMfhr-1592550302381)(https://s1.ax1x.com/2020/06/19/NKkb9A.png)]
这个函数中开始准备做提交的内容。
val (childArgs, childClasspath, sparkConf, childMainClass) = prepareSubmitEnvironment(args)
这个函数关键信息也在第一句,准备提交环境。
加下来:
//设定类加载器。
Thread.currentThread.setContextClassLoader(loader)
//加载和获取指定名称的类信息
mainClass = Utils.classForName(childMainClass)
//动态(反射)创建对象
mainClass.newInstance().asInstanceOf[SparkApplication]
//同时在之后的代码调用start方法
app.start(childArgs.toArray, sparkConf)
小结回顾
进入start方法,会发现start是一个抽象类。
所以,代码一定在之前就已经被实现。
回顾app方法,是从mainClass.newInstance().asInstanceOf[SparkApplication]这个方法过来,
mainClass是从mainClass = Utils.classForName(childMainClass)得到的类信息
childMainClass是val (childArgs, childClasspath, sparkConf, childMainClass) = prepareSubmitEnvironment(args)准备提交环境的时候返回的结果。
所以以上的代码就连成了串,关键在于prepareSubmitEnvironment
2.2.4 prepareSubmitEnvironment
- 进入 prepareSubmitEnvironment方法
这是一个代码量非常大的函数 - 直接看末尾处,**748行处(childArgs, childClasspath, sparkConf, childMainClass)**这是函数的返回结果,从返回结果往上推,能找到赋值的地方。
- 查找childMainClass跟着提示往上走,会找到这样一个代码段
childMainClass = YARN_CLUSTER_SUBMIT_CLASS
这里是Yarn的cluster模式,进入YARN_CLUSTER_SUBMIT_CLASS可以看到完整的类对象为org.apache.spark.deploy.yarn.YarnClusterApplication
- 继续查找会看到client模式的代码
childMainClass = args.mainClass
而mainClass在前面已经提到为–class的值
所以目前的结构是:
SparkSubmit
-- main
// 执行提交
// args表示应用程序的命令行参数
-- submit.doSubmit(args)
// 解析命令行参数
// --master : yarn => master
// --class : xxxxx.WordCount => mainClass
-- parseArguments
//
-- submit
// 执行主程序
-- doRunMain
// 执行主程序
-- runMain
// childArgs
// childClasspath
// sparkConf
// childMainClass
// 准备提交的环境
-- prepareSubmitEnvironment(args)
// client => xxxxx.WordCount
// isYarnCluster => org.apache.spark.deploy.yarn.YarnClusterApplication
-- (childArgs, childClasspath, sparkConf, childMainClass)
// 设定类加载器
-- Thread.currentThread.setContextClassLoader(loader)
// 加载和获取指定名称的类信息
-- mainClass = Utils.classForName(childMainClass)
// 动态(反射)创建对象
-- app = mainClass.newInstance().asInstanceOf[SparkApplication]
-- app.start
到此SparkSubmit就已经结束