Spark之Driver启动

概要


  在前面,我们已经介绍了Spark-Submit任务提交的流程,并且从源码分析了Driver的启动注册流程。此外,由于Driver的启动注册涉及到了Master和Worker,因此我们还介绍了Master启动并提供服务Worker启动、注册、发送心跳

  1. Spark-Submit任务提交
  2. Driver的启动注册流程
  3. Master启动并提供服务
  4. Worker启动、注册、发送心跳

基于此,我们需要分析一下Driver启动的过程。在此之前。请浏览以上文章,以便更好的理解此文。
在这里,我们假定:

  • deploy-mode = cluster
  • Master、Worker均已经启动
  • Client已经向Master发送了RequestSubmitDriver消息。
  • 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()
在这里插入图片描述

  1. Driver信息添加到对应的Worker
  2. 同样将Worker信息添加到对应的Driver
  3. 通过本地Worker信息中的workerRpcEndpointRef向Worker发送LaunchDriver消息,让Worker去启动Driver

1.3 Driver启动之Worker接收消息

通过底层RPC来receive()消息
在这里插入图片描述

  1. Driver封装为DriverRunner,然后调用其Start()方法启动Driver。在582行:在这里插入图片描述
    可以看到,这里就从DriverDesc中获取了前面封装的Command,并传给了DriverRunner线程。(封装部分参见Spark Submit任务提交中的ClientEndpoint.onStart())
  2. 将此Driver添加到维护的Drivers(HashMap)中,格式是<driverid,driver>
  3. 修改已使用的内存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)
  }
  ------------------------------------------------------------
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值