文章目录
YARN Cluster模式
-
执行脚本提交任务,实际是启动一个SparkSubmit的JVM进程;
-
SparkSubmit类中的main方法反射调用YarnClusterApplication的main方法;
-
YarnClusterApplication创建Yarn客户端,然后向Yarn发送执行指令:bin/java ApplicationMaster;
-
Yarn框架收到指令后会在指定的NM中启动ApplicationMaster;
-
ApplicationMaster启动Driver线程,执行用户的作业;
-
AM向RM注册,申请资源;
-
获取资源后AM向NM发送指令:bin/java CoarseGrainedExecutorBackend;
-
CoarseGrainedExecutorBackend进程会接收消息,跟Driver通信,注册已经启动的Executor;然后启动计算对象Executor等待接收任务
-
Driver分配任务并监控任务的执行。
注意:SparkSubmit、ApplicationMaster和CoarseGrainedExecutorBackend是独立的进程;Driver是独立的线程;Executor和YarnClusterApplication是对象。
bin/spark-submit
--class prg.apache.spark.examples.SparkPi
--master yarn
--deploy-mode cluster
/example/jars/spark-examples.jar
10
以上脚本会启动一个类,sparksubmit类,启动一个进程,这个进程就用来跑我们的任务,跑一个rm客户端;
submit启动之后会去连接rm,向rm提交一个指令,告诉rm去启动一个am,rm收到指令之后,会找一台nm启动一个进程叫am,am有时候也叫driver进程,但源码真正的driver是一个线程;【只有进程才有进程,核心】
am启动成功之后,会启动一个子线程,叫driver线程,driver线程启动之后,会执行用户类的main函数,创建SparkContext,开始作DAG,碰见行动算子后,会划分stage,提交任务,提交任务之前要保证Executor进程启动好
Driver启动之后,并不会等你所有的代码执行完之后才启动Executor,Driver是一个子线程
Am启动之后,有两个事情,一个事情是在子线程启动Driver,另一个事情是在主线程启动Executor,根据rm返回的信息,启动Executor
怎么启动Executor?
am找rm申请资源,rm收到申请之后,会给am分配容器,容器里面主要封装了一些资源,cpu内核和内存,am收到容器之后,会在每一个容器启动一个executor进程,这个进程的名字叫做ExecutorBackend,进程启动成功之后,会找driver注册自己(反注册),注册成功之后,会出创建一个对象,叫Executor对象,这个对象里面有一个run方法,方法会执行我们具体的任务,每个任务对应一个线程
# Am一般是Driver进程,Driver其实是Am的一个子线程,Am的主线程是去启动Executor
# 先去Rm申请资源,Am个它分配一些容器。Am收到容器后,会在每个容器启动一个进程,进程启动成功之后,会去找Driver注册,注册自己,收到Driver发送的注册成功消息之后,会启动一个Executor,Executor是一个对象,有run方法等,对象里面可以执行任务,每个任务对应一个线程
源码详解
bin/spark-submit --master yarn
--deploy-mode cluster
--class com.atguigu.spark.day01.WordCount1
/opt/module/spark-standalone/bin/spark-1.0-SNAPSHOT.jar hdfs://hadoop102:9820/input
bin/spark-class org.apache.spark.deploy.SparkSubmit "$@"
在项目中添加依赖:
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-yarn_2.12</artifactId>
<version>3.0.0</version>
</dependency>
跑submit之前javahome是必配的
① SparkSubmit
bin/spark-submit #这里不能用spark-shell,spark-shell
--class prg.apache.spark.examples.SparkPi
--master yarn
--deploy-mode cluster
/example/jars/spark-examples.jar
10
if [ -z "${SPARK_HOME}" ]; then #判断当前环境是否是否配了SPARK_HOME环境
source "$(dirname "$0")"/find-spark-home
fi
# disable randomized hash for string in Python 3.3+
export PYTHONHASHSEED=0
exec "${SPARK_HOME}"/bin/spark-class org.apache.spark.deploy.SparkSubmit "$@" #将以上shell指令传递的参数都传给spark-class脚本
CMD=("${CMD[@]:0:$LAST}") #这句好好看
exec "${CMD[@]}"
#这个参数的内容是: ${CMD[@]}
/opt/module/jdk1.8.0_144/bin/java
#将spark-yarn的环境,jar包,hadoop环境都加载进来了
-cp /opt/module/spark-yarn/conf/: /opt/module/spark-yarn/jars/*: /opt/module/hadoop-3.1.3/etc/hadoop/
#启动这个程序就相当于启动这个类
org.apache.spark.deploy.SparkSubmit
#以下是传给这个类的参数,args
--master yarn
--deploy-mode cluster
--class org.apache.spark.examples.SparkPi ./examples/jars/spark-examples_2.12-3.0.0.jar
10
org.apache.spark.deploy.SparkSubmit
--main
--val submit = new SparkSubmit(){...} //这里new了一个sparksubmit匿名子类的对象(匿名内部类)
--submit.doSubmit(args) //执行dosubmit方法
--super.doSubmit(args) //dosubmit方法就是执行它父类的doSubmit()方法
--val appArgs = parseArguments(args) //解析spark-submit后面传递的各种参数,把传递的参数做封装
--appArgs.action match
case SparkSubmitAction.SUBMIT
=> submit(appArgs, uninitLog)
// 除非特别指定, action 就应该是 SUBMIT
action = Option(action).getOrElse(SUBMIT)
-- doRunMain()
--runMain(args, uninitLog) // 使用提交的参数,运行child class中的main 方法
--val (childArgs, childClasspath, sparkConf, childMainClass) = prepareSubmitEnvironment(args)
--var childMainClass = "" // 需要重点关注
--childMainClass = YARN_CLUSTER_SUBMIT_CLASS
// 如果是client模式,此时 childMainClass=args.mainClass
// 如果是cluster模式,此时 childMainClass=org.apache.spark.deploy.yarn.YarnClusterApplication(对用户定义的类的包装)
--var mainClass: Class[_] = null
//加载childMainClass
--mainClass = Utils.classForName(childMainClass)
// 判断SparkApplication是否为mainClass或者是 mainClass 的父类
// 在 yarn-cluster 模式下:
// mainClass = org.apache.spark.deploy.yarn.YarnClusterApplication
--val app: SparkApplication=
//通过反射的放射得到他的无参构造器,new一个实例,看这个实例对象mainClass是不是SparkApplication,显然是;
mainClass.getConstructor().newInstance().asInstanceOf[SparkApplication]
// 启动 SparkApplication
--app.start(childArgs.toArray, sparkConf)
//在Yarn\CLient.scala文件下这个类YarnClusterApplication继承SparkApplication
-- new Client(new ClientArguments(args), conf, null).run() //YarnClusterApplication.start run(): 向RM提交一个appliction,提交应用
// 向RM申请资源,运行AM进程
--this.appId = submitApplication()
//以下是submitApplication()方法的方法体
--> 【
--launcherBackend.connect()
// 运行了YarnClient的init()和start()
--yarnClient.init(hadoopConf) // 初始化 Yarn 客户端
// 关键代码: 创建YarnClient 对象, 用于连接 ResourceManager
private val yarnClient = YarnClient.createYarnClient
--yarnClient.start()
// 向RM申请应用
--val newApp = yarnClient.createApplication()
//获取RM的响应
--val newAppResponse = newApp.getNewApplicationResponse()
//从响应中获取RM生成的应用ID
--appId = newAppResponse.getApplicationId()
// 生成Job的临时作业目录
--val appStagingBaseDir
// 确保YARN有足够的资源运行AM
-- verifyClusterResources(newAppResponse)
// 开始安装 AM 运行的上下文
// Container中要运行的AM的进程的上下文 ,确定Container中进程的启动命令是什么
//核心代码:
--val containerContext = createContainerLaunchContext(newAppResponse)
// java 虚拟机一些启动参数 amMemory 默认1g
javaOpts += "-Xmx" + amMemory + "m"
// 确定 AM 类
// Cluster 模式:amClass=org.apache.spark.deploy.yarn.ApplicationMaster
// Client 模式: amClass=org.apache.spark.deploy.yarn.ExecutorLaunche
--val amClass =
if (isClusterMode) {
Utils.classForName("org.apache.spark.deploy.yarn.ApplicationMaster").getName
} else {
Utils.classForName("org.apache.spark.deploy.yarn.ExecutorLauncher").getName
}
//得到这个类之后怎么执行的呢?
// 指令参数:
--val amArgs
// 封装指令
val commands
//createContainerLaunchContext()方法返回一个容器,里面封装了指令
amContainer
【注意:】"提交指令之后,rm会执行org.apache.spark.deploy.yarn.ApplicationMaster这个类"
// AM进程运行后读取Spark应用的上下文
--val appContext = createApplicationSubmissionContext(newApp, containerContext)
// 核心代码: 提交应用 ->
// 向RM申请运行AM,真正的提交
-- yarnClient.submitApplication(appContext)
//最后返回appId
<--】
SparkSubmit总结:
1.通过脚本启动SparkSubmit进程
2.反射出来YarnClusterApplication
3.给RM提交应用 Application
② ApplicationMaster
org.apache.spark.deploy.yarn.ApplicationMaster
--main
// 获取AM需要的参数,对参数进行封装
--val amArgs = new ApplicationMasterArguments(args)
--val sparkConf = new SparkConf()
// 创建 ApplicationMaster 对象
--master = new ApplicationMaster(amArgs, sparkConf, yarnConf)
--ugi.doAs
// 执行AM 对象的 run 方法 ->
override def run(): Unit = System.exit(master.run())
final def run(): Int = ...
//如果是cluster模式: runDriver()
//如果是client模式: runExecutorLauncher()
-- runDriver() //运行Driver
//两件事情
"--->1.执行用户类(子线程中)"
// 启动应用程序 -> 启动一个线程,返回一个线程id
--userClassThread = startUserApplication()
// 加载用户定义的类, 并获取用户类的 main 方法
--val mainMethod = userClassLoader.loadClass(args.userClass).getMethod("main",classOf[Array[String]])
// 在一个子线程中执行用户类的 main 方法
--val userThread = new Thread(){
run(
// 运行用户定义的Driver类的main方法
//userArgs就是最后传的那个10
mainMethod.invoke(null, userArgs.toArray)
)
}
//给线程取一个名字叫Driver
-- userThread.setName("Driver")
//启动Driver线程,启动后,创建SparkContext,提交Job
--userThread.start()
//然后把userThread返回
"--->2.向rm注册Am,申请资源(容器)"
//等待sc的初始化
//在AM的主线程中,等待Driver线程创建 SparkContext,获取SparkContext
--val sc = ThreadUtils.awaitResult
// 从SparkContext的SparkEnv属性中,获取RpcEnv
-- val rpcEnv = sc.env.rpcEnv
//向RM注册 AM
-- registerAM(host, port, userConf, sc.ui.map(_.webUrl), appAttemptId)
// 创建 YarnRMClient 对象, 用于同 RM 通讯
--val client = new YarnRMClient()
//注册成功之后,rm分配些资源
-- client.register(host, port, yarnConf, _sparkConf, uiAddress, historyAddress)
//获取Driver的 EndpointRef
//这个地址将来要让executor知道
--val driverRef = rpcEnv.setupEndpointRef
// allocator:YarnAllocator 负责申请资源,在申请到Containers 后决定拿Container干什么
--createAllocator(driverRef, userConf, rpcEnv, appAttemptId, distCacheConf)
--allocator = client.createAllocator
// 创建并注册 AMEndpoint
--rpcEnv.setupEndpoint("YarnAM", new AMEndpoint(rpcEnv, driverRef))
// 向RM发请求,申请Containers
//分配资源
--allocator.allocateResources()
//尝试申请可分配的资源,得到所有资源列表
--val allocatedContainers = allocateResponse.getAllocatedContainers()
--if (allocatedContainers.size > 0){
// 决定用Container干什么事情
//处理分配到的资源
--handleAllocatedContainers(allocatedContainers.asScala)
// 运行匹配后的资源
--runAllocatedContainers(containersToUse)
//对于每个Container,每个容器启动一个Executor
--for (container <- containersToUse){
if (launchContainers) {
launcherPool.execute(() => {
try {
new ExecutorRunnable().run()
//run函数里
//创建和NM通信的客户端
--nmClient = NMClient.createNMClient()
//初始化NodeManager客户端
--nmClient.init(conf)
//启动NodeManager 客户端
--nmClient.start()
// 正式启动NM上的Container
--startContainer()
【注意:】"org.apache.spark.executor.YarnCoarseGrainedExecutorBackend Container中启动的进程"
//准备在Container上运行的Java命令
--val commands = prepareCommand()
}
}
ApplicationMaster总结:
1. 启动driver线程(运行用户类的main函数)
初始化sc
2. sc初始化成功之后,
am向rm注册am, 申请资源, 获取容器, 在能用的容器中启动 Executor进程
向NM提交指令
bin/java org.apache.spark.executor.YarnCoarseGrainedExecutorBackend ...
③ YarnCoarseGrainedExecutorBackend
org.apache.spark.executor.YarnCoarseGrainedExecutorBackend
--main
|--CoarseGrainedExecutorBackend=new YarnCoarseGrainedExecutorBackend
--YarnCoarseGrainedExecutorBackend extends CoarseGrainedExecutorBackend()
--CoarseGrainedExecutorBackend extends IsolatedRpcEndpoint
--override def onStart()
|--rpcEnv.asyncSetupEndpointRefByURI(driverUrl).flatMap{
ref =>
driver = Some(ref) // 拿到 driver 的引用
// 向 driver 注册当前的 Executor 向driver发送信息
//ask(必须回复) send(只发送, 不要求对方回复)
ref.ask[Boolean](RegisterExecutor(executorId, self, hostname, cores, extractLogUrls,extractAttributes, _resources, resourceProfile.id))
|--CoarseGrainedExecutorBackend.run(backendArgs, createFn)
--backendCreateFn => CoarseGrainedExecutorBackend
--SparkHadoopUtil.get.runAsSparkUser () =>
|--val executorConf = new SparkConf
|--val fetcher = RpcEnv.create //创建通信环境
|--var driver: RpcEndpointRef = null
// 获取Driver的EndpointRef
// DriverEndpointRef 通过此 ref 可以向 driver 发送信息
--driver = fetcher.setupEndpointRefByURI(arguments.driverUrl)
//向Driver发请求,请求SparkApp的配置
|--val cfg = driver.askSync[SparkAppConfig] (RetrieveSparkAppConfig(arguments.resourceProfileId))
|--fetcher.shutdown()
// 基于从Driver获取的SparkApp的配置,重新创建一个通信环境
①--val env = SparkEnv.createExecutorEnv(driverConf, arguments.executorId, arguments.bindAddress,arguments.hostname, arguments.cores, cfg.ioEncryptionKey, isLocal = false)
// 向新创建的环境中,
//注册一个通信端点 Executor => CoarseGrainedExecutorBackend(RpcEndPoint)
//之后,CoarseGrainedExecutorBackend需要运行onStart()
②--env.rpcEnv.setupEndpoint("Executor",
backendCreateFn(env.rpcEnv, arguments, env, cfg.resourceProfile))
//【env环境创建好了,再在backendCreateFn这创建一个endpoint,把endpoint放到环境里面去】
// 阻塞当前YarnCoarseGrainedExecutorBackend的线程,知道应用结束
③--env.rpcEnv.awaitTermination()
问 ?
向 driver 注册当前的 Executor, 向driver发送信息,Driver是如何接收消息的,以及Executor端创建Executor成功之后又是如何给Driver发送消息,简化版解析)
④ CoarseGrainedExecutorBackend.onStart()
--onStart()
--driver = Some(ref) // 获取Driver的EndpointRef
--ref.ask[Boolean](RegisterExecutor) // 向Driver 发RegisterExecutor消息,要求Driver回复Boolean型的消息
--case Success(_) => self.send(RegisteredExecutor) //收到true,自己给自己发送一个RegisteredExecutor消息
--case Failure(e) => exitExecutor // 退出Executor
--receive
-- case RegisteredExecutor =>
--logInfo("Successfully registered with driver")
// Executor: 计算者,负责运行Task
-- executor = new Executor(executorId, hostname, env, userClassPath, isLocal = false, resources = _resources)
--driver.get.send(LaunchedExecutor(executorId)) //给Driver发信息LaunchedExecutor(executorId)
⑤ SparkContext (Driver构造)
核心属性: var _env: SparkEnv : 封装了Spark所有的环境信息
_env = createSparkEnv(_conf, isLocal, listenerBus)
var _taskScheduler: TaskScheduler //任务调度器
val (sched, ts) = SparkContext.createTaskScheduler(this, master, deployMode) //528行
--case masterUrl =>
--val scheduler = cm.createTaskScheduler(sc, masterUrl) // YARN Cluster模式
-- val backend = cm.createSchedulerBackend(sc, masterUrl, scheduler) //YarnClusterSchedulerBackend
--YarnClusterSchedulerBackend 父类 YarnSchedulerBackend 的构造器中
-- 爷爷类 CoarseGrainedSchedulerBackend 的构造器
// 名称: CoarseGrainedScheduler 端点类型: DriverEndpoint
--属性 val driverEndpoint = rpcEnv.setupEndpoint(ENDPOINT_NAME, createDriverEndpoint())
--cm.initialize(scheduler, backend)
-- (backend, scheduler)
private var _dagScheduler: DAGScheduler //将RDD根据依赖划分阶段
⑥ Driver如何处理RegisterExecutor消息 , Driver使用 DriverEndpoint作为通信端点
Driver的EndPoint设备:
sc.env.rpcEnv: SparkContext
DriverEndpoint
--onStart
--receive
-- case LaunchedExecutor(executorId) =>
--executorDataMap.get(executorId).foreach { data =>
data.freeCores = data.totalCores
}
--makeOffers(executorId) //发offer, 成为工作团队的一员,准备接活
--receiveAndReply
--case RegisterExecutor
//如果当前executorId 已经存在在 记录的注册的executor的Map中,
--if (executorDataMap.contains(executorId)) context.sendFailure // 回复false
// 如果executorId在黑名单中,回复false
--else if (scheduler.nodeBlacklist.contains(hostname) || isBlacklisted(executorId, hostname)) context.sendFailure
--val data = new ExecutorData //记录Executor信息
-- executorDataMap.put(executorId, data)
-- context.reply(true) //回复true
源码概述
① SparkSubmit
org.apache.spark.deploy.yarn.ApplicationMaster
-- run
if (isClusterMode) { //集群模式
// ->
runDriver()
} else { // client 模式
runExecutorLauncher()
}
-- runDriver
两件事情:
1. 执行用户类(子线程中)
startUserApplication
val mainMethod = userClassLoader.loadClass(args.userClass)
.getMethod("main", classOf[Array[String]])
mainMethod.invoke(null, userArgs.toArray)
2. 向rm注册Am, 申请资源(容器)
registerAM(host, port, userConf, sc.ui.map(_.webUrl), appAttemptId)
createAllocator
-- allocator.allocateResources()
-- handleAllocatedContainers(allocatedContainers.asScala)
runAllocatedContainers(containersToUse)
-- ExecutorRunnable.run
startContainer()
-- prepareCommand()
org.apache.spark.executor.YarnCoarseGrainedExecutorBackend
② ApplicationMaster
org.apache.spark.deploy.yarn.ApplicationMaster
-- run
if (isClusterMode) { //集群模式
// ->
runDriver()
} else { // client 模式
runExecutorLauncher()
}
-- runDriver
两件事情:
1. 执行用户类(子线程中)
startUserApplication
val mainMethod = userClassLoader.loadClass(args.userClass)
.getMethod("main", classOf[Array[String]])
mainMethod.invoke(null, userArgs.toArray)
2. 向rm注册Am, 申请资源(容器)
registerAM(host, port, userConf, sc.ui.map(_.webUrl), appAttemptId)
createAllocator
-- allocator.allocateResources()
-- handleAllocatedContainers(allocatedContainers.asScala)
runAllocatedContainers(containersToUse)
-- ExecutorRunnable.run
startContainer()
-- prepareCommand()
org.apache.spark.executor.YarnCoarseGrainedExecutorBackend
③ YarnCoarseGrainedExecutorBackend
//创建了一个对象: endPoint
new YarnCoarseGrainedExecutorBackend(rpcEnv, arguments.driverUrl, arguments.executorId,
arguments.bindAddress, arguments.hostname, arguments.cores, arguments.userClassPath, env,
arguments.resourcesFileOpt, resourceProfile)
构造器, onStart, receive*, onStop
//向driver发送信息: ask(必须回复) send(只发送, 不要求对方回复)
ref.ask[Boolean](RegisterExecutor(executorId, self, hostname, cores, extractLogUrls,
extractAttributes, _resources, resourceProfile.id))
//接到成功的信息之后:
self.send(RegisteredExecutor)
//Driver端的endPoint
new SparkContext
YarnClusterScheduler
YarnClusterSchedulerBackend
val driverEndpoint = rpcEnv.setupEndpoint(ENDPOINT_NAME, createDriverEndpoint())
注册成功, 会给execute端发送成功信息
_taskScheduler.start()
backend.start()
bindToYarn(attemptId.getApplicationId(), Some(attemptId))
YARN Cluter模式总结
1.首先:通过sparksubmit脚本提交,启动一个sparksubmit进程,启动成功之后会封装一个指令,这个指令会启动一个AM;
--问:那么AM是怎么启动的?这个指令又是怎么封装的呢?
--答:在SparkSubmit里会反射一个类,反射的类叫YARNClusterApplication,这个类里面会把怎么启动的指令交给RM(ResourceManager)RM收到指令之后会找一台NodeManager启动一个AM(ApplicationMaster)
2.AM启动之后会做两件事情:
(1)运行一个Driver;
(2)向ResourceManager注册自己,注册成功之后,RM会给AM返回一些容器(资源);
3.AM有了这些容器(资源)之后会启动它们,启动这些容器就是在这些容器里面启动Exector进程,启动Executor进程是交给NM(NodeMAnager)的
4.Executor启动成功之后,会向Driver注册自己,注册成功之后,Driver会向Executor发送注册成功的信息,Executor进程收到成功的信息之后,会创建一个Executor对 象,new 了一个Executor对象,对象里面会有一些CPU啊,线程池啊之类的,后面会涉及到任务的调度;