接上一篇”SparkContext初始化流程”,其中在初始化***SchedulerBackend那里提到了,在那里会创建一个AppClient对象,用于和Spark集群交互注册App,申请并启动Executor,本篇就来分析下注册App和启动Executor的流程。
先描述下流程:
1. SparkContext启动过程中,AppClient会向Master发送RegisterApplication消息。
2. Master按照FIFO的顺序启动App,通知Worker启动Executor(先确定Executor分配在哪些Worker上),并通知driver应用注册完成
3. Worker接受到Master消息之后,通过ExecutorRunner线程启动Executor所在进程,并向Master汇报ExecutorState.RUNNING,Master再向Driver发送ExecutorUpdated消息。
4. Executor进程启动过程中,首先会发送RegisterExecutor给Driver;Driver记录Executor信息,成功注册会发送RegisteredExecutor给CoarseGrainedExecutorBackend;失败会发送RegisterExecutorFailed消息让Executor进程退出(Driver随后会调用makeOffers()分配运行任务的资源,最后发送LauncherTask消息执行任务)
5. CoarseGrainedExecutorBackend接收到driver注册Executor成功的消息后,真正实例化Executor对象,此时Executor正式启动完毕,会定时向Driver发送心跳。
补充:
6. Driver根据任务结果进行不同的处理,然后给Executor分配新的任务直到结束。
7. Executor启动后,接受从Driver发送的LauncherTask执行任务消息,调用executor.launchTask真正执行任务。
Executor启动时序图:
此处以standalone为例进行,进行代码层面的分析:
1.AppClient向Master发送RegisterApplication消息
执行StandaloneAppClient.scala中的tryRegisterAllMasters()方法(在driver端执行),因为有HA,所以会遍历所有的Master。找到正在运行的Master后,向其发送RegisterAplication消息。
Master收到消息后,会记录APP以及Driver的一些信息,发送RegistererApplication消息给driver。然后执行Master.scala中的schedule()方法,这个方法是调度集群资源用的,这里的资源个人理解就是Executor。(ps:每次有新的App加入或者Executor资源有变化都会调用一遍该方法)
2.Master通知Work启动Executor,并通知driver
Master.scala执行上面提及的schedule()方法,该方法内部会调用startExecutorsOnWorkers()方法,分配资源并启动Executor。(ps:Deploy Mode为cluster的情况下,注册的Driver的信息是放在waitingDrivers中,此时还要在Worker上启动Driver)
按照FIFO顺序逐个启动Application。(这里是从waitingApps中拿数据,也就是说所谓的资源变化时调用scheduler(),这个方法不会影响到已经启动的App)
2.1方法首先获取到usableWorkers,即内存和cores都足够的Workers,然后按cores从多到少的顺序进行排列。
2.2执行scheduleExecutorsOnWorkers(),确定在哪些Worker上启动Executor。这里选择Worker是有一定规则的,稍后在下面分析。
2.3最后执行allocateWorkerResourceToExecutors()方法,Master会向Worker发送启动Executor消息,并向driver发送ExecutorAdd消息,让driver完成注册App的流程(standalone模式下,driver端只是打印个日志)。代码如下:
上面2.2中的ScheduleExecutorsOnWorkers()有两种分配的模式:一种是将Executor尽可能多的分配到Worker上,而另一种是将Executor尽可能少的分配到Worker。
默认都是使用前一种。(ps:如果Worker上Executor的数量比coresPerExecutor的少,那么executors就不会启动.(1.4之后就不是这样每次一个core这样分了,而是一次性corePerNode分完,所以1.4之后不会有这样的问题))
3.Worker接受到Master消息之后,通过创建一个叫ExecutorRunner线程启动Executor所在进程,并向Master汇报ExecutorState.RUNNING
从代码中可以看出,Worker首先创建了一个ExecutorRunner线程,由该线程真正启动Executor进程。(这么设计应该是为了让该线程可以kill掉Executor进程,然后进行资源的回收么?这一步意义还不是很理解)
启动Executor是使用了进程生成器ProcessBuilder,它的start()方法会以System.Runtime的方式启动一个子进程,这个进程的类名是CoarseGrainedExecutorBackend,它是Executor的容器,启动就是执行它的main()方法。
(ps:类名的Command的信息是在***schedulerbackend对象中就拼接好了的)
4.Worker上真正启动Executor进程
启动进程时会实例化CoarseGrainedExecutorBackend并向RpcEnv进行注册,注册时会执行它的onStart()方法,向Driver发送RegisterExecutor消息。
成功注册的话,Driver会向Executor发送RegisteredExecutor消息。Driver端的***SchedulerBackend接收到消息后会记录一些信息,然后发送RegisteredExecutor消息给Executor。(如果发送RegisterExecutorFailed,进程退出)
CoarseGrainedExecutorBackend在接受到消息后会创建一个Executor对象准备任务的执行,至此Executor才算真正启动完毕。Executor会保持和driver的心跳,接下来就是等待任务的调度与执行了。
Worker上启动Executor进程的整个过程图示如下:
启动Executor完整流程图为:
下一篇会分析下Job具体执行的流程,这里先按照书上的内容先梳理下,晚点再调试源码:
5.driver端接收到RegisterExecutor消息之后的操作
Driver端向Executor端发送RegisteredExecutor消息之后,会调用makeOffers()执行任务。
makeOffers()会将任务随机分发给各个Executor,这样做是为了负载均衡,并且方法内部会参考数据本地性。最后将任务信息进行包装,发送LaunchTasks信息到Executor。
6.Executor是如何执行Task的
CoarseGrainedExecutorBackend收到消息后会执行Task。
每个Task会被封装成一个TaskRunner对象,然后提交到Executor的线程池中执行。
Task执行成功后,回收该Executor运行改任务的CPU数,然后根据任务重新分配。
参考:
https://www.jianshu.com/p/0b19b2b954b6(Executor启动流程)
https://blog.csdn.net/u011564172/article/details/69922241(Executor进程如何启动)
《图解Spark》