概述
在探究了SparkContext与SparkSubmit源码之后,我们大致了解了任务提交与SC的创建,但是有一个疑问就是:
- 文章中所说的,在SparkContext初始化的过程中,会先注册Driver,再去注册Application。
- 而SparkSubmit中也有提到Driver注册。
- 那么,SaprkSubmit中的注册Driver与SC中的注册Driver,到底是怎么样的一个关系呢?
- 感觉写的可能还是些问题的,可以看看Spark2.0.2源码分析——Driver的注册和启动
此外,问题在于有没有Driver注册这个阶段,这取决于集群部署模式:
- 典型的集群模式,参考集群模式以及Client Mode和Cluster Mode的区别
- 例如
standalone模式
,就是spark自己的一个独立集群,因此属于cluster模式,也就是说,在StandaloneAppClient在向Master注册Application时,不会像AppClient一样去先向Master注册Driver,因为它的Driver注册是交给了它自己指定的Worker。- 而像
yarn-cluster
这样的,Application不是交给Driver去管理,而是他们自身有例如ApplicationMaster这样的组件去进行管理。
大致流程
既然提到了这里,那么我们就要综合两个源码来进行分析了。
这里具体的执行流程大致如此,至于内部一些细节有待考察。
Master: Standalone模式中主控节点,负责接收Client提交的作业,管理Worker,并命令Worker启动Driver和Executor。
关于这句话,参考Spark架构与作业执行流程简介
值得注意的是,在Spark任务提交过程中,有两种Client:
一种是ClientApp
,是用于启动、注册、终止Driver
一种是AppClient
,是用于启动、注册、管理Application的。例如StandaloneAppClient。
之前一直以为StandaloneAppClient即会去注册Driver,又会去注册Application,这是错误的。
那么开始分析:
先看SparkSubmit.scala: :创建、注册ClientEndpoint,让ClientEndpoint获取用户程序并运行
创建、注册ClientEndpoint :
SparkSubmit
向Master
提交应用过程中,会创建loader用于加载Jar包,参见Spark-Submit。- 然后去获取Jar包中
Client.mainClass
-----即Client.scala
中Object Client
的的main方法
- 借助此
mainClass.newInstance().asInstanceOf[SparkApplication]
来创建一个SparkApplication
( RestSubmissionClientApp或 ClientApp,根据网关提交方式确定)。 ClientApp
其实就是Driver,只是它现在还不叫Driver而已(在SparkContext.SparkEnv.RpcEnv中注册之后才叫Driver)。它内部会去创建ClientEndpoint(其实就是DriverEndpoint),负责与外界的通信。- 创建
ClientEndpoint
时,在它的onStart()
中,如果接收到的参数是launch
,就会获取用户程序的main
,以及一些依赖项、参数等等,封装为一个Command
,然后创建一个DriverDescription
(它包含了command) - 接着,
ClientEndpoint
去向Master
发送RequestSubmitDriver(driverDescription)
(也就是它本身)。参见Spark Submit任务提交中的Driver注册 - 而
Master
接收到消息之后,就会注册Driver
,并执行Driver的启动
。也就是说, Master决定了Driver何时启动,而DriverDescription中包含了执行用户程序的Command。参见Spark之Driver启动(Driver在Worker上启动之后,就会,启动Executor,来执行用户的任务了)
接着,Driver启动并运行用户程序:
- 在
Driver
的启动中,主要是,Master去让Worker启动Driver
,而Worker中创建一个DriverRunner
,来执行前面的DriverDescription中的Command
—即用户程序main()
。参见Spark之Driver启动 - 在用户的
main
方法中,是会创建一个SparkContext
,接下来就看SC的初始化中对Driver的操作。
SparkContext.scala::利用ClientEndpoint在RpcEnv中注册DriverEndpoint
- 在SparkContext源码分析一文中,在3.9 TaskScheduler启动中有提到,TaskScheduler在启动的时候实际是调用了
StandaloneSchedulerBackend
的start
方法. - 事实上,
backend.start()
方法又调用了父类的start()方法
(StandaloneSchedulerBackend extends CoarseGrainedSchedulerBackend) - 在
super.start()
中, 会在sc.env.rpcEnv
中创建一个DriverEndpoint()
。这里,从它的定义可以看出来,这个创建用的参数都是前面SparkConf中的Properties,说明,这里的创建其实只是将SparkSubmit
注册的ClientEndpoint
到这里重新注册
为DriverEndpoint
,参数、配置并未修改
附录
我们就主要看看那Client的代码:
------------------------------class ClientEndpoint--------------------------
override def onStart(): Unit = {
driverArgs.cmd match {
case "launch" =>
// TODO: We could add an env variable here and intercept it in `sc.addJar` that would
// truncate filesystem paths similar to what YARN does. For now, we just require
// people call `addJar` assuming the jar is in the same directory.
// TODO:我们可以在这里添加一个env变量,并在"sc.addJar"中拦截它,这将截文件系统路径,类似于YARN所做的。
// 现在,我们只需要人们调用"addJar"假设这个jar在同一个目录中。
val mainClass = "org.apache.spark.deploy.worker.DriverWrapper"
val classPathConf = "spark.driver.extraClassPath"
val classPathEntries = getProperty(classPathConf, conf).toSeq.flatMap { cp =>
cp.split(java.io.File.pathSeparator)
}
val libraryPathConf = "spark.driver.extraLibraryPath"
val libraryPathEntries = getProperty(libraryPathConf, conf).toSeq.flatMap { cp =>
cp.split(java.io.File.pathSeparator)
}
val extraJavaOptsConf = "spark.driver.extraJavaOptions"
val extraJavaOpts = getProperty(extraJavaOptsConf, conf)
.map(Utils.splitCommandString).getOrElse(Seq.empty)
val sparkJavaOpts = Utils.sparkJavaOpts(conf)
val javaOpts = sparkJavaOpts ++ extraJavaOpts
val command = new Command(mainClass,
Seq("{{WORKER_URL}}", "{{USER_JAR}}", driverArgs.mainClass) ++ driverArgs.driverOptions,
sys.env, classPathEntries, libraryPathEntries, javaOpts)
val driverDescription = new DriverDescription(
driverArgs.jarUrl,
driverArgs.memory,
driverArgs.cores,
driverArgs.supervise,
command)
asyncSendToMasterAndForwardReply[SubmitDriverResponse](
RequestSubmitDriver(driverDescription))
case "kill" =>
val driverId = driverArgs.driverId
asyncSendToMasterAndForwardReply[KillDriverResponse](RequestKillDriver(driverId))
}
}
------------------------------object Client--------------------------
/**
* Executable utility for starting and terminating drivers inside of a standalone cluster.
* 用于在standaloneCluster集群内启动和终止Driver的公用可执行程序
*/
object Client {
def main(args: Array[String]) {
// scalastyle:off println
if (!sys.props.contains("SPARK_SUBMIT")) {
println("WARNING: This client is deprecated and will be removed in a future version of Spark")
println("Use ./bin/spark-submit with \"--master spark://host:port\"")
}
// scalastyle:on println
new ClientApp().start(args, new SparkConf())
}
}
------------------------------class ClientApp--------------------------
private[spark] class ClientApp extends SparkApplication {
override def start(args: Array[String], conf: SparkConf): Unit = {
val driverArgs = new ClientArguments(args)
if (!conf.contains("spark.rpc.askTimeout")) {
conf.set("spark.rpc.askTimeout", "10s")
}
Logger.getRootLogger.setLevel(driverArgs.logLevel)
// 先通过create()获取一个nettyEnv
val rpcEnv =
RpcEnv.create("driverClient", Utils.localHostName(), 0, conf, new SecurityManager(conf))
//通过master的地址和ENDPOINT_NAME(即"Master")来获取一个setupEndpointRef代理对象,即只需要去拿到这个Ref就可以
// 因为在Master的main函数中,已经在startRpcEnvAndEndpoint函数中,
// 通过调用rpcEnv.setupEndpoint()在rpcEnv中注册了masterEndpoint,这里不需要重复去注册。
val masterEndpoints = driverArgs.masters.map(RpcAddress.fromSparkURL).
map(rpcEnv.setupEndpointRef(_, Master.ENDPOINT_NAME))// val ENDPOINT_NAME = "Master"
// 这里就注册一个clientEndpoint.
rpcEnv.setupEndpoint("client",
// 在创建过程中,onStart()会去做初始化,并且创建一个DriverDescription
// 然后通过这个masterEndpoints来发送消息,注册Driver
new ClientEndpoint(rpcEnv, driverArgs, masterEndpoints, conf))
rpcEnv.awaitTermination()
}
}