【读懂面经中的源码】SPARK源码解析——Spark任务提交、调度、执行过程

本次为各位同学准备的是Spark高频八股–Spark任务提交、调度、执行过程~

创作不易!多多支持!

面筋

Spark任务提交、调度、执行过程

Spark的架构有三种方式:local模式standalone模式cluster模式(yarn、mesos、k8s等),因此对执行过程也可以拆分为3种。

参考链接

Standalone

是Spark实现的资源调度框架,主要的节点有Client节点、Master节点和Worker节点。Driver既可以运行在Master节点上,也可以运行在本地Client端。

当以standalone模式向spark集群提交作业时,作业的运行流程如下(图片来源:公众号旧时光大数据):

在这里插入图片描述

  1. 我们提交一个任务,任务就叫Application
  2. 初始化程序的入口SparkContext,
    1. 初始化DAG Scheduler
    2. 初始化Task Scheduler
  3. SparkContext中的Task Scheduler连接到Master,向Master注册并申请资源(CPU Core和Memory)
  4. Worker定期发送心跳信息给Master并报告Executor状态
  5. Master根据SparkContext的资源申请要求和Worker心跳周期内报告的信息决定在哪个worker上分配资源,然后在该worker上获取资源,启动StandaloneExecutorBackend
  6. StandaloneExecutorBackend向SparkContext注册
  7. SparkContext将Applicaiton代码发送给StandaloneExecutorBackend;并且SparkContext解析Applicaiton代码,构建DAG图,并提交给DAG Scheduler分解成Stage(当碰到Action操作 时,就会催生Job;每个Job中含有1个或多个Stage,Stage一般在获取外部数据和shuffle之前产生)
  8. 将Stage(或者称为TaskSet)提交给Task Scheduler。Task Scheduler负责将Task分配到相应的Worker,最后提交给StandaloneExecutorBackend执行
  9. 对task进行序列化,并根据task的分配算法,分配task
  10. 对接收过来的task进行反序列化,把task封装成一个线程
  11. 开始执行Task,并向SparkContext报告,直至Task完成。
  12. 资源注销

yarn-client

图片来源:公众号旧时光大数据:

在这里插入图片描述

  1. Spark Yarn Client向YARN的ResourceManager申请启动Application Master。同时在SparkContent初始化中将创建DAGScheduler和TASKScheduler等,由于我们选择的是Yarn-Client模式,程序会选择YarnClientClusterScheduler和YarnClientSchedulerBackend
  2. ResourceManager收到请求后,在集群中选择一个NodeManager,为该应用程序分配第一个Container,要求它在这个Container中启动应用程序的ApplicationMaster,与YARN-Cluster区别的是在该ApplicationMaster不运行SparkContext,只与SparkContext进行联系进行资源的分派;
  3. Client中的SparkContext初始化完毕后,与ApplicationMaster建立通讯,向ResourceManager注册,根据任务信息向ResourceManager申请资源(Container);
  4. 一旦ApplicationMaster申请到资源(也就是Container)后,便与对应的NodeManager通信,要求它在获得的Container中启动启动CoarseGrainedExecutorBackend,CoarseGrainedExecutorBackend启动后会向Client中的SparkContext注册并申请Task;
  5. Client中的SparkContext分配Task给CoarseGrainedExecutorBackend执行,CoarseGrainedExecutorBackend运行Task并向Driver汇报运行的状态和进度,以让Client随时掌握各个任务的运行状态,从而可以在任务失败时重新启动任务;
  6. 应用程序运行完成后,Client的SparkContext向ResourceManager申请注销并关闭自己。

yarn-cluster

图片来源:公众号旧时光大数据:

在这里插入图片描述

  1. Spark Yarn Client向YARN中提交应用程序,包括ApplicationMaster程序、启动ApplicationMaster的命令、需要在Executor中运行的程序等;
  2. ResourceManager收到请求后,在集群中选择一个NodeManager,为该应用程序分配第一个Container,要求它在这个Container中启动应用程序的ApplicationMaster,其中ApplicationMaster进行SparkContext等的初始化;
  3. ApplicationMaster向ResourceManager注册,这样用户可以直接通过ResourceManage查看应用程序的运行状态,然后它将采用轮询的方式通过RPC协议为各个任务申请资源,并监控它们的运行状态直到运行结束;
  4. 一旦ApplicationMaster申请到资源(也就是Container)后,便与对应的NodeManager通信,要求它在获得的Container中启动启动CoarseGrainedExecutorBackend,CoarseGrainedExecutorBackend启动后会向ApplicationMaster中的SparkContext注册并申请Task。这一点和Standalone模式一样,只不过SparkContext在Spark Application中初始化时,使用CoarseGrainedSchedulerBackend配合YarnClusterScheduler进行任务的调度,其中YarnClusterScheduler只是对TaskSchedulerImpl的一个简单包装,增加了对Executor的等待逻辑等;
  5. ApplicationMaster中的SparkContext分配Task给CoarseGrainedExecutorBackend执行,CoarseGrainedExecutorBackend运行Task并向ApplicationMaster汇报运行的状态和进度,以让ApplicationMaster随时掌握各个任务的运行状态,从而可以在任务失败时重新启动任务;
  6. 应用程序运行完成后,ApplicationMaster向ResourceManager申请注销并关闭自己。

yarn-client和yarn-cluster的区别

理解YARN-Client和YARN-Cluster深层次的区别之前先清楚一个概念:Application Master。在YARN中,每个Application实例都有一个ApplicationMaster进程,它是Application启动的第一个容器。它负责和ResourceManager打交道并请求资源,获取资源之后告诉NodeManager为其启动Container。从深层次的含义讲YARN-Cluster和YARN-Client模式的区别其实就是ApplicationMaster进程的区别。

  • YARN-Cluster模式下,Driver运行在AM(Application Master)中,它负责向YARN申请资源,并监督作业的运行状况。当用户提交了作业之后,就可以关掉Client,作业会继续在YARN上运行,因而YARN-Cluster模式不适合运行交互类型的作业;
  • YARN-Client模式下,Application Master仅仅向YARN请求Executor,Client会和请求的Container通信来调度他们工作,也就是说Client不能离开。

Spark任务提交

当使用spark-submit命令提交任务的时候,脚本内部调用的是SparkSubmit类。定位到其main函数中,其中执行了**submit.doSubmit(args)**的函数。

  def doSubmit(args: Array[String]): Unit = {
    // Initialize logging if it hasn't been done yet. Keep track of whether logging needs to
    // be reset before the application starts.
    // 如果尚未完成日志记录,则初始化日志记录。跟踪在应用程序启动之前是否需要重置日志记录
    val uninitLog = initializeLogIfNecessary(true, silent = true)

    val appArgs = parseArguments(args)
    if (appArgs.verbose) {
      logInfo(appArgs.toString)
    }
    // 根据4种行为处理
    appArgs.action match {
      // 代理入口点为这个方法,提交应用程序的处理方法
      case SparkSubmitAction.SUBMIT => submit(appArgs, uninitLog) // 提交,调用submit方法
      case SparkSubmitAction.KILL => kill(appArgs) // 杀死,调用kill方法
      case SparkSubmitAction.REQUEST_STATUS => requestStatus(appArgs) // 请求状态,调用requestStast方法
      case SparkSubmitAction.PRINT_VERSION => printVersion()
    }
  }

经过一系列的处理,最后执行app.start方法,这时候就是执行不同模式的分水岭。

Standalone cluster模式

程序会走到这个位置

  override def start(args: Array[String], conf: SparkConf): Unit = {
    // 实例化出一个SparkConfig对象,通过这个配置对象,可以在代码中指定一些配置项,
    // 如appName、Master地址等。val driverArgs = new ClientArguments(args)
    // 使用传入的args参数构建一个ClientArguments对象,该对象同样保留传入的配置信息,
    // 如Executor memory、Executor cores等都包含在这个
    val driverArgs = new ClientArguments(args)
    // 设置RPC请求超时时间为10sif (!conf.contains("spark.rpc.
    if (!conf.contains(RPC_ASK_TIMEOUT)) {
      conf.set(RPC_ASK_TIMEOUT, "10s")
    }
    Logger.getRootLogger.setLevel(driverArgs.logLevel)
    // 使用RpcEnv的create创建RPC环境
//    使用该成员设置好到Master的通信端点,通过该端点实现同Master的通信
    val rpcEnv =
      RpcEnv.create("driverClient", Utils.localHostName(), 0, conf, new SecurityManager(conf))
    // 得到master的URL并得到Master的Endpoints,用于同Master通信
    val masterEndpoints = driverArgs.masters.map(RpcAddress.fromSparkURL).
      map(rpcEnv.setupEndpointRef(_, Master.ENDPOINT_NAME))
    // 在rpcEnv.setupEndpoint方法中调用new()函数创建一个Driver ClientEndpoint。
    // ClientEndpoint是一个ThreadSafeRpcEndpoint消息循环体,
    // 至此就生成了Driver ClientEndpoint。
    // 在ClientEndpoint的onStart方法中向Master提交注册。
    // 这里通过masterEndpoint向Master发送RequestSubmitDriver(driverDescription)请求,
    // 完成Driver的注册

    // Master收到Driver ClientEndpoint的RequestSubmitDriver消息以后,就将Driver的信息加入到waitingDrivers和drivers的数据结构中。
    // 然后进行schedule()资源分配,Master向Worker发送LaunchDriver的消息指令


    rpcEnv.setupEndpoint("client", new ClientEndpoint(rpcEnv, driverArgs, masterEndpoints, conf))
    // 等待rpcEnv的终止
    rpcEnv.awaitTermination()
  }

在这里,主要的流程是

  1. 注册ClientEndpoint
  2. 封装DriverDescription,比如
  3. 调用onStart()方法向Master提交Driver注册向Master发送注册请求RequestSubmitDriver(driverDescription))

Master接受RequestSubmitDriver请求,然后执行如下代码

override def receiveAndReply(context: RpcCallContext): PartialFunction[Any, Unit] = {
	case RequestSubmitDriver(description) =>
      // 若 state 不为 ALIVE,直接向 Client 返回 SubmitDriverResponse(selffalse
      // None_msg)消息
      if (state != RecoveryState.ALIVE) {
        val msg = s"${Utils.BACKUP_STANDALONE_MASTER_PREFIX}: $state. " +
          "Can only accept driver submissions in ALIVE state."
        context.reply(SubmitDriverResponse(self, false, None, msg))
      } else {
        logInfo("Driver submitted " + description.command.mainClass)
        // 使用description创建driver,该方法返回DriverDescription
        val driver = createDriver(description)
        // watingDrviers等待在调度数组中加入该driver
        persistenceEngine.addDriver(driver)
        waitingDrivers += driver
        // 用schedule方法调度资源
        drivers.add(driver)
        schedule()

        // TODO: It might be good to instead have the submission client poll the master to determine
        //       the current status of the driver. For now it's simply "fire and forget".

        context.reply(SubmitDriverResponse(self, true, Some(driver.id),
          s"Driver successfully submitted as ${driver.id}"))
      }
    ...
}

Master收到Driver ClientEndpoint的RequestSubmitDriver消息以后,就将Driver的信息加入到waitingDrivers和drivers的数据结构中。然后进行schedule()资源分配,Master向Worker发送LaunchDriver的消息指令。

Master.launchDriver代码如下,表示发送命令给worker节点,让远程的worker启动driver。

private def launchDriver(worker: WorkerInfo, driver: DriverInfo): Unit = {
  logInfo("Launching driver " + driver.id + " on worker " + worker.id)
  worker.addDriver(driver)
  driver.worker = Some(worker)
  // 发指令给worker,让远程的worker启动driver,driver启动之后,Driver的状态就变成DriverState.RUNNING
  worker.endpoint.send(LaunchDriver(driver.id, driver.desc, driver.resources))
  driver.state = DriverState.RUNNING
}

在worker接收到启动driver的命令后,内部使用Thread来处理Driver和Executor的启动。代码在Worker.receive部分

case LaunchDriver(driverId, driverDesc, resources_) =>
  // Worker进程:Worker的DriverRunner调用start方法,内部使用Thread来处理Driver启动。
  // DriverRunner创建Driver在本地系统的工作目录(即Linux的文件目录),每次工作都有自己的目录,
  // 封装好Driver的启动Command,通过ProcessBuilder启动Driver。这些内容都属于Worker进程。
  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,
    workerWebUiUrl,
    securityMgr,
    resources_)
  drivers(driverId) = driver
  // 启动Driver
  driver.start()
  // start之后,将消耗的cores、memory增加到coresUsed、memoryUsed
  coresUsed += driverDesc.cores
  memoryUsed += driverDesc.mem
  addResourcesUsed(resources_)

点击计入DriverRunner.start的代码中,可以看到有一行代码写的是如下代码,表示准备Driver的jar包,并且运行Driver

val exitCode = prepareAndRunDriver()

这里运行的是DriverRunner.runCommandWithRetry方法,在这里执行之前spark.submit上传的jar包方法

在Driver执行jar包的过程中,发送Driver的启动DriverStateChanged信息至Master。Master接受到信息之后,移除Master内存、持久化引擎中的drivers信息,并把当前driver加入到Master的已完成的drivers队列,并减去该driver关联的worker信息中的Driver内存和cores资源,最后重新调度schedule()方法。

schedule()方法中有一个调用startExecutorsOnWorkers()为当前的程序调度和启动Worker的Executor的方法,默认情况下排队的方式是FIFO

private def startExecutorsOnWorkers(): Unit = {
  // Right now this is a very simple FIFO scheduler. We keep trying to fit in the first app
  // in the queue, then the second app, etc.
  // 这是一个非常简单的 FIFO 调度。我们尝试在队列中推入第一个应用程序,然后推入第2个应用程序等
  for (app <- waitingApps) {
    // 筛选出workers,其没有足够资源来启动Executor
    val coresPerExecutor = app.desc.coresPerExecutor.getOrElse(1)
    // If the cores left is less than the coresPerExecutor,the cores left will not be allocated
    if (app.coresLeft >= coresPerExecutor) {
      // Filter out workers that don't have enough resources to launch an executor
      // 如果剩余的核心小于coresPerExecutor,则不会分配剩余的核心
      val usableWorkers = workers.toArray.filter(_.state == WorkerState.ALIVE)
        .filter(canLaunchExecutor(_, app.desc))
          // 在此基础上进行排序,产生计算资源由大到小的coresFree数据结构
        .sortBy(_.coresFree).reverse
      val appMayHang = waitingApps.length == 1 &&
        waitingApps.head.executors.isEmpty && usableWorkers.isEmpty
      if (appMayHang) {
        logWarning(s"App ${app.id} requires more resource than any of Workers could have.")
      }
      // spreadOutApps默认让应用程序尽可能地多运行在所有的Node上
      val assignedCores = scheduleExecutorsOnWorkers(app, usableWorkers, spreadOutApps)

      // 现在我们决定每个worker分配多少个cores,进行分配
      // Now that we've decided how many cores to allocate on each worker, let's allocate them
      for (pos <- 0 until usableWorkers.length if assignedCores(pos) > 0) {
        allocateWorkerResourceToExecutors(
          app, assignedCores(pos), app.desc.coresPerExecutor, usableWorkers(pos))
      }
    }
  }

在其中遍历wattingApps需要分配的apps,然后查找可用的worker列表内存和cores是否满足并根据剩余cores排序;根据分配机制尽可能地将应用程序多运行在所有的Node上。

根据分配结果,执行allocateWorkerResourceToExecutorslaunchExecutor(worker, exec)的方法,通过向worker发送LaunchExecutor启动executor,并把executor添加的ExecutorAdded信息向Driver发送。

worker接收到launchExecutor信息之后,启动一个线程Thread,在run方法中调用fetchAndRunExecutor方法

/**
 * Download and run the executor described in our ApplicationDescription
 * fetchAndRunExecutor类似于启动Driver的过程,在启动Executor时首先构建CommandUtils.buildProcessBuilder,
 * 然后是builder.start(),退出时发送ExecutorStateChanged消息给Worker
 */
private def fetchAndRunExecutor(): Unit = {
  try {
    val resourceFileOpt = prepareResourcesFile(SPARK_EXECUTOR_PREFIX, resources, executorDir)
    // Launch the process
    // 传入的入口类是"org.apache.spark.executor.CoarseGrainedExecutorBackend
    val arguments = appDesc.command.arguments ++ resourceFileOpt.map(f =>
      Seq("--resourcesFile", f.getAbsolutePath)).getOrElse(Seq.empty)
    
    // 在ExecutorRunner中将通过CommandUtil构建一个ProcessBuilder,
    // 调用ProcessBuilder的start方法将会以进程的方式启动org.apache.spark.executor.CoarseGrainedExecutorBackend

    // 当Worker节点中启动ExecutorRunner时,ExecutorRunner中会启动CoarseGrainedExecutorBackend进程,
    // 在CoarseGrainedExecutorBackend的onStart方法中,向Driver发出RegisterExecutor注册请求
    val builder = CommandUtils.buildProcessBuilder(subsCommand, new SecurityManager(conf),
      memory, sparkHome.getAbsolutePath, substituteVariables)
    val command = builder.command()
    
	...
    process = builder.start()
    
  } catch {
    ...
  }
}

可以看到,上述的代码中实际上是启动了CoarseGrainedExecutorBackend的进程,这个进程实际上调用了onStrart()的方法

  // 当Worker节点中启动ExecutorRunner时,ExecutorRunner中会启动CoarseGrainedExecutorBackend进程,
  // 在CoarseGrainedExecutorBackend的onStart方法中,向Driver发出RegisterExecutor注册请求
  override def onStart(): Unit = {
    ...
    rpcEnv.asyncSetupEndpointRefByURI(driverUrl).flatMap { ref =>
      // This is a very fast action so we can use "ThreadUtils.sameThread"
        // 将向Driver端发送RegisterExecutor消息请求注册,完成注册后
      driver = Some(ref)
      // 向Driver发送ask请求,等待Driver回应
      ref.ask[Boolean](RegisterExecutor(executorId, self, hostname, cores, extractLogUrls,
        extractAttributes, _resources, resourceProfile.id))
    }
      ...
  }

由上述代码可以看到,CoarseGrainedExecutorBackend执行onStart向Driver端发送了RegisterExecutor的信息,表示向Driver端发送的是已经注册后的信息,这就跟面筋对应上了!

client模式

SparkSubmit.prepareSubmitEnviorment中有一段代码显示

    if (deployMode == CLIENT) {
      childMainClass = args.mainClass
        ...
    }

这段代码的childMainClass就是你自己构建应用程序的类,表示直接运行你应用程序的代码。

YarnCluster模式

SparkSubmit最终调用的是YarnClusterApplication对象中的start方法

private[spark] class YarnClusterApplication extends SparkApplication {

  override def start(args: Array[String], conf: SparkConf): Unit = {
    // SparkSubmit would use yarn cache to distribute files & jars in yarn mode,
    // so remove them from sparkConf here for yarn mode.
    conf.remove(JARS)
    conf.remove(FILES)
    conf.remove(ARCHIVES)

    new Client(new ClientArguments(args), conf, null).run()
  }
}

在这里创建了Clinet对象,并执行run方法。run方法的注释写的很清楚,这个方法就是向ResourceManager提交申请。如果设置Spark.yarn.Submit.WaitappCompletion to true,它将活着报告应用程序状态,直到由于任何原因退出了应用程序。否则,客户流程将在提交后退出。如果申请以失败,杀死或未定义的身份完成,请抛出适当的Sparkexception。

其中执行的是this.appId = submitApplication()

/**
 * Submit an application running our ApplicationMaster to the ResourceManager.
 *
 * The stable Yarn API provides a convenience method (YarnClient#createApplication) for
 * creating applications and setting up the application submission context. This was not
 * available in the alpha API.
 */
def submitApplication(): ApplicationId = {
  ResourceRequestHelper.validateResources(sparkConf)

  var appId: ApplicationId = null
  try {
    launcherBackend.connect()
    yarnClient.init(hadoopConf)
    yarnClient.start()

    logInfo("Requesting a new application from cluster with %d NodeManagers"
      .format(yarnClient.getYarnClusterMetrics.getNumNodeManagers))

    // Get a new application from our RM
    val newApp = yarnClient.createApplication()
    val newAppResponse = newApp.getNewApplicationResponse()
    appId = newAppResponse.getApplicationId()

    // The app staging dir based on the STAGING_DIR configuration if configured
    // otherwise based on the users home directory.
    // scalastyle:off FileSystemGet
    val appStagingBaseDir = sparkConf.get(STAGING_DIR)
      .map { new Path(_, UserGroupInformation.getCurrentUser.getShortUserName) }
      .getOrElse(FileSystem.get(hadoopConf).getHomeDirectory())
    stagingDirPath = new Path(appStagingBaseDir, getAppStagingDir(appId))
    // scalastyle:on FileSystemGet

    new CallerContext("CLIENT", sparkConf.get(APP_CALLER_CONTEXT),
      Option(appId.toString)).setCurrentContext()

    // Verify whether the cluster has enough resources for our AM
    verifyClusterResources(newAppResponse)

    // Set up the appropriate contexts to launch our AM
    val containerContext = createContainerLaunchContext(newAppResponse)
    val appContext = createApplicationSubmissionContext(newApp, containerContext)

    // Finally, submit and monitor the application
    logInfo(s"Submitting application $appId to ResourceManager")
    yarnClient.submitApplication(appContext)
    launcherBackend.setAppId(appId.toString)
    reportLauncherState(SparkAppHandle.State.SUBMITTED)

    appId
  } catch {
    case e: Throwable =>
      if (stagingDirPath != null) {
        cleanupStagingDir()
      }
      throw e
  }
}

submitApplication()执行了以下步骤:

  1. 初始化YarnClient对象,执行YarnClient方法,提交Application
  2. 创建AM容器启动的上下文环境、启动命令、上传程序包到HDFS
  3. 调用yarnClient的方法,提交创建AM Container请求
  4. 执行APP启动Command bin/java ApplicationMaster –class --jar:根据deployMode不同,启动ApplicationMaster(YarnCluster)和ExecutorLauncher(YarnClient)

ApplicationMaster中的main方法中执行master.run(),这里有个代码表示如下

      if (isClusterMode) {
        runDriver()
      } else {
        runExecutorLauncher()
      }

点进去runDriver()代码中一看

private def runDriver(): Unit = {
  addAmIpFilter(None, System.getenv(ApplicationConstants.APPLICATION_WEB_PROXY_BASE_ENV))
  userClassThread = startUserApplication()

  // This a bit hacky, but we need to wait until the spark.driver.port property has
  // been set by the Thread executing the user class.
  logInfo("Waiting for spark context initialization...")
  val totalWaitTime = sparkConf.get(AM_MAX_WAIT_TIME)
  try {
    val sc = ThreadUtils.awaitResult(sparkContextPromise.future,
      Duration(totalWaitTime, TimeUnit.MILLISECONDS))
    if (sc != null) {
      val rpcEnv = sc.env.rpcEnv

      val userConf = sc.getConf
      val host = userConf.get(DRIVER_HOST_ADDRESS)
      val port = userConf.get(DRIVER_PORT)
      registerAM(host, port, userConf, sc.ui.map(_.webUrl), appAttemptId)

      val driverRef = rpcEnv.setupEndpointRef(
        RpcAddress(host, port),
        YarnSchedulerBackend.ENDPOINT_NAME)
      createAllocator(driverRef, userConf, rpcEnv, appAttemptId, 
}

其中userClassThread = startUserApplication()在单独的userThread线程中启动包含driver的user class。

然后回到ApplicationMaster.runDriver代码中,在创建driver的过程向ResourceManager注册ApplicationMaster,并启动ApplicationMaster。

AM向RM申请Container容器资源,分配资源并在YarnAllocator.runAllocatedContainers启动ExcutorRunner对象

启动ExecutorRunner对象的代码如下:

        if (launchContainers) {
          launcherPool.execute(() => {
            try {
              new ExecutorRunnable(
                Some(container),
                conf,
                sparkConf,
                driverUrl,
                executorId,
                executorHostname,
                containerMem,
                containerCores,
                appAttemptId.getApplicationId.toString,
                securityMgr,
                localResources,
                rp.id
              ).run()

YarnClient模式可自行查阅源码,执行流程和YarnCluster类似,仅最终启动的组件不同(ExecutorLauncher)

SparkContext初始化

Driver启动完成后就是在调用自定义的MainClass方法,比如下面的代码

object WordCount {
  def main(args: Array[String]) {
//    Logger.getLogger("org").setLevel(Level.WARN)

    /**
      * 第1步:创建Spark的配置对象SparkConf,设置Spark程序的运行时的配置信息,
      * 例如说通过setMaster来设置程序要链接的Spark集群的Master的URL,如果设置
      * 为local,则代表Spark程序在本地运行,特别适合于机器配置条件非常差(例如
      * 只有1G的内存)的初学者       *
      */
    val conf = new SparkConf() //创建SparkConf对象
    conf.setAppName("Wow,My First Spark App!") //设置应用程序的名称,在程序运行的监控界面可以看到名称
    conf.setMaster("local[*]") //此时,程序在本地运行,不需要安装Spark集群
    /**
      * 第2步:创建SparkContext对象
      * SparkContext是Spark程序所有功能的唯一入口,无论是采用Scala、Java、Python、R等都必须有一个SparkContext
      * SparkContext核心作用:初始化Spark应用程序运行所需要的核心组件,包括DAGScheduler、TaskScheduler、SchedulerBackend
      * 同时还会负责Spark程序往Master注册程序等
      * SparkContext是整个Spark应用程序中最为至关重要的一个对象
      */
    val sc = new SparkContext(conf) //创建SparkContext对象,通过传入SparkConf实例来定制Spark运行的具体参数和配置信息
    /**
      * 第3步:根据具体的数据来源(HDFS、HBase、Local FS、DB、S3等)通过SparkContext来创建RDD
      * RDD的创建基本有三种方式:根据外部的数据来源(例如HDFS)、根据Scala集合、由其它的RDD操作
      * 数据会被RDD划分成为一系列的Partitions,分配到每个Partition的数据属于一个Task的处理范畴
      */
    val lines = sc.textFile("D:\\programe\\BigDataPrograms\\spark-3.1.3\\spark-3.1.3\\examples\\src\\main\\scala\\org\\apache\\spark\\examples\\my_test\\chapter3_RDD_DataSet\\helloSpark.txt", 1) //读取本地文件并设置为一个Partion
    /**
      * 第4步:对初始的RDD进行Transformation级别的处理,例如map、filter等高阶函数等的编程,来进行具体的数据计算
      * 第4.1步:讲每一行的字符串拆分成单个的单词
      */

    val words = lines.flatMap { line => line.split(" ") } //对每一行的字符串进行单词拆分并把所有行的拆分结果通过flat合并成为一个大的单词集合

    /**
      * 第4步:对初始的RDD进行Transformation级别的处理,例如map、filter等高阶函数等的编程,来进行具体的数据计算
      * 第4.2步:在单词拆分的基础上对每个单词实例计数为1,也就是word => (word, 1)
      */
    val pairs = words.map { word => (word, 1) }

    /**
      * 第4步:对初始的RDD进行Transformation级别的处理,例如map、filter等高阶函数等的编程,来进行具体的数据计算
      * 第4.3步:在每个单词实例计数为1基础之上统计每个单词在文件中出现的总次数
      */
    val wordCountsOdered = pairs.reduceByKey(_ + _, 1000).map(pair => (pair._2, pair._1)).sortByKey(false).map(pair => (pair._2, pair._1)) //对相同的Key,进行Value的累计(包括Local和Reducer级别同时Reduce)
    wordCountsOdered.collect.foreach(wordNumberPair => println(wordNumberPair._1 + " : " + wordNumberPair._2))

    sc.stop()

  }

点击去val sc = new SparkContext(conf)方法的时候,表示开始创建SparkContext环境,后续的什么RDD、累加器和广播变量,所有的执行都是在这里定义的,所以就是非常重要的

sparkEnv环境创建

执行代码

// Create the Spark execution environment (cache, map output tracker, etc)
_env = createSparkEnv(_conf, isLocal, listenerBus)

继续执行SparkEnv.createDriverEnv的代码,其中调用了create方法,接下来讲按顺序讲解里面SparkEnv环境的组件的启动

  1. 创建Driver RPC Endpoint对象,是Driver的RPC通信对象,用于和外部组件通信

    val rpcEnv = RpcEnv.create(systemName, bindAddress, advertiseAddress, port.getOrElse(-1), conf,
                               securityManager, numUsableCores, !isDriver)
    
  2. 创建SerializerManager(默认为JavaSerializer)。为各种 Spark 组件配置序列化、压缩和加密的组件,包括自动选择要用于随机shuffle的Serializer程序。

    val serializerManager = new SerializerManager(serializer, conf, ioEncryptionKey)
    
  3. 创建broadcastManager。用于管理broadcast的过程

    val broadcastManager = new BroadcastManager(isDriver, conf, securityManager)
    
  4. 创建MapOutTrackerMaster。MapOutputTrackerMaster负责Shuffle中数据输出和读取的管理。Shuffle的时候将数据写到本地,下一个Stage要使用上一个Stage的数据, 因此写数据的时候要告诉Driver中的MapOutputTrackerMaster具体写到哪里,下一个Stage读取数据的时候也要访问Driver的MapOutputTrackerMaster 获取数据的具体位置。

    // 如果是driver的话,就创建MapOutputTrackerMaster,否则创建MapOutputTrackerWorker
    val mapOutputTracker = if (isDriver) {
      new MapOutputTrackerMaster(conf, broadcastManager, isLocal)
    } else {
      new MapOutputTrackerWorker(conf)
    }
    
  5. 创建ShuffleManager(默认为sort shuffle)

    // ShuffleManager是一个用于shuffle系统的可插拔接口。在Driver端SparkEnv中创建ShuffleManager,在每个Executor上也会创建。基于spark.shuffle.manager进行设置。
    val shuffleManager = instantiateClass[ShuffleManager](shuffleMgrClass)
    
  6. MemoryManager(默认为UnifiedMemoryManager)

    // 创建memoryManager,是UnifiedMemoryManager
    val memoryManager: MemoryManager = UnifiedMemoryManager(conf, numUsableCores)
    
  7. 创建BlockManagerMaster

    // 创建BlockManagerMasterEndpoint
    // blockManagerMaster主要对内提供各节点之间的指令通信服
    // BlockManagerMaster对整个集群的Block数据进行管理,Block是Spark数据管理的单位,与数据存储没有关系,
    // 数据可能存在磁盘上,也可能存储在内存中,还可能存储在offline,如Alluxio
    val blockManagerMaster = new BlockManagerMaster(
      registerOrLookupEndpoint(
        BlockManagerMaster.DRIVER_ENDPOINT_NAME,
        new BlockManagerMasterEndpoint(
          rpcEnv,
          isLocal,
          conf,
          listenerBus,
          if (conf.get(config.SHUFFLE_SERVICE_FETCH_RDD_ENABLED)) {
            externalShuffleClient
          } else {
            None
          }, blockManagerInfo,
          mapOutputTracker.asInstanceOf[MapOutputTrackerMaster])),
      registerOrLookupEndpoint(
        BlockManagerMaster.DRIVER_HEARTBEAT_ENDPOINT_NAME,
        new BlockManagerMasterHeartbeatEndpoint(rpcEnv, isLocal, blockManagerInfo)),
      conf,
      // isDriver判断是否运行在Driver
      isDriver)
    
  8. 创建BlockManager

    // 创建BlockManager,主要对外提供统一的访问接口
    // 注:blockManager无效,直到initialize()被调用
    // 这里的BlockManagerMaster和BlockManager属于聚合关系
    val blockManager = new BlockManager(
      executorId,
      rpcEnv,
      blockManagerMaster,
      serializerManager,
      conf,
      memoryManager,
      mapOutputTracker,
      // shuffleManager是在SparkEnv中创建的,将shuffleManager传入到BlockManager,BlockManager就拥有shuffleManager的成员变量,
      // 从而可以调用shuffleManager的相关方法
      shuffleManager,
      blockTransferService,
      securityManager,
      externalShuffleClient)
    
  9. 创建blockTransferService。是使用netty来获取blocks的工具

    val blockTransferService =
      new NettyBlockTransferService(conf, securityManager, bindAddress, advertiseAddress,
        blockManagerPort, numUsableCores, blockManagerMaster.driverEndpoint)
    
  10. 除此之外,还创建了OutputCommitCoordinator、outputCommitCoordinatorRef等

上述这些对象将传入new SparkEnv的构造函数返回出来。

TaskScheduler以及SchedulerBackend的初始化

TaskSchedulerImpl是底层的任务调度接口TaskScheduler的实现,这些Schedulers从每一个Stage中的DAGScheduler中获取TaskSet, 运行它们,尝试是否有故障。DAGScheduler是高层调度,它计算每个Job的Stage的DAG, 然后提交Stage,用TaskSets的形式启动底层TaskScheduler调度在集群中运行。

SchedulerBackend负责集群计算资源的管理和调度,这是从作业的角度来考虑的。

回归到SparkContext的代码中,可以看到这个位置创建taskscheduler和schedulerbackend

// Create and start the scheduler
// 创建一个TaskSchedulerImpl和schedulerBackend的实例,
val (sched, ts) = SparkContext.createTaskScheduler(this, master, deployMode)
_schedulerBackend = sched
_taskScheduler = ts

根据不同的maste urlr,会创建不同的scheduler,以5种情况考虑

  1. Local(本地单CPU模式): TaskSchedulerImpl:max_local_task_failures:1(本地最大任务重试)。返回LocalSchedulerBackend:totalCores:1(本地启动cpu核数数量1)
  2. Local_N_REGEX(Local[*]模式): TaskSchedulerImpl :max_local_task_failures:1((本地最大任务重试)。返回LocalSchedulerBackend:threadCount:1(本地启动指定数目CPU/所以可执行cpu)
  3. LOCAL_N_FAILURES_REGEX (Local[n,m]本地失败重试模式): TaskSchedulerImpl :maxFailures:m(本地最大任务失败重试) 。返回LocalSchedulerBackend:threadCount:1(本地启动指定数目CPU/所以可执行cpu)
  4. SPARK_REGEX(StandAlone模式): 返回TaskSchedulerImpl/StandaloneSchedulerBackend
  5. Yarn模式下的TaskScheduler和SchedulerBackend创建 TaskSchedulerImpl:根据master-url(cluster/client)初始化 YarnClient/YarnClusterSchedulerBackend: 根据master url(cluster/client)初始化相应的Backend

创建完之后就会立刻调用scheduler.initialize(backend)的方法,这里以standalone模式来解释

  def initialize(backend: SchedulerBackend): Unit = {
    this.backend = backend
    // 根据调度模式的配置创建实现了schedulableBuilder接口的相应的实例对象
    // 并且创建的对象会立即调用buildPools创建相应数量的Pool存放和管理TaskSetManager的实例对象。
    // 实现SchedulerBuilder接口的具体类都是SchedulerBuilder的内部类。
    schedulableBuilder = {
      // 默认情况FIFO
      schedulingMode match {
        case SchedulingMode.FIFO =>
          new FIFOSchedulableBuilder(rootPool)
        case SchedulingMode.FAIR =>
          new FairSchedulableBuilder(rootPool, conf)
        case _ =>
          throw new IllegalArgumentException(s"Unsupported $SCHEDULER_MODE_PROPERTY: " +
          s"$schedulingMode")
      }
    }
    schedulableBuilder.buildPools()
  }

经过上述的代码,这时候已经创建了taskScheduler以及shcedulerBackend,于是在这里调用_taskScheduler.start()表示启动taskScheduler,创建FIFO(默认模式)/FAIR的taskset资源调度池,这里后续调度taskset任务

创建启动DAGScheduler

DAGScheduler负责高层调度(如Job中Stage的划分、数据本地性等内容)

DAGScheduler是面向Stage调度的高层调度实现。它为每一个Job计算DAG, 跟踪RDDS及Stage输出结果进行物化,并找到一个最小的计划去运行Job, 然后提交stages中的TaskSets到底层调度器TaskScheduler提交集群运行, TaskSet包含完全独立的任务,基于集群上已存在的数据运行(如从上一个Stage输出的文件), 如果这个数据不可用,获取数据可能会失败。

Spark Stages根据RDD图中Shuffle的边界来创建,如果RDD的操作是窄依赖,如map()和filter(), 在每个Stages中将一系列tasks组合成流水线执行。但是,如果是宽依赖,Shuffle依赖需要多个Stages (上一个Stage进行map输出写入文件,下一个Stage读取数据文件),每个Stage依赖于其他的Stage,其中进行多个算子操作。 算子操作在各种类型的RDDS(如MappedRDD、FilteredRDD)的RDD.compute()中实际执行。

DAGScheduler是面向Stage调度的高层调度实现。它为每一个Job计算DAG,跟踪RDDS及Stage输出结果进行物化, 并找到一个最小的计划去运行Job,然后提交stages中的TaskSets到底层调度器TaskScheduler提交集群运行, TaskSet包含完全独立的任务,基于集群上已存在的数据运行(如从上一个Stage输出的文件),如果这个数据不可用,获取数据可能会失败。

执行以下代码_dagScheduler = new DAGScheduler(this),等待后续Job的任务DAG调度

  1. 执行事件处理DAGSchedulerEventProcessLoop线程,主要作用于后续处理DAG切分的核心逻辑
  2. 发送TaskScheduler成功创建心跳到HeartbeatReceiver

启动SchedulerBackend

_taskScheduler.start()

在Standalone环境种调用的是taskSchedulerImpl的start方法。在这个方法中

  1. backend.start()最终注册应用程序AppClient
  2. 根据配置判断是否周期性的检查任务的推测执行

推测任务的执行

speculationScheduler.scheduleWithFixedDelay(
  () => Utils.tryOrStopSparkContext(sc) { checkSpeculatableTasks() },
  SPECULATION_INTERVAL_MS, SPECULATION_INTERVAL_MS, TimeUnit.MILLISECONDS)

对一个Stage里面运行慢的Task,会在其他节点的Executor上再次启动这个task,如果其中一个Task实例运行成功则将这个最先完成的Task的计算结果作为最终结果,同时会干掉其他Executor上运行的实例,从而加快运行速度。

  1. checkSpeculatableTasks检测是否有需要推测式执行的Task, 满足非local模式下开启spark.speculation,开启推测执行,存在则backend调用reviveOffers获取资源运行推测任务。
  2. TaskSetManager.checkSpeculatableTasks当成功的Task数超过总Task数的75% (spark.speculation.quantile: 0.75),再统计任务运行时间中位数乘以1.5(spark.speculation.multiplier)的运行时间阈值,如果超出该阈值则启动推测
  3. 在TasksetManager为下个task分配executor时候dequeueTask()中启用调度检测,先过滤掉已经成功执行的task,另外,推测执行task不在和正在执行的tasks同一Host执行,不在黑名单executor里执行。

schedulerBackend.start()执行流程

以Standalone模式讲解

Master发指令给Worker去启动Executor所有的进程时加载的Main方法所在的入口类就是command中的CoarseGrainedExecutorBackend, 在CoarseGrainedExecutorBackend中启动Executor(Executor是先注册,再实例化),Executor通过线程池并发执行Task,然后再调用它的run方法。

// 构建了一个command对象
// 将command封装注册给Master,Master转过来要Worker启动具体的Executor。
// command已经封装好指令,Executor具体要启动进程入口类CoarseGrainedExecutorBackend
val command = Command("org.apache.spark.executor.CoarseGrainedExecutorBackend",
                      args, sc.executorEnvs, classPathEntries ++ testingClassPath, libraryPathEntries, javaOpts)

获取executor_cores的参数,就是每个executor多少个cores

val coresPerExecutor = conf.getOption(config.EXECUTOR_CORES.key).map(_.toInt)

将各种运行信息封装成ApplicationDescription

val appDesc = ApplicationDescription(sc.appName, maxCores, sc.executorMemory, command,
  webUrl, sc.eventLogDir, sc.eventLogCodec, coresPerExecutor, initialExecutorLimit,
  resourceReqsPerExecutor = executorResourceReqs)

创建StandaloneAppCient

// 创建一个很重要的对象,然后调用它的client.start方法
// 这个携带了command信息,command信息中指定了要启动的ExecutorBackend的实现类
client = new StandaloneAppClient(sc.env.rpcEnv, masters, appDesc, this, conf)

最后调用

client.start()

这个是一个RpcEndPoint,首先调用自己的onStart方法,接下来向Master注册。

blockManager初始化

// 初始化blockManager
_env.blockManager.initialize(_applicationId)

进入具体初始化方法

  def initialize(appId: String /** 应用id **/): Unit = {
    // 调用blockTransferService的init方法,blockTransferService用于在不同节点fetch数据,传送数据
    // 初始化BlockTransferService
    blockTransferService.init(this)
    externalBlockStoreClient.foreach { blockStoreClient =>
        // 用于读取其他Executor上的shuffle files
        // 初始化ShuffleClient
      blockStoreClient.init(appId)
    }
    blockReplicationPolicy = {
      val priorityClass = conf.get(config.STORAGE_REPLICATION_POLICY)
      val clazz = Utils.classForName(priorityClass)
      val ret = clazz.getConstructor().newInstance().asInstanceOf[BlockReplicationPolicy]
      logInfo(s"Using $priorityClass for block replication policy")
      ret
    }

    // 创建BlockManagerId
    val id =
      BlockManagerId(executorId, blockTransferService.hostName, blockTransferService.port, None)
    // 向blockManagerMaster注册BlockManager,在registerBlockManager方法中传入了slaveEndpoint,slaveEndpoint为BlockManager
    // 中的RPC对象,用于和blockManagerMasater通信
    val idFromMaster = master.registerBlockManager(
      id,
      diskBlockManager.localDirsString,
      maxOnHeapMemory,
      maxOffHeapMemory,
      storageEndpoint )  // 这个消息循环体来接收Driver中BlockManagerMaster的信息
    // 得到shuffleManageId
    blockManagerId = if (idFromMaster != null) idFromMaster else id
    // 得到shuffleServerId
    shuffleServerId = if (externalShuffleServiceEnabled) {
      logInfo(s"external shuffle service port = $externalShuffleServicePort")
      BlockManagerId(executorId, blockTransferService.hostName, externalShuffleServicePort)
    } else {
      blockManagerId
    }

    // Register Executors' configuration with the local shuffle service, if one should exist.
    // 注册shuffleserver
    // 如果存在,将注册Executors配置与本地shuffle服务
    if (externalShuffleServiceEnabled && !blockManagerId.isDriver) {
      registerWithExternalShuffleServer()
    }

    hostLocalDirManager = {
      if (conf.get(config.SHUFFLE_HOST_LOCAL_DISK_READING_ENABLED) &&
          !conf.get(config.SHUFFLE_USE_OLD_FETCH_PROTOCOL)) {
        Some(new HostLocalDirManager(
          futureExecutionContext,
          conf.get(config.STORAGE_LOCAL_DISK_BY_EXECUTORS_CACHE_SIZE),
          blockStoreClient))
      } else {
        None
      }
    }

    logInfo(s"Initialized BlockManager: $blockManagerId")
  }
  1. 首先是调用blockTransferService.init方法,blockTransferService用于在不同节点fetch数据,传送数据
  2. 然后遍历exterlBlockStoreClient,初始化blockStoreClient
  3. 创建BlockManagerId
  4. val idFromMaster = master.registerBlockManager,向blockManagerMaster注册BlockManager,在registerBlockManager方法中传入了slaveEndpoint,slaveEndpoint为BlockManager中的RPC对象,用于和blockManagerMasater通信
  5. 创建shuffleServerId
  6. 注册shuffleServer
  7. 最后是创建hostLocalDirManager

由此,blockManager初始化成功

激活SparkContext

SparkContext.setActiveContext(this)

将当前SparkContext的状态从contextBeingConstructed(正在构建中)改为activeContext(已激活)

Executor启动

当在上文调用schedulerBackend.start()的时候,实际上就是向Master注册RegisterApplication,并携带要启用的worker信息,在Masetr中有一段代码表示如下

case RegisterApplication(description, driver) =>
  // TODO Prevent repeated registrations from some driver
  if (state == RecoveryState.STANDBY) {
    // ignore, don't send response
  } else {
    logInfo("Registering app " + description.name)
    // 创建ApplicationInfo
    val app = createApplication(description, driver)
    // 完成application的注册
    registerApplication(app)
    logInfo("Registered app " + description.name + " with ID " + app.id)
    // 通过持久化引擎(如ZooKeeper)把注册信息持久化
    persistenceEngine.addApplication(app)
    // 给driver发送RegisteredApplication的信息
    driver.send(RegisteredApplication(app.id, self))
    schedule()
  }

将description从SchedulerBackend发送到Master的时候,Master,完成application的注册,并且给driver发送RegisteredApplication的信息。最后调用shedule()开始调度,里面有个方法叫做startExecutorsOnWorkers()就是启动executor,调度方式默认的是FIFO的模式

CoarseGrainedExecutorBackend运行

刚才Master已经调度了executor的启动,接下来,CoarseGrainedExecutorBackendk开始运行。

在其onStart方法中,向Driver发送ask注册请求

// 向Driver发送ask请求,等待Driver回应
ref.ask[Boolean](RegisterExecutor(executorId, self, hostname, cores, extractLogUrls,
  extractAttributes, _resources, resourceProfile.id))

Driver的CoarseGrainedScheduleBackend接收请求并注册Executor

如果注册Executor成功,则给自己发送RegisteredExecutor,并且在receive接收到RegisteredExecutor的信息,立即创建Executor

executor = new Executor(executorId, hostname, env, userClassPath, isLocal = false,
                        resources = _resources)
driver.get.send(LaunchedExecutor(executorId))

如果创建executer成功了,就像driver发送LauchedExecutor启动的信息

在Driver中,接受LauchedExecutor启动的信息,并且调用makeOffers(executorId)等待后续分配taskset给Executor。

Task启动

Task任务切分

在完成SparkContext初始化和Executor启动后,这里还是回到我们提交的任务Main方法中。

wordCountsOdered.collect.foreach(wordNumberPair => println(wordNumberPair._1 + " : " + wordNumberPair._2))

这里有个collect的方法,表示收集所有的wordCountsOrdered,收集到driver端中。进入这个方法里面,可以看到里面执行的是Action算子,在其中执行sc.runJob。

  def runJob[T, U: ClassTag](
      rdd: RDD[T],
      func: (TaskContext, Iterator[T]) => U,
      partitions: Seq[Int],
      resultHandler: (Int, U) => Unit): Unit = {
    if (stopped.get()) {
      throw new IllegalStateException("SparkContext has been shutdown")
    }
    val callSite = getCallSite
    val cleanedFunc = clean(func)
    logInfo("Starting job: " + callSite.shortForm)
    if (conf.getBoolean("spark.logLineage", false)) {
      logInfo("RDD's recursive dependencies:\n" + rdd.toDebugString)
    }
    // dagScheduler出现了,可以切分stage
    dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, resultHandler, localProperties.get)
    progressBar.foreach(_.finishAll())
    rdd.doCheckpoint()
  }

注意里面执行了dagScheduler.runJob,并在内部执行了submitJob来提交任务。主要就是用来切分stage的了。

submitJob方法调用eventProcessLoop的post方法,调用eventProcessLoop post将JobSubmitted事件添加到DAGScheduler事件队列,给自己发送一个提交任务的作业。

定位到DAGscheduler的doOnReceive中,可以看到DagScheduler接受了JobSubmitted的任务处理,在中间调用

dagScheduler.handleJobSubmitted(jobId, rdd, func, partitions, callSite, listener, properties)

来处理JobSubmitted的任务。

private[scheduler] def handleJobSubmitted(jobId: Int,
    finalRDD: RDD[_],
    func: (TaskContext, Iterator[_]) => _,
    partitions: Array[Int],
    callSite: CallSite,
    listener: JobListener,
    properties: Properties): Unit = {
  var finalStage: ResultStage = null
  try {
    // New stage creation may throw an exception if, for example, jobs are run on a
    // HadoopRDD whose underlying HDFS files have been deleted.
    // 如果作业运行在HadoopRDD上,而底层HDFS 的文件已被删除,那么在创建新的Stage
    // 时将会跑出一个异常
    // 通过createResultStage创建finalStage,传入的参数包括最后一个finalRDD,
    // 操作的函数func,分区partitions、jobId、
    // callSite等内容。创建过程中可能捕获异常。例如,
    // 在Hadoop上,底层的hdfs文件被删除了或者被修改了,就出现异常。
    finalStage = createResultStage(finalRDD, func, partitions, jobId, callSite)
  } catch {
    case e: BarrierJobSlotsNumberCheckFailed =>
      // If jobId doesn't exist in the map, Scala coverts its value null to 0: Int automatically.
      // 如果jobId在映射中不存在,scala会自动将其值nul1转换为0:Int
      val numCheckFailures = barrierJobIdToNumTasksCheckFailures.compute(jobId,
        (_: Int, value: Int) => value + 1)

      logWarning(s"Barrier stage in job $jobId requires ${e.requiredConcurrentTasks} slots, " +
        s"but only ${e.maxConcurrentTasks} are available. " +
        s"Will retry up to ${maxFailureNumTasksCheck - numCheckFailures + 1} more times")

      if (numCheckFailures <= maxFailureNumTasksCheck) {
        messageScheduler.schedule(
          new Runnable {
            override def run(): Unit = eventProcessLoop.post(JobSubmitted(jobId, finalRDD, func,
              partitions, callSite, listener, properties))
          },
          timeIntervalNumTasksCheck,
          TimeUnit.SECONDS
        )
        return
      } else {
        // Job failed, clear internal data.
        // Job失败,清理内部数据
        barrierJobIdToNumTasksCheckFailures.remove(jobId)
        listener.jobFailed(e)
        return
      }

    case e: Exception =>
      logWarning("Creating new stage failed due to exception - job: " + jobId, e)
      listener.jobFailed(e)
      return
  }
  // Job submitted, clear internal data.
  // 提交作业,清除内部数据
  barrierJobIdToNumTasksCheckFailures.remove(jobId)

  val job = new ActiveJob(jobId, finalStage, callSite, listener, properties)
  clearCacheLocs()
  logInfo("Got job %s (%s) with %d output partitions".format(
    job.jobId, callSite.shortForm, partitions.length))
  logInfo("Final stage: " + finalStage + " (" + finalStage.name + ")")
  logInfo("Parents of final stage: " + finalStage.parents)
  // 根据finalStage找父Stage,如果有父Stage,就直接返回,如果没有父Stage,就进行创建
  logInfo("Missing parents: " + getMissingParentStages(finalStage))

  val jobSubmissionTime = clock.getTimeMillis()
  jobIdToActiveJob(jobId) = job
  activeJobs += job
  finalStage.setActiveJob(job)
  val stageIds = jobIdToStageIds(jobId).toArray
  val stageInfos = stageIds.flatMap(id => stageIdToStage.get(id).map(_.latestInfo))
  listenerBus.post(
    SparkListenerJobStart(job.jobId, jobSubmissionTime, stageInfos,
      Utils.cloneProperties(properties)))
  // 以递归的方式提交stage
  submitStage(finalStage)
}

handleJobSubmitted代码处理流程如下:

  1. 触发job的最后一个rdd,创建finalStage并同时创建shuffleMapStage

  2. 用finalStage创建一个Job,这个job的最后一个stage,就是finalStage

  3. 将Job相关信息,加入内存缓冲中

  4. 第四步,使用submitStage方法提交finalStage,这是以递归的方式进行的。

    private def submitStage(stage: Stage): Unit = {
        // activeJobForStage中获得JobID
        val jobId = activeJobForStage(stage)
        // 如果已经定义isDefined,那就获得即将计算的Stage(getMissingParentStages),然后进行升序排列
        if (jobId.isDefined) {
          logDebug(s"submitStage($stage (name=${stage.name};" +
            s"jobs=${stage.jobIds.toSeq.sorted.mkString(",")}))")
          if (!waitingStages(stage) && !runningStages(stage) && !failedStages(stage)) {
    //        获得即将计算的Stage,然后进行升序排序
            val missing = getMissingParentStages(stage).sortBy(_.id)
            logDebug("missing: " + missing)
            // 已经没有missing的parents了
            if (missing.isEmpty) {
              logInfo("Submitting " + stage + " (" + stage.rdd + "), which has no missing parents")
              // 提交任务,将每一个Stage和jobId传入
              submitMissingTasks(stage, jobId.get)
            } else {
              // 如果父Stage不为空,将循环递归调用submitStage,从后往前回溯,也就是说,只有前面的依赖的Stage计算完毕后,
              // 后面的Stage才会运行。submitStage一直循环调用,导致的结果是父Stage的父Stage……一直回溯到最左侧的父Stage开始计算。
              for (parent <- missing) {
                submitStage(parent)
              }
              waitingStages += stage
            }
          }
        } else {
          abortStage(stage, "No active job for stage " + stage.id, None)
        }
      }
    

过程如下:

  1. 获取final RDD的shuffleDependies,遍历调用查找finalStage的父stage
  2. 调用getMissingParentStage查找finalStage的父stage(根据rdd的dependies判断, 如果是shuffleDependency宽依赖则生成stage, Narrow窄依赖则继续压入栈中继续向上遍历),最后返回stage列表
  3. 如果存在父stage, 则递归调用submitStage(如果一直存在则递归直到stage0), 将当前stage加入waitingStage待提交;
  4. 如果不存在stage, 则直接提交stage中未提交的tasks(submitMissingTasks)
  5. 后续submitMissingTask, 为stage创建一批tasks,数量等同于partitions(final RDD的); 计算每个task对应的Partition最佳位置
  6. 对于stage的task,创建taskset对象,调用TaskSchduler的submitTasks方法

根据上述的过程,这里是递归获取parent依赖,如果已经没有parent依赖了,就开始submitMissingTask,中间过程如下:

  1. 获取当前stage没有计算的partitions和properities

    1. 如果是shuffleMapStage,调用MapOutputTrackerMaster的findMissingpartitions方法查找MapOutputTracker中需要参与计算的该stage的partitionIds
    2. 如果是ResultStage, 获取当前job中未计算的partitionId
  2. 将stage添加到runningStage中

  3. 匹配stage类型,获取task对应partition的最优资源位置来运行job(查看缓存cache中内存->查找BlockManager存储优先级别)

  4. 根据stage类型不同封装task, 传递给TaskScheduler调用task

    /**
           * 创建了一个TaskSet对象,将所有任务的信息封装,包括task任务列表,stageId,任务id,分区数参数等
           */
    taskScheduler.submitTasks(new TaskSet(
      tasks.toArray, stage.id, stage.latestInfo.attemptNumber, jobId, properties,
        stage.resourceProfileId))
    

此时已经把stage切分成很多个task的了,点进去taskScheduler.submitTasks的方法中,代码表述如下

override def submitTasks(taskSet: TaskSet): Unit = {
  val tasks = taskSet.tasks
  logInfo("Adding task set " + taskSet.id + " with " + tasks.length + " tasks "
    + "resource profile " + taskSet.resourceProfileId)
  this.synchronized {
    // 创建TaskSetManager保存了taskSet任务列表
    // TaskSetManager对其生命周期进行管理,当TaskSchedulerImpl得到Worker节点上的Executor计算资源的时候,
    // 会通过TaskSetManager发送具体的Task到Executor上执行计算。
    val manager = createTaskSetManager(taskSet, maxTaskFailures)
    val stage = taskSet.stageId
    val stageTaskSets =
      taskSetsByStageIdAndAttempt.getOrElseUpdate(stage, new HashMap[Int, TaskSetManager])

    // 在添加新任务时,将此阶段的所有现有任务集管理器TaskSetManagers 标记为zombi,
    // 这是处理极端情况所必需的。假设一个 stage 有 10个分区,
    // 2个分区任务集管理器TaskSetManagers:TSM1(zombie)和 TSM2(active)。
    // TSM1的10个分区的任务都运行完成了。TSM2 完成了分区 1-9 的任务,
    // 认为它仍然活跃,因为分区 10 还没有完成。但是,DAGScheduler
    // 获取到所有 10 个分区完成任务的事件,并认为该阶段已完成。
    // 如果是洗牌阶段shuffle不知何故缺少映射输出,DAGScheduler
    // 将重新提交它并创建一个TSM3。由于一个阶段不能有多个活动任务集管理器,
    // 因此必须标记TSM2是zombie(实际上是)。
    stageTaskSets.foreach { case (_, ts) =>
      ts.isZombie = true
    }
    // 将任务加入调度池
    stageTaskSets(taskSet.stageAttemptId) = manager
    // 创建了TaskSetManager后,非常关键的一行是这个代码
    // SchedulableBuilder会确定TaskSetManager的调度顺序,
    // 然后按照TaskSetManager的locality aware来确定每个Task具体运行在哪个ExecutorBackend中。
    //
    // 两种调度模式FIFOSchedulableBuilder、FairSchedulableBuilder
    // 这里的调度策略可以通过spark-env.sh中的spark.sheduler.mode进行具体设置,默认是FIFO的方式
    schedulableBuilder.addTaskSetManager(manager, manager.taskSet.properties)

    if (!isLocal && !hasReceivedTask) {
      starvationTimer.scheduleAtFixedRate(new TimerTask() {
        override def run(): Unit = {
          if (!hasLaunchedTask) {
            logWarning("Initial job has not accepted any resources; " +
              "check your cluster UI to ensure that workers are registered " +
              "and have sufficient resources")
          } else {
            this.cancel()
          }
        }
      }, STARVATION_TIMEOUT_MS, STARVATION_TIMEOUT_MS)
    }
    hasReceivedTask = true
  }
  // 接受任务,这里的backend是coarseGrainedSchedulerBackend一个Executor任务调度对象
  backend.reviveOffers()
}

代码主要完成了以下操作

  1. 把task封装到TaskSetManager,并且放入到了调度器
  2. 执行backend.reviveOffers(),调用CoarseGrainedSchedulerBackend的reviveOffers进行任务分配(executor最优位置分配),然后把ReviveOffers内部消息发回给自己

driver接受到ReviveOffers之后就调用makeOffers()方法,然后执行启动tasks,launchTasks(scheduler.resourceOffers(workOffers)),这些task已经在前面确定好运行在那个ExecutorBackend上。在内部将task序列化然后发送LaunchTask(data)信息给已经选定的CoarseGrainedExecutorBackend。

随后CoarseGrainedExecutorBackend解析task任务信息,并且启动executor.launchTask(this, taskDesc)

def launchTask(context: ExecutorBackend, taskDescription: TaskDescription): Unit = {
  // 调用TaskRunner句柄创建TaskRunner对象
  // TaskRunner是一个Runnable,里面的run方法种包括任务的反序列化等内容,通过Runnable封装任务,然后放入到runningTasks中,
  val tr = new TaskRunner(context, taskDescription, plugins)
  // 将创建的TaskRunner对象放入即将进行的堆栈中
  runningTasks.put(taskDescription.taskId, tr)
  // 从线程池中分配一条线程给TaskRunner,是一个newDaemonCachedThreadPool,任务交给Executor的线程池中的线程去执行,执行
  // 的时候下载资源、数据等内容
  threadPool.execute(tr)
  if (decommissioned) {
    log.error(s"Launching a task while in decommissioned state.")
  }
}

在方法内部创建TaskRunner对象,然后把创建的TaskRunner对象放入到即将进行的堆栈中。从线程池中分配一条线程给TaskRunner,是一个newDaemonCachedThreadPool,任务交给Executor的线程池中的线程去执行。

至此,task任务的划分和分配已经完成,下面就是task任务在executor的启动执行过程

Task任务执行

已知道TaskRunner继承了Runnable对象,所以在里面实现了run方法。

点进去方法看一下,主要实现了

  1. 初始化Task线程环境和TaskMemoryManger等组件
  2. 对序列化的task数据进行反序列化
  3. 远程网络通信拉取文件(文件、资源、jar等)
  4. 调用task.run方法,在方法内部调用了runTask(context)。runTask方法内部回调用RDD的iterator()方法,该方法就是针对前Task对应的Partition进行计算的关键所在,在处理的方法内部回迭代Partition的元素,并交给自定以的function进行处理。注意,这里的runTask有两种实现,一种是ShuffleMapTask、另外一种是ResultTask

ShufleMapTask任务实现

在ShuffleMapTask的实现中,首先,ShuffleMapTask会反序列化RDD及其依赖关系,然后通过调用RDD的iterator方法进行计算, 而iterator方法中进行的最终运算的方法是compute()。

  override def runTask(context: TaskContext): MapStatus = {
    // Deserialize the RDD using the broadcast variable.
    // 使用广播变量反序列化RDD
    val threadMXBean = ManagementFactory.getThreadMXBean
    val deserializeStartTimeNs = System.nanoTime()
    val deserializeStartCpuTime = if (threadMXBean.isCurrentThreadCpuTimeSupported) {
      threadMXBean.getCurrentThreadCpuTime
    } else 0L
    // 创建序列化器
    val ser = SparkEnv.get.closureSerializer.newInstance()
    // 反序列化出RDD和依赖关系
    val rddAndDep = ser.deserialize[(RDD[_], ShuffleDependency[_, _, _])](
      ByteBuffer.wrap(taskBinary.value), Thread.currentThread.getContextClassLoader)
    // RDD反序列化的时间
    _executorDeserializeTimeNs = System.nanoTime() - deserializeStartTimeNs
    _executorDeserializeCpuTime = if (threadMXBean.isCurrentThreadCpuTimeSupported) {
      threadMXBean.getCurrentThreadCpuTime - deserializeStartCpuTime
    } else 0L

    val rdd = rddAndDep._1
    val dep = rddAndDep._2
    // While we use the old shuffle fetch protocol, we use partitionId as mapId in the
    // ShuffleBlockId construction.
    val mapId = if (SparkEnv.get.conf.get(config.SHUFFLE_USE_OLD_FETCH_PROTOCOL)) {
      partitionId
    } else context.taskAttemptId()
    // 用来将结果写入Shuffle管理器
    dep.shuffleWriterProcessor.write(rdd, dep, mapId, context, partition)
  }

可以见到,首先反序列化出RDD和依赖关系,然后调用 dep.shuffleWriterProcessor.write(rdd, dep, mapId, context, partition)来最终执行你定义的RDD算子func,点进去看一下

// 将计算结果通过writer对象的write方法写出
writer.write(
  // 会调用RDD的iterator,然后针对partition进行计算
  rdd.iterator(partition, context).asInstanceOf[Iterator[_ <: Product2[Any, Any]]])

ShuffleMapTask在write写出map信息的时候,在内部调用了RDD的iterator,针对partition进行计算

final def iterator(split: Partition, context: TaskContext): Iterator[T] = {
  // 判断此RDD的持久化等级是否为None,(不进行持久化)
  if (storageLevel != StorageLevel.NONE) {
    // iterator方法中进行的最终运算方法是compute
    // RDD的compute方法是一个抽象方法,每个RDD都需要重写的方法。
    getOrCompute(split, context)
  } else {
    computeOrReadCheckpoint(split, context)
  }
}

在这里根据RDD是否为持久化来执行

private[spark] def computeOrReadCheckpoint(split: Partition, context: TaskContext): Iterator[T] =
{ // 如果RDD进行了checkpoint,则从父RDD的iterator中直接获取数据
  if (isCheckpointedAndMaterialized) {
    firstParent[T].iterator(split, context)
  } else {
    // 没有checkpoint,则重新计算RDD的数据
    // 具体计算有具体的RDD,如果MapPartitionsRDD的compute,传进去的Partition及TaskContext上下文
    compute(split, context)
  }
}

如果RDD进行了checkpoint,则从父RDD的iterator中直接获取数据,而如果没有checkpoint,则通过compute来执行自己定义的算子方法。

返回到ShuffleMapTask的writer.write方法中,实际上这里就是shuffle的过程了,这里可以看我写的一篇shuffle过程文章【读懂面经中的源码】SPARK源码解析——shuffle过程_番茄薯仔的博客-CSDN博客,中间的过程也是非常的刺激好玩~

ResultTask

override def runTask(context: TaskContext): U = {
  // Deserialize the RDD and the func using the broadcast variables.
  // 使用广播变量反序列化RDD及函数
  val threadMXBean = ManagementFactory.getThreadMXBean
  val deserializeStartTimeNs = System.nanoTime()
  val deserializeStartCpuTime = if (threadMXBean.isCurrentThreadCpuTimeSupported) {
    threadMXBean.getCurrentThreadCpuTime
  } else 0L
  /// 创建序列化器
  val ser = SparkEnv.get.closureSerializer.newInstance()
  // 反序列RDD和func处理函数
  val (rdd, func) = ser.deserialize[(RDD[T], (TaskContext, Iterator[T]) => U)](
    ByteBuffer.wrap(taskBinary.value), Thread.currentThread.getContextClassLoader)
  _executorDeserializeTimeNs = System.nanoTime() - deserializeStartTimeNs
  _executorDeserializeCpuTime = if (threadMXBean.isCurrentThreadCpuTimeSupported) {
    threadMXBean.getCurrentThreadCpuTime - deserializeStartCpuTime
  } else 0L

  func(context, rdd.iterator(partition, context))
}

这里反序列化的func处理函数,最终调用func(context, rdd.iterator(partition, context))来执行自定义RDD算子方法

至此,Spark任务提交、调度、执行过程就全部全部梳理一遍

总结

本篇Spark源码解读很大一部分参考这篇文章,可以说从头带着我读完了Spark启动流程的源码,受益匪浅!!!

因为读源码中间过程代码量巨大,在提交、调度、执行的过程每个环节都有其非常复杂的实现,篇幅有限则无法深入到具体的实现(比如递归切分stage、比如如何获取内存block,这些具体的理解太过于缺乏了),只能浮在表面去解释。

但这也是一个契机,在我接下来的文章会尝试深入非常具体的实现,希望因此能够提升自我、帮助到各位读者同学!!!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值