spark提交
./bin/spark-submit \
--class \
--master \
--deploy-mode \
--conf = \
... # other options
\
[application-arguments]
例如WordCount
代码
import org.apache.spark.{SparkConf, SparkContext}
/**
* Created by lancerlin on 2018/2/2.
*/
object WordCount {
def main(args: Array[String]): Unit = {
System.setProperty("HADOOP_USER_NAME", "hadoop");
//非常重要,是通向Spark集群的入口
val conf = new SparkConf().setAppName("WC")
val sc = new SparkContext(conf)
sc.textFile(args(0))
.flatMap(_.split(" "))
.map((_, 1))
.reduceByKey(_ + _)
.saveAsTextFile(args(1))
sc.stop()
}
}
提交脚本
./bin/spark-submit \
--class WordCount \
--master master01:7070 \
--deploy-mode client \
--executor-memory 8G \
-- total-executor-cores 3 \
wordcount.jar \
hdfs://wordcount.txt
所以,想要分析程序提交的流程,必须从spark-submit
脚本开始分析,--deploy-mode
有两种模式,client
和cluster
,client是client 跟driver都在客户端启动,同一个jvm中,一种是client跟driver分开启动的方式,为什么叫client和driver,在源码中我们会找到答案。
spark-submit
查看分析
org.apache.spark.deploy.SparkSubmit
类
- 查看源码,我们可以知道,
SparkSubmit
类中的main方法中,调用Submit方法,submit方法调用doRunMain,doRunMain方法通过反射,实例化主程序,这里就是WordCount
,在调用主程序的main
方法,所以Submit的工作就完成了,接下来分析我们的主程序代码。 -
总体流程如下图所示
主程序
WordCount 代码
import org.apache.spark.{SparkConf, SparkContext}
/**
* Created by lancerlin on 2018/2/2.
*/
object WordCount {
def main(args: Array[String]): Unit = {
System.setProperty("HADOOP_USER_NAME", "hadoop");
//非常重要,是通向Spark集群的入口
val conf = new SparkConf()
val sc = new SparkContext(conf)
sc.textFile(args(0))
.flatMap(_.split(" "))
.map((_, 1))
.reduceByKey(_ + _)
.saveAsTextFile(args(1))
sc.stop()
}
}
SparkContext
sparkContext是spark程序的入口,通过sc来连接集群
sparkEnv
272行代码,创建了sparkDriverEnv,sparkEnv里面,翻译文档就是
保存正在运行的Spark实例(master或worker)的所有运行时环境对象, 包括序列化器,Akka ActorSystem,块管理器,地图输出追踪器等
这里最重要的是,前面我们分析master
worker
的时候,看到的ActorSystem
SparkContext.DAGScheduler
主要作用
主要就是计算生成stages,并跟踪RDD跟stage的输入输出,将stage的tasks封装成一个taskSet并提交,每个task的失败重试,推测,都由DAGScheduler处理
DAGSchedulerEventProcessLoop
DAGScheduler里有个很重要的成员变量DAGSchedulerEventProcessLoop
private[scheduler] class DAGSchedulerEventProcessLoop(dagScheduler: DAGScheduler) extends EventLoop[DAGSchedulerEvent]("dag-scheduler-event-loop") with Logging
可以学习下这个类的编程模式,设计模式中,这个叫做模板方法模式
父类EventLoop
定义好了编程模型,子类DAGSchedulerEventProcessLoop
重写recieve方法
schedulerBackend, taskScheduler
创建了两个非常重要的成员变量,schedulerBackend, taskScheduler
,然后我们查看SparkContext.createTaskScheduler(this, master)
,这个方法的实现
根据传入的master信息,来决定调度器的实现,我们查看spark的模式实现
- 首先创建
TaskSchedulerImpl(sc)
,这是task任务的实现类 - 然后创建
SparkDeploySchedulerBackend
,接着调用scheduler.initialize(backend)
scheduler.initialize(backend)
方法中,创建了一个调度器,默认是FIFO调度器
381行代码中,将刚才创建的
taskScheduler
启动了起来,backend
是实现类是SparkDeploySchedulerBackend
,查看SparkDeploySchedulerBackend
的start
方法
SparkDeploySchedulerBackend
继承了CoarseGrainedSchedulerBackend
,spark-on-yarn
的时候,backend的实现类就是这个CoarseGrainedSchedulerBackend
CoarseGrainedSchedulerBackend
的start
方法中,创建了一个driverActor
,通过查看DriverActor
类,结合查看master worker的源码,我们知道,DriverActor
会走生命周期方法,然而,CoarseGrainedSchedulerBackend
只是创建了DriverActor
,并没有启动,分析DriverActor
,我们可以知道,他是发送task到executor上执行的,所以等待分配好资源后,才启动
class DriverActor(sparkProperties: Seq[(String, String)]) extends Actor with ActorLogReceive
看回SparkDeploySchedulerBackend
的start方法
重要的代码,查看
AppClient
client = new AppClient(sc.env.actorSystem, masters, appDesc, this, conf)
client.start()
start方法中通过actorSystem
创建了一个ClientActor
,执行preStart(),receiveWithLogging()
生命周期方法
master接收到RegisterApplication(appDescription)
,保存app信息,告诉client注册完毕,RegisteredApplication(app.id, masterUrl)
,然后调用schedule()
,这个方法我们在分析master、worker的时候已经看到过,后面我们会重点分析,主要就是mater指挥worker启动executor
Master:schedule
这里是启动executor的两种方式,一种是尽量打散,默认的方式,一种是尽量集中,通过
val spreadOutApps = conf.getBoolean("spark.deploy.spreadOut", true)
配置
尽量打散
分析源码我们可以看到尽量打散的源码分析, 比如此时需要--executor-memory 4G --total-executor-cores 8
-
首先判断内存大于app资源,这里是4G的worker并且该worker没有该app的executor,源码里说的是,standalone的情况下,不允许app的两个executor在同一个worker上
符合条件的worker按照剩余内存降序,判断cpu cores,如果app需要的cores > 符合条件workers的cores的总数,则取小的,比如app需要10cores,而符合条件的worker一共只有9cores,那么就是用9cores
-
分配好每个worker的cores和memory后,
launchExecutor(usableWorkers(pos), exec)
比如现在有3个Worker来执行WordCount,程序,按照尽量打散的逻辑,分配前后的executor如下图所示
集中
- 找到内存资源符合的worker,计算该worker的cores是否大于app需要的cores,取两者的最小值
- 启动Executor
launchExecutor(worker, exec)
launchExecutor
接下来重点分析如何启动Executor,分析到这里,我们知道,SparkDeploySchedulerBackend start()
方法中,将将app信息封装成一个appDesc,然后通过ClientActor
,将appDesc封装成case class发送给Master,master接收到后,通过集中
或者打散
的规则给Worker分配需要启动的Executor资源,调用launchExecutor
,重点分析launchExecutor
master发送信息给Worker,LaunchExecutor
,然后又给driver发送ExecutorAdded
,需要分别查看Worker跟DirverActor
Worker接收到LaunchExecutor,首先创建executor工作目录,然后启动一个ExecutorRunner.start
,start
方法中创建了一个线程workerThread
去启动executor,因为启动executor进程可能会消耗很多时间,需要异步处理,所以开启线程去启动。fetchAndRunExecutor
是启动executor的方法。通过fetchAndRunExecutor
启动的类是clientActor指定的,org.apache.spark.executor.CoarseGrainedExecutorBackend
,所以此时,需要到org.apache.spark.executor.CoarseGrainedExecutorBackend
类中查看main方法,启动Executor后,worker会相应的减少cores和memory。
org.apache.spark.executor.CoarseGrainedExecutorBackend
Executor也是一个Actor,会走跟master、worker一样的Actor生命周期方法。executor启动包含dirverUrl,appId,cores,worker等信息
private[spark] class CoarseGrainedExecutorBackend( driverUrl: String, executorId: String, hostPort: String, cores: Int, userClassPath: Seq[URL], env: SparkEnv) extends Actor with ActorLogReceive with ExecutorBackend with Logging
run方法中,启动了两个actor
CoarseGrainedExecutorBackend
, WorkerWatcher
org.apache.spark.executor.CoarseGrainedExecutorBackend.preStart & receiveWithLogging
向dirver发送信息注册executor,dirver回复注册executor成功,executor接收driver注册成功后,创建一个Executor对象
Executor
构造了一个线城池threadPool
,launchTask
方法中,将接收到的task封装成一个TaskRunner
,然后将他提交给threadPool
执行
创建executorActor与driver交互
startDriverHeartbeater
,查看代码,这是executor与driver保持心跳的,默认是val interval = conf.getInt("spark.executor.heartbeatInterval", 10000)
,10秒钟发送一次,但是在driverActor中,没有找到跟他交互的代码
CoarseGrainedExecutorBackend
中还启动了个WorkerWatcher
,负责跟worker保持心跳
总结
说些了那么多,最终我们可以得到如下图所示的关系,一切都准备好了,此时,就等待driver task的发送,
executor接收task,开始执行task