读前准备
本文档旨在帮助新人更好地学习Apache Spark源码,在阅读文档之前,需要读者掌握以下前置知识:
- 明白driver,executor等Spark中的基本概念,知道YARN的RM,NM,AM各有什么作用
- 了解Spark的Local,Standalone,YARN client,YARN cluster等部署模式的概念
相关信息可以查阅Apache官方文档:- Cluster Mode Overview - Spark Documentation
- Running Spark on YARN - Spark Documentation
- 文档以一个简单的Spark作业codelab/wordCount为例,展示了用infra-client在YARN cluster模式下提交Spark作业的流程.需要读者掌握:
- codelab/spark/scala/wordCount的代码
tips
- 建议新人学习时多思考,针对源码多提问题,带着问题去看源码,这样会事半功倍,能加深对源码的理解. 这是我看源码时的部分问题,以供参考:
- 利用infra-client提交作业和使用原生Spark的./bin/spark-submit脚本提交有何不同,infra-client帮我们做了什么?
- 命令行参数和spark-default.conf中的参数是如何层层传递到driver和executor.这些参数都在哪里被使用?
- YARN cluster和YARN client模式有何不同,ApplicationMaster在这两种模式下分别什么时候启动?
- 可以参照命令提交作业,之后对照Spark UI上的日志去学习源码,能更好地定位代码的分支跳转等情况,关于如何查看Spark UI请参考链接Spark UI&日志
流程介绍
如下图所示,将Spark作业以YARN cluster模式提交到YARN集群大致分为以下几步:
执行spark-submit脚本,启动Spark客户端
向YARN RM提交作业,RM启动ApplicationMaster
AM在一个新线程里启动driver
AM向RM注册,提交资源请求,RM启动executors
executors向driver注册,结束Spark框架的初始化
文档会以wordCount为例,介绍整个流程的具体过程并解释关键节点.
首先,在idc执行这样一条命令:
$INFRA_CLIENT/bin/spark-submit
--cluster c3prc-hadoop-spark2.1
--class com.xiaomi.infra.codelab.spark.WordCount
--master yarn-cluster
--num-executors 3
--driver-memory 4g
--executor-memory 2g
--executor-cores 1
-queue root.production.cloud_group.hadoop.queue_1
--conf spark.yarn.job.owners=chenfan
~/gitRepo/codelab/spark/scala/wordcount/target/spark-scala-wordcount-1.0-SNAPSHOT.jar
hdfs://c3prc-hadoop/tmp/s_spark_tst/input.dat
hdfs://c3prc-hadoop/tmp/s_spark_tst/output
其中$INFRA_CLIENT是本地infra-client的路径, --cluster是infra-client的参数用以制定作业提交的集群,--class是作业的主类,之后是Spark的参数,它们的作用详见官方文档Configurtion - Spark Documentation,在之后是作业的jar包,最后两行是作业主类WordCount#main的参数,有时也会称作业代码为用户代码,以便于和框架代码作区分.
第一步: 脚本启动Spark框架的客户端
infra-client提供的脚本主要是拉取提交作业时本地所需的包并确保其是最新的.具体流程如下:
- 在infra-client的bin目录下载并更新infra_client-pack.并添加环境变量:INFRA_CLIENT_PACK
- 在infra_client-pack包载并更新spark-pack,spark-conf,hadoop-pack,hadoop-conf.并添加环境变量:SPARK_HOME,SPARK_CONF_DIR,SPARK_DIST_CLASSPATH,HADOOP_HOME,HADOOP_CONF_DIR,JAVA_HOME,PATH
- 在spark-pack里的./bin/spark-class脚本生成java命令,主类是org.apache.spark.launcher.Main,在wordCount这个例子中,此命令如下:
java -Xmx128m -cp "$LAUNCH_CLASSPATH" org.apache.spark.launcher.Main
--cluster c3prc-hadoop-spark2.1
--class com.xiaomi.infra.codelab.spark.WordCount
--master yarn-cluster
--num-executors 3
--driver-memory 4g
--executor-memory 2g
--executor-cores 1
-queue root.production.cloud_group.hadoop.queue_1
--conf spark.yarn.job.owners=chenfan
~/gitRepo/codelab/spark/scala/wordcount/target/spark-scala-wordcount-1.0-SNAPSHOT.jar
hdfs://c3prc-hadoop/tmp/s_spark_tst/input.dat
hdfs://c3prc-hadoop/tmp/s_spark_tst/output
这里为了节省篇幅方便阅读,省略了classpath.
使用infra-client提交作业使得我们Spark,Hadoop和相关配置的更新对用户透明
关键类:
$INFRA_CLIENT/bin/spark-submit ./env.sh
$INFRA_CLIENT/bin/current/common-infra_client-pack/bin/spark-submit ./env.sh
$INFRA_CLIENT/bin/current/common-infra_client-pack/bin/current/c3prc-hadoop-spark2.1-spark-pack/bin/spark-submit
$INFRA_CLIENT/bin/current/common-infra_client-pack/bin/current/c3prc-hadoop-spark2.1-spark-pack/bin/spark-class
第二步:Spark客户端提交作业到集群
这一步涉及Spark客户端向YARN集群提交作业的过程,比较重要,由于此时的Spark运行在提交作业的机器上,我们可以称之为Spark客户端.具体流程如下:
- 脚本启动的Main#main方法主要作用是将命令行参数解析为SparkSubmit类可以识别的形式,一些参数的形式会被修改.比如在wordCount这个例子中:--driver.memory=4g会被修改为--conf spark.driver.memory=4g,这个解析的操作在SparkSubmitCommandBuilder类中完成.解析形成一条新的java命令并输出到bash执行,主类是org.apache.spark.deploy.SparkSubmit,在本例中,此命令如下(观察两个命令的区别):
java -cp "$LAUNCH_CLASSPATH" -XX:MaxPermSize=256m
org.apache.spark.deploy.SparkSubmit
--master yarn-cluster
--conf spark.yarn.job.owners=chenfan
--conf spark.driver.memory=4g
--class com.xiaomi.infra.codelab.spark.WordCount
--num-executors 3
--executor-memory 2g
--queue root.production.cloud_group.hadoop.queue_1
--executor-cores 1
/home/chenfan/test/spark-scala-wordcount-1.0-SNAPSHOT.jar
hdfs://c3prc-hadoop/tmp/s_spark_tst/input.dat
hdfs://c3prc-hadoop/tmp/s_spark_tst/output
这里为了节省篇幅方便阅读,省略了classpath和部分参数
-
SparkSubmit#main方法主要创建类SparkSubmitArguments,加载时首先会去加载默认配置文件spark-default.conf(由之前脚本设置的环境变量SPARK_CONF_DIR得到),然后进行参数检查等工作
- 随后进入SparkSubmit#submit,如上图所示,首先执行prepareSubmitEnvironment方法,会根据不同的部署模式,返回不同的args(childMainClass的参数),classpath,sysProps(Spark参数会写在sysProps,方便传递给childMainClass),childMainClass(启动主类),上图中展示了三种不同部署模式下的childMainClass,最后会通过反射执行childMainClass的main方法,在本例中部署模式是YARN cluster,所以主类是org.apache.spark.deploy.yarn.Client
-
进入Client#main方法.依然是先进行参数检查,随后执行new Client().run
- run方法中,首先会建立到YARN RM的连接,尝试向YARN申请appid,并获取当前集群资源的metrics信息(包括Container最大memory和最大vCore数目),之后执行资源验证:如果当前作业的executorMem或者amMem超过集群中Container允许设置的最大内存,就抛出IllegalArgumentException
-
验证通过后,会创建一个ContainerLaunchContext对象,它主要包括prefixEnv(库路径),amArgs(类参数),javaOpts(java参数),和环境变量等,它的结构如下图:
-
有了ContainerLaunchContext对象后,就可以创建YARN需要的ApplicationSubmissionContext对象了,它包含了ContainerLaunchContext对象,并且还包括application的其他信息,如申请队列,App信息,重试信息,申请容量等,它的结构如下图:
- 有了ApplicationSubmittsionContext对象后,调用YarnClient#submitApplication提交作业.至此,Spark客户端的提交操作结束,接下来客户端会循环获取application当前的状态
关键类
org.apache.spark.launcher.Main
org.apache.spark.deploy.SparkSubmit
org.apache.spark.deploy.yarn.Client
第三步:ApplicationMaster的执行过程
这一步开始,代码是在集群上执行.ApplicationMaster先在一个新线程中启动driver,再向RM申请资源启动executors.具体流程如下:
- YARN RM收到客户端的submitApplication请求后,会在某个节点拉取作业所需的运行环境和资源,比如Jar包等,并生成一个文件存储所有的Spark参数.随后执行java命令,命令行参数--properties-file指向Spark参数的文件,主类是org.apache.spark.deploy.yarn.ApplicationMaster
- AM初始化时,通过--properties-file得到Spark参数,之后将参数写入sys.props
-
在YARN cluster模式下,AM会启动一个叫做driver的线程去执行用户代码,也就是例子中的WordCount#main,然后阻塞等待driver线程中SparkContext对象初始化完成.SparkContext初始化完成后,会调用ApplicationMaster#sparkContextInitialized方法通知AM,在YARN cluster模式下,这个过程如下图所示:
- AM收到消息后继续执行下一步操作,首先AM会向RM注册,拿到一个YarnAllocator对象,YarnAllocator负责向RM申请containers,同时新建一个名为"ContainerLauncher"守护进程用来启动containers
- 拿到YarnAllocator后,执行allocator.allocateResources方法,这个方法首先执行amClient.allocate,向RM发送申请资源请求,拿到分配给作业的containers信息
- 随后执行handleAllocatedContainer,通过ContainerLauncher进程去在申请到的container上启动executor
- 执行userClassThread.join,userClassThread就是driver线程
关键类
org.apache.spark.deploy.yarn.ApplicationMaster
org.apache.spark.deploy.yarn.ApplicationMasterArguments
org.apache.spark.deploy.yarn.YarnAllocator
至此,ApplicationMaster完成初始化,接下来介绍driver和executor的初始化过程,driver是AM的子线程,在AM初始化的过程中初始化,executor是在AM初始化后,在YARN的其他container上被启动.
driver : SparkContext的初始化
AM初始化时启动driver线程,用来执行用户作业代码,在本例中是WordCount#main,我们主要关注SparkContext的初始化过程,之后的作业调度和计算本文档不做介绍.SparkContext初始化时会创建许多Spark运行时组件,WordCount#main的具体流程如下:
- 首先执行val sparkConf = new SparkConf,创建一个SparkConf的实例,SparkConf初始化时会从sys.props中获取Spark参数(由AM写入sys.props),然后执行val sc = new SparkContext(sparkConf),开始SparkContext的初始化
- 首先初始化LiveListenerBus,是driver的事件总线,有四个不同的queue,分别监听不同的事件,每个Listener可以绑定到某个队列,比如jobProgressListener会绑定到"appState"这个Queue,用以追踪任务的运行状态
- 初始化SparkEnv,创建一个SparkEnv,它持有许多Spark运行时对象,包括安全管理器SecurityManager,分布式Rpc消息系统RpcEnv以及MapOutputTracker等
- 初始化HeartbeatReceiver,用以接收executor传来的心跳
- 初始化TaskScheduler和SchedulerBackend,在YARN cluster模式下,这两个对象分别是YarnClusterScheduler和YarnClusterSchedulerBackend的实例
- 初始化DAGScheduler 用来划分job的stage,建立stage之间的关系等
- 初始化BlockManager,MetricsSystem等其他组件
- 启动ListenerBus,提交SparkListenerEnvironmentUpdate和SparkListenerApplicationStart两个事件
- 执行post startHook(startHook会唤醒AM) 和 add shutdownHook (JVM被kill掉时,hook没有用)
- 此时SparkContext的初始化结束,driver线程执行接下来的sc.textFile方法,不赘述.AM被唤醒后也继续执行之后的代码.
关键类
org.apache.spark.SparkContext
org.apache.spark.SparkConf
org.apache.spark.SparkEnv
executor的初始化过程
RM会根据收到的AM请求,在集群某一节点启动相应的executor,启executor会先创建Spark运行时环境,然后向driver注册.具体流程如下:
- 启动executor的java命令主类是CoarseGrainedExecutorBackend. 这个类会先去获取driver的SparkConf,然后调用SparkEnv#createExecutorEnv方法创建运行时环境
- 类似driver,executor的SparkEnv也会有许多组件,例如 安全管理SecurityManager,消息系统RpcEnv,存储管理BlockManager等,但要注意这些组件在driver和executor下的不同行为,不赘述
- CoarseGrainedExecutorBackend会注册到RpcEnv,然后接收到OnStart事件,创建一个Executor对象
- Executor对象进行初始化,主要是设置一些参数,比如files,jars等,随后初始化env.BlockManager,最后会创建一个新线程向driver定时发送heartbeat消息
- Executor对象初始化完成后,会通过Rpc向driver发送RegisterExecutor事件,注册该executor
- executor初始化完成,等待driver分配task
关键类
org.apache.spark.executor.CoarseGrainedExecutorBackend
org.apache.spark.executor.Executor
org.apache.spark.SparkEnv
提交流程结束
至此,框架为作业所做的准备工作完成,接下来会继续按步执行用户代码,开始调度并计算Spark作业