Spark源码分析之AM端运行流程(Driver)

文章目录

  • 先验知识
  • Yarn启动AM流程
  • AM启动Driver流程
  • AM申请Executors流程

先验知识

接之前文章 Spark源码分析之任务提交流程 介绍了Client提交Spark任务的源码分析过程。本文继续分析ApplicationMaster的启动流程(源码Hadoop2.7.1),首先给出Client的提交的一些先决条件如下:
提交命令:

spark-submit --master yarn \
--deploy-mode cluster \
--class org.apache.spark.examples.SparkPi \
/usr/local/spark-2.4.3-bin-hadoop2.7/examples/jars/spark-examples_2.11-2.4.3.jar 100000

由之前文章可知,最终是由SparkSubmit类向Yarn集群提交任务,首先会把任务依赖的文件上传hdfs,然后生成Yarn提交上下文参数,通过RPC方式向Yarn的Resource Manager提交任务,其中提交上下文参数如下:

提交给HDFS的文件如下图
在这里插入图片描述

Yarn启动AM流程

SparkSubmit最终会调用Yarn Client通过RPC方式提交给Yarn的RM,提交给Yarn的RM后会首先申请一个Container(其实质就是ApplicationMaster),并与之NodeManager建立联系,NodeManager先进行资源本地化,然后在工作目录下生成并调用 default_container_executor.sh -> default_container_executor_session.sh -> launch_container.sh 启动JVM进程用于执行AM(Yarn集群的调度原理见 Yarn源码分析之集群启动流程 、Yarn源码分析之事件模型 和 Yarn源码分析之状态机机制 )。其中NodeManager进行资源本地化后的磁盘目录如下图(假设yarn配置的参数 yarn.nodemanager.local-dirs=/var/lib/hadoop-yarn/cache/yarn/):

于测试集群是单节点的,所以上图包含三个容器container_1587104773637_0002_01_000001container_1587104773637_0002_01_000002container_1587104773637_0002_01_000003,第一个容器用于运行Driver容器,后两个容器用于运行Executor容器。NodeManager的工作目录为 cd /tmp/hadoop-root/nm-local-dir/usercache/root/appcache/application_1587104773637_0002/container_1587104773637_0002_01_000001
重点关注两个文件launch_container.sh__spark_conf__.properties

  • launch_container.sh文件的主要作用为设置AM启动环境变量和NM通过sh启动JVM运行AM;
  • __spark_conf__.properties则是AM启动Driver和Executor的配置参数。

上图给出文件供参考(由于jar文件过多过大,删除了解压到nm-local-dir/usercache/root/filecache/13/__spark_libs__3004195479466524435.zip目录中的所有jar包)。

单独列出launch_container.sh供参考,可以看出AM的入口类 org.apache.spark.deploy.yarn.ApplicationMaster

AM启动Driver流程

由上节可知,NodeManager启动AM的入口类是org.apache.spark.deploy.yarn.ApplicationMaster,其启动过程会通过伴生类的main作为入口,代码分析直接见注释,如下图:

然后代码继续执行 ApplicationMaster.run() -> ApplicationMaster.runDriver(),最终在runDiver()做Driver的初始化工作,代码分析见注释如下:

如上图,我们也重点分析两部分:AM如何启动Driver程序和AM主线程如何获得子线程中SparkContext的初始化。Driver程序的启动是在userClassThread = startUserApplication()函数完成的,如下图,首先获得提交参数中定义的--class,反射并实例化运行初始化SparkContext(即Driver),如下图:

然后我们回到runDriver()看AM主线程如何获得子线程中SparkContext的初始化,答案就是采用了Promise机制,下面截取关键处代码:

private[spark] class ApplicationMaster(
  ...
  // In cluster mode, used to tell the AM when the user's SparkContext has been initialized.
  private val sparkContextPromise = Promise[SparkContext]()
  private def sparkContextInitialized(sc: SparkContext) = {
    sparkContextPromise.synchronized {
      // Notify runDriver function that SparkContext is available
      sparkContextPromise.success(sc)
      // Pause the user class thread in order to make proper initialization in runDriver function.
      sparkContextPromise.wait()
    }
  }

 private def startUserApplication(): Thread = {
    logInfo("Starting the user application in a separate Thread")
    ...
    val mainMethod = userClassLoader.loadClass(args.userClass)
      .getMethod("main", classOf[Array[String]])

    val userThread = new Thread {
      override def run(): Unit = {
        try {
          if (!Modifier.isStatic(mainMethod.getModifiers)) {
            ...
          } else {
            mainMethod.invoke(null, userArgs.toArray)
            ...
          }
        } catch {
          ...
            sparkContextPromise.tryFailure(e.getCause())
        } finally {
          sparkContextPromise.trySuccess(null)
        }
      }
    }
    userThread.setContextClassLoader(userClassLoader)
    userThread.setName("Driver")
    userThread.start()
    userThread
  }

  private def runDriver(): Unit = {
    //开辟线程,加载用户定义--class函数,即Driver
    userClassThread = startUserApplication()
    ...
    try {
      //等待用户定义Driver完成SparkContext的初始化完成
      val sc = ThreadUtils.awaitResult(sparkContextPromise.future,
        Duration(totalWaitTime, TimeUnit.MILLISECONDS))
    ...
  }

如上代码,关键就是val sc = ThreadUtils.awaitResult(sparkContextPromise.future, Duration(totalWaitTime, TimeUnit.MILLISECONDS)),此函数会阻塞在超时时间内等待sparkContextPromise的Future对象返回SparkContext实例。其原理是先在ApplicationMaster类中定义了变量private val sparkContextPromise = Promise[SparkContext](),这样在userClassThread = startUserApplication()中开辟的子线程中在SparkContext的初始化后会调用hook,进而通知主线程完成SparkContext的初始化并赋值返回,过程如下图:

AM申请Executors流程

runDriver()函数中完成sparkContext的初始化后,紧接着就将AM注册到RM,并根据driver的host、port和rpc server名称YarnSchedulerBackend.ENDPOINT_NAME获取到driver的EndpointRef对象driverRef,用于AM与Driver通信;同时传递driverRef给Executor用于executor与driver之间进行rpc通信,最后通过调用createAllocator(driverRef, userConf, rpcEnv, appAttemptId, distCacheConf),向RM申请Container资源,并启动Executors,代码如下:

下面给出打印上下文参数示例:

如果申请分配了多个executor容器,提交上下文参数类似,如果区别是--executor-id参数不同。
如图可以看出向Yarn申请Executors与申请Driver容器的过程类似,区别在于Executor的入口类为org.apache.spark.executor.CoarseGrainedExecutorBackend

你可能感兴趣的:(大数据:Spark)