文章目录

  • 一、向Master申请启动Driver
  • 二、启动Driver
  • 三、DriverWrapper向Master申请资源(给Application)


一、向Master申请启动Driver

SparkSubmit的main方法执行,首先要设置一些参数:

//设置参数
val appArgs = new SparkSubmitArguments(args)

接着会进行模式匹配,匹配到submit,调用submit方法,做了两件事:

//以下方法prepareSubmitEnvironment 返回四元组,重点注意childMainClass类 这里以standalone-cluster为例
    // childMainClass:未来启动Driver的main class是谁
    val (childArgs, childClasspath, sparkConf, childMainClass) = prepareSubmitEnvironment(args)

    //假设standalone模式,集群提交
    def doRunMain(): Unit = ......

首先要准备要提交的环境,它会进行一系列的判断:以standalone还是yarn的方式提交,还是以client或者cluster模式等等。以standalone的cluster模式为例,它又会去判断到底是代码还是命令行方式执行。这里会得到一个重要的变量:

childMainClass = REST_CLUSTER_SUBMIT_CLASS

private[deploy] val STANDALONE_CLUSTER_SUBMIT_CLASS = classOf[ClientApp].getName()

可以简单将childMainClass 理解为是ClientApp,继承自SparkApplication,要加载的就是这东西。

准备好要提交的环境后,会执行runMain方法,并将刚才得到的childMainClass 作为参数传过来、加载、实例化为对象。换句话说,就是要加载并实例化ClientApp对象。随后会调用start方法,即ClientApp#start方法。这个start干了两件事:

  • 创建RPC通信环境
  • 注册Client节点
//创建rpc通信环境
    val rpcEnv =
      RpcEnv.create("driverClient", Utils.localHostName(), 0, conf, new SecurityManager(conf))
    //得到Master的通信邮箱
    val masterEndpoints = driverArgs.masters.map(RpcAddress.fromSparkURL).
      map(rpcEnv.setupEndpointRef(_, Master.ENDPOINT_NAME))
    //在rpc中设置提交当前任务的Endpoint,只要设置肯定会运行 new ClientEndpoint 类的 start方法
    rpcEnv.setupEndpoint("client", new ClientEndpoint(rpcEnv, driverArgs, masterEndpoints, conf))

创建RPC通信环境,跟启动Master、Worker一样,都是创建Netty的RPC环境…
注册这里有点特别,根据以往经验,会塞入"OnStart"作为启动的引信。在onStart方法中,会准备好DriverWrapper类、并将其封装为DriverDescription对象(包含jarUrl、memory、cores等资源):

val mainClass = "org.apache.spark.deploy.worker.DriverWrapper"
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)

对象包装好后,就要向Master发送异步消息来启动Driver,并等待回复:

//向Master申请启动Driver,Master中的 receiveAndReply 方法会接收此请求消息
asyncSendToMasterAndForwardReply[SubmitDriverResponse](
  RequestSubmitDriver(driverDescription))

Master的receiveAndReply方法会匹配RequestSubmitDriver,做相应的处理、回复

二、启动Driver

Master首先要判断自己的状态,如果不是alive,就咋滴咋滴。接着会创建Driver节点:

//创建Driver
val driver: DriverInfo = createDriver(description)

  private def createDriver(desc: DriverDescription): DriverInfo = {
    val now = System.currentTimeMillis()
    val date = new Date(now)
    new DriverInfo(now, newDriverId(date), desc, date)
  }

包装DriverInfo对象,并将Date也塞进去。然后会把刚刚创建好的DriverInfo添加到ArrayBuffer中:

waitingDrivers += driver

private val waitingDrivers = new ArrayBuffer[DriverInfo]

接着就该忙活正事了,调用schedule()申请启动Driver。
首先还是要判断Master状态,如果不是alive就return:

if (state != RecoveryState.ALIVE) {
      return
    }

换言之,如果Master有问题,是根本没办法启动Driver的。
然后会从HashSet[WorkInfo]过滤出所有alive状态的Worker节点,并打散。目的就是方便后面随机挑选一台Worker节点来启动Driver:

//val workers = new HashSet[WorkerInfo],把所有alive状态的Worker统统过滤出来
    //Random.shuffle会把worker打散,方便随机找一台worker节点
    val shuffledAliveWorkers = Random.shuffle(workers.toSeq.filter(_.state == WorkerState.ALIVE))

拿着要启动的Driver,和随机挑出来的Worker作比较,看Worker的可用内存、core是否符合Driver的要求。如果满足就去启动Driver。否则就随机取下一个Worker,如果目前所有的Worker都不符合,不会报错,但也不会执行launchDriver方法。它会一直等待下去:

while (numWorkersVisited < numWorkersAlive && !launched) {
        //拿到curPos位置的worker
        val worker = shuffledAliveWorkers(curPos)
        numWorkersVisited += 1
        //选中的worker的可用内存,是否大于Driver描述的要求的内存
        //还有core
        if (worker.memoryFree >= driver.desc.mem && worker.coresFree >= driver.desc.cores) {
          //这里是启动Driver,启动Driver之后会为当前的application 申请资源
          launchDriver(worker, driver)
          waitingDrivers -= driver
          launched = true
        }
        //curPos 就是一直加一的往后取 Worker  ,一直找到满足资源的worker
        curPos = (curPos + 1) % numWorkersAlive
      }

既然要启动Driver,那就得告诉Worker。获取Worker的EndPoint,给Master发消息。Worker的receive方法会接收并匹配到“”LaunchDriver“”(会一并携带Driver的id、desc等。desc里面塞的就是DriverWrapper):

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,会初始化 org.apache.spark.deploy.worker.DriverWrapper ,运行main方法
      driver.start()

      coresUsed += driverDesc.cores
      memoryUsed += driverDesc.mem

启动Driver就是启动DriverWrapper,start方法会触发DriverWrapper的main方法的运行。它会启动Driver进程,只要进程有了,就会把我们的代码跑起来。在DriverWrapper中,mainClass就是我们要提交的Application,并从中得到main方法,并执行main方法:

//得到提交application的主方法
        val mainMethod = clazz.getMethod("main", classOf[Array[String]])

        //启动提交的application 中的main 方法。
        mainMethod.invoke(null, extraArgs.toArray[String])

三、DriverWrapper向Master申请资源(给Application)

我们的代码会new SparkContext,它会创建TaskScheduler和DAGScheduler:

private[spark] def taskScheduler: TaskScheduler = _taskScheduler
private[spark] def dagScheduler: DAGScheduler = _dagScheduler

从以下代码可知,Executor的启动内存为1G:

//默认每个Executor启动内存为1024M
    _executorMemory = _conf.getOption("spark.executor.memory")
      .orElse(Option(System.getenv("SPARK_EXECUTOR_MEMORY")))
      .orElse(Option(System.getenv("SPARK_MEM"))
      .map(warnSparkMem))
      .map(Utils.memoryStringToMb)
      .getOrElse(1024)

TaskScheduler和DAGScheduler是一起创建出来的:

val (sched, ts) = SparkContext.createTaskScheduler(this, master, deployMode)
    _schedulerBackend = sched //StandaloneSchedulerBackend
    _taskScheduler = ts //TaskSchedulerImpl
    _dagScheduler = new DAGScheduler(this)
    _heartbeatReceiver.ask[Boolean](TaskSchedulerIsSet)

createTaskScheduler()方法的参数:master,指的是standalone或者yarn,它返回了1个二元组:

//standalone 提交任务都是以 “spark://”这种方式提交
      case SPARK_REGEX(sparkUrl) =>
        //scheduler 创建TaskSchedulerImpl 对象
        val scheduler = new TaskSchedulerImpl(sc)
        val masterUrls = sparkUrl.split(",").map("spark://" + _)
        /**
          * 这里的 backend 是StandaloneSchedulerBackend 这个类型
          */
        val backend = new StandaloneSchedulerBackend(scheduler, sc, masterUrls)
        //这里会调用 TaskSchedulerImpl 对象中的  initialize 方法将 backend 初始化,一会要用到
        scheduler.initialize(backend)
        //返回了  StandaloneSchedulerBackend 和 TaskSchedulerImpl 两个对象
        (backend, scheduler)

以standalone为例,SPARK_REGEX代表的就是要匹配“spark://”这样的。可以看出,返回的这个二元组(backend, scheduler),backend就是StandaloneSchedulerBackend,scheduler就是TaskSchedulerImpl对象。得到TaskSchedulerImpl后,会调用start方法,间接走的backend的start方法,最终执行super.start(),也就是走的父类CoarseGrainedSchedulerBackend,创建Driver引用,未来的Executor就是向CoarseGrainedSchedulerBackend中反向注册信息的:

//这就是创建Driver的EndPoint
driverEndpoint = createDriverEndpointRef(properties)

//后面做的事很熟悉了:向Map中添加EndPointData,并将其放到receivers中
  protected def createDriverEndpointRef(
      properties: ArrayBuffer[(String, String)]): RpcEndpointRef = {
    rpcEnv.setupEndpoint(ENDPOINT_NAME, createDriverEndpoint(properties))
  }

  protected def createDriverEndpoint(properties: Seq[(String, String)]): DriverEndpoint = {
    new DriverEndpoint(rpcEnv, properties)
  }

ENDPOINT_NAME=“CoarseGrainedScheduler”,这才是真正的Driver,而非DriverWrapper。就是去注册CoarseGrainedScheduler,作为Driver

start之后会将CoarseGrainedExecutorBackend包装成Command对象,并近一步封装为ApplicationDescription对象(里面有启动Executor所需要的的内存、core等信息),最后会将其包装为StandaloneAppClient对象,并调用其start方法:

//往Map里塞AppClient
endpoint.set(rpcEnv.setupEndpoint("AppClient", new ClientEndpoint(rpcEnv)))

只要设置了EndPoint,就一定会调用onStart方法:

override def onStart(): Unit = {
      try {
        //向Master 注册当前application的信息
        registerWithMaster(1)
      } catch {
        case e: Exception =>
          logWarning("Failed to connect to master", e)
          markDisconnected()
          stop()
      }
    }

这里会向所有的Master注册Application,以为Master是高可用的。做法就是遍历出所有的Master地址,通过Master的ENDPOINT_NAME获取到Master引用,最后send一个RegisterApplication类型的消息让Master去匹配(携带有command对象、core、memory等)