概要
在前面,我们已经介绍了Spark-Submit任务提交的流程,并且从源码分析了Driver的启动注册流程。此外,由于Driver的启动注册涉及到了Master和Worker,因此我们还介绍了Master启动并提供服务和Worker启动、注册、发送心跳。
基于此,我们需要分析一下Driver启动的过程。在此之前。请浏览以上文章,以便更好的理解此文。
在这里,我们假定:
deploy-mode = cluster
Master、Worker
均已经启动Client
已经向Master发送了RequestSubmitDrive
r消息。Master接收
后,需要通过判断deploy-mode确定Driver在何处启动。这里是cluster模式,因此在Master指定的Worker上启动。
1. 启动流程
请务必先看完Driver的启动注册流程,确保了解了Driver是如何达到向Master进行注册的。接下来,我们在看看Driver的启动:
1.1 Driver启动之Master执行Schedule()调度
在Master
接收到RequestSubmitDriver
之后,会做一下的事:
- 向
client
回复SubmitDriverResponse
- 根据
DriverDesc
创建Driver
,并持久化此Driver
。 - 将
Driver
加入waitingDrivers
等待调度队列。 Schedule()
进行调度(在等待的apps中计划当前可用的资源。每次新app加入或资源可用性更改时都将调用此方法。)
这里Schedule()
资源调度的说明很有意思,表面上说是为app
来进行资源调度,但是实际上,app是基于Driver运行的
,因此,在Schedule()内部,是通过Master自身的资源调度算法执行的launchDriver()
1.2 Driver启动之Master执行launchDriver()
前面提到了,schedule()
中确定worker后最终就是去lunchDriver()
:
- 将
Driver信息
添加到对应的Worker
中 - 同样将
Worker信息
添加到对应的Driver
中 - 通过本地Worker信息中的
workerRpcEndpointRef
向Worker发送LaunchDriver
消息,让Worker去启动Driver
1.3 Driver启动之Worker接收消息
通过底层RPC来receive()消息
- 将
Driver
封装为DriverRunner
,然后调用其Start()
方法启动Driver。在582行:
可以看到,这里就从DriverDesc
中获取了前面封装的Command
,并传给了DriverRunner线程。(封装部分参见Spark Submit任务提交中的ClientEndpoint.onStart()) - 将此
Driver
添加到维护的Drivers
(HashMap)中,格式是<driverid,driver>
- 修改已使用的
内存
和CPU核数
1.4 Driver启动之Worker启动Driver
前面提到的,封装之后,
1. 执行DriverRunner.start()来启动driver:
(省略了很多代码,完整的可以参考附录)
1.1 启动一个Thread
Thread的run()中调用prepareAndRunDriver(),准备jar包并运行Driver
2. 执行prepareAndRunDriver()
进行准备工作并启动Driver
在这里,我们可以看到,创建出来的ProccessBuilder
将执行是DriverDesc.Command
,而这个Command
,其实就是用户程序,入口为main(). (原因参见Spark Submit任务提交中的ClientEndpoint.onStart())
2.1 创建本地文件夹,存储下载的用户程序jar包
2.2 运行Driver
- 设置ProcessBuilder的目录
- 重定向输入流到前面下载到本地的用户jar包
- 执行runCommandWithRetry,传入自定义的initialize函数,如果失败,Retry。
执行步骤如上图注释,最终使用Java中的java.lang.ProcessBuilder类执行Linux命令的方式启动Driver,Linux命令大致如下
3. 执行生成的命令
这是Worker通过解析参数和配置,获得的Command,获取到之后,它会自动去执行这些命令。而到此,Driver也就正式启动并运行在Worker了。结合步骤二可知,Driver上运行的就是用户程序jar中的自定义程序。
4. 最后,Worker会将Driver的执行状态返回给Master。
总结
介绍了Master将Driver发送到Worker,及在Worker节点启动Driver的流程,如下
附录
图片中涉及到的代码
---------------------------------------------------------------------------------
// 在等待的apps中计划当前可用的资源。每次新app加入或资源可用性更改时都将调用此方法。
// 即使是deploy-mode=cluster模式中,注册的Driver信息是在waitingDrivers中
// 即使是client模式,不注册Driver的情况下,依然会执行Schedule():执行可能因资源或Master Recovery等问题处于waiting状态的driver
private def schedule(): Unit = {
if (state != RecoveryState.ALIVE) {return}
// Drivers优先于Executors
// 打乱Worker顺序,避免Driver集中
val shuffledAliveWorkers = Random.shuffle(workers.toSeq.filter(_.state == WorkerState.ALIVE))
val numWorkersAlive = shuffledAliveWorkers.size
var curPos = 0
for (driver <- waitingDrivers.toList) { // 遍历waitingdrivers的副本
// 我们以循环的方式将worker分配给每个等待的driver。
// 对于每个driver,我们从分配给driver的最后一个worker开始,然后继续,直到我们探索了所有活着的worker。
var launched = false
var numWorkersVisited = 0
while (numWorkersVisited < numWorkersAlive && !launched) {
val worker = shuffledAliveWorkers(curPos)
numWorkersVisited += 1
if (worker.memoryFree >= driver.desc.mem && worker.coresFree >= driver.desc.cores) {
// 如果Worker上没有启动Driver,并且Worker上的空闲内存与空闲CPU核数均大于Driver启动所需的资源,那么就在此worker上启动Driver。
launchDriver(worker, driver)
// 执行了lunch之后,就将Driver从当前等待调度的Driver队列中移除。
waitingDrivers -= driver
launched = true
}
curPos = (curPos + 1) % numWorkersAlive
}
}
// 规划并启动worker上的Executors
startExecutorsOnWorkers()
}
----------------------------------------------------------------------------------------------
private def launchDriver(worker: WorkerInfo, driver: DriverInfo) {
logInfo("Launching driver " + driver.id + " on worker " + worker.id)
worker.addDriver(driver)
driver.worker = Some(worker)
worker.endpoint.send(LaunchDriver(driver.id, driver.desc))
driver.state = DriverState.RUNNING
}
---------------------------------------------------Worker.scala-------------------------------------------
case LaunchDriver(driverId, driverDesc) =>
logInfo(s"Asked to launch driver $driverId")
val driver = new DriverRunner(
conf,
driverId,
workDir,
sparkHome,
driverDesc.copy(command = Worker.maybeUpdateSSLSettings(driverDesc.command, conf)),
self,
workerUri,
securityMgr)
drivers(driverId) = driver
driver.start()
coresUsed += driverDesc.cores
memoryUsed += driverDesc.mem
-------------------------------------------------DriverRunner.scala-------------------------------------
private[worker] def start() = {
new Thread("DriverRunner for " + driverId) {
override def run() {
var shutdownHook: AnyRef = null
try {
shutdownHook = ShutdownHookManager.addShutdownHook { () =>
logInfo(s"Worker shutting down, killing driver $driverId")
kill()
}
// 准备Driver jars并运行Driver
val exitCode = prepareAndRunDriver()
// set final state depending on if forcibly killed and process exit code
finalState = if (exitCode == 0) {
Some(DriverState.FINISHED)
} else if (killed) {
Some(DriverState.KILLED)
} else {
Some(DriverState.FAILED)
}
} catch {
case e: Exception =>
kill()
finalState = Some(DriverState.ERROR)
finalException = Some(e)
} finally {
if (shutdownHook != null) {
ShutdownHookManager.removeShutdownHook(shutdownHook)
}
}
// notify worker of final driver state, possible exception
worker.send(DriverStateChanged(driverId, finalState.get, finalException))
}
}.start()
}
-----------------------------------------------DriverRunner.scala-------------------------------------------------
private[worker] def prepareAndRunDriver(): Int = {
// 下载Driver Jar到Worker本地,也就是用户提交的用户程序jar包等
val driverDir = createWorkingDirectory()
val localJarFilename = downloadUserJar(driverDir)
// 替换参数中的workerUrl和localJarFileName
def substituteVariables(argument: String): String = argument match {
case "{{WORKER_URL}}" => workerUrl
case "{{USER_JAR}}" => localJarFilename
case other => other
}
/**
* 将Driver中的参数组织为Linux命令
* 通过Java执行组织好的命令,使用java.lang.ProcessBuilder运行
* 这一就是启动Driver,即执行用户程序中的main方法。
*/
// TODO: 如果我们增加了提交多个jar的能力,它们也应该添加到这里
val builder = CommandUtils.buildProcessBuilder(driverDesc.command, securityManager,
driverDesc.mem, sparkHome.getAbsolutePath, substituteVariables)
runDriver(builder, driverDir, driverDesc.supervise)
}
// 将用户jar下载到提供的目录并返回其本地路径。如果下载jar时出错,将引发异常。
private def downloadUserJar(driverDir: File): String = {
val jarFileName = new URI(driverDesc.jarUrl).getPath.split("/").last
val localJarFile = new File(driverDir, jarFileName)
if (!localJarFile.exists()) { // May already exist if running multiple workers on one node
logInfo(s"Copying user jar ${driverDesc.jarUrl} to $localJarFile")
Utils.fetchFile(
driverDesc.jarUrl,
driverDir,
conf,
securityManager,
SparkHadoopUtil.get.newConfiguration(conf),
System.currentTimeMillis(),
useCache = false)
if (!localJarFile.exists()) { // Verify copy succeeded
throw new IOException(
s"Can not find expected jar $jarFileName which should have been loaded in $driverDir")
}
}
localJarFile.getAbsolutePath
}
private def runDriver(builder: ProcessBuilder, baseDir: File, supervise: Boolean): Int = {
// 1. 设置ProcessBuilder的目录
builder.directory(baseDir)
// initialize中主要是为了输出的信息格式化等,不是核心
def initialize(process: Process): Unit = {
// 将stdout and stderr重定向到files中
val stdout = new File(baseDir, "stdout")
// 重定向输入流到前面下载到本地的用户jar包
CommandUtils.redirectStream(process.getInputStream, stdout)
val stderr = new File(baseDir, "stderr")
// 从ProcessBuilder中获取到命令参数,并格式化命令
// formattedCommand这个是为了格式化输出日志或者其他,不是核心代码
val formattedCommand = builder.command.asScala.mkString("\"", "\" \"", "\"")
val header = "Launch Command: %s\n%s\n\n".format(formattedCommand, "=" * 40)
// 如果出现错误,将错误信息输出到文件中
Files.append(header, stderr, StandardCharsets.UTF_8)
CommandUtils.redirectStream(process.getErrorStream, stderr)
}
// 2. 执行前面命令,闯入自定义的initialize函数,如果失败,Retry
runCommandWithRetry(ProcessBuilderLike(builder), initialize, supervise)
}
------------------------------------------------------------