一.前言
在用户提交应用程序时,SparkContext会向Master发送注册消息,并由Master给该应用分配Executor。
这里的SparkContext主要用于负责和ClusterManager通信,进行资源的管理,任务分配和监控,负责作业执行的生命周期管理,ClusterManager提供了资源的分配和管理。
在不同模式下ClusterManager的角色不同,Standalone中由Master担任,在Yarn模式下由ResourceManager担任。SparkContext对运行的作业划分并分配资源后,会把任务发送到Executor去运行。
本文主要着重对Excutor资源分配的过程进行梳理。
二.Excutor资源分配原理分析
运行一个作业都会一定会有SparkContext,这里我们要明确,在SparkContext启动过程中,会首先创建DAGScheduler和TaskSechduler两个调度器,还有schedulerBackend用于分配当前可用的资源。
1.DAGScheduler
DAGScheduler主要负责将用户的应用的DAG划分为不同的Stage,其中每个Stage由可以并发执行的一组Task构成, 这些Task的执行逻辑完全相同,只是作用于不同的数据。
2.TaskSechduler
负责具体任务的调度执行,从DAGScheduler接收不同Stage的任务,按照调度算法,分配给应用程序的资源Executor上执行相关任务,并为执行特别慢的任务启动备份任务。 TaskSchedulerImpl创建时创建SchedulableBuilder,SchedulableBuilder根据类型分为FIFOSchedulableBuilder和FairSchedulableBuilder两类,。调度器的区别我会在以后的文章中说明,这里先记住有这个概念就行。
3.SchedulerBackend
分配当前可用的资源, 具体就是向当前等待分配计算资源的Task分配计算资源(即Executor) , 并且在分配的Executor上启动Task, 完成计算的调度过程。 它使用reviveOffers完成上述的任务调度。
SparkContext新建时,内部创建一个SparkEnv,SparkEnv内部创建一个RpcEnv
- SparkEnv:用户执行的环境信息,包括通信相关的端点。
- RpcEnv:SparkContext中远程通信环境
Excutor资源分配首先会通过TaskSchedulerImpl的start方法,调用StandaloneSchedulerBackend的start方法,在向RPCEnv注册DriverEndpoint和ClientEndpoint端点。
//TaskSchedulerImpl的start方法
override def start() {
//StandaloneSchedulerBackend 启动
backend.start()
if (!isLocal && conf.getBoolean("spark.speculation", false)) {
logInfo("Starting speculative execution thread")
speculationScheduler.scheduleWithFixedDelay(new Runnable {
override def run(): Unit = Utils.tryOrStopSparkContext(sc) {
checkSpeculatableTasks()
}
}, SPECULATION_INTERVAL_MS, SPECULATION_INTERVAL_MS, TimeUnit.MILLISECONDS)
}
}
//StandaloneSchedulerBackend的start方法
override def start() {
/**
* super.start()中有创建Driver的通信邮箱也就是Driver的引用
* 未来Executor就是向 StandaloneSchedulerBackend中父类 CoarseGrainedSchedulerBackend 中反向注册信息的.
*/
super.start()
// SPARK-21159. The scheduler backend should only try to connect to the launcher when in client
// mode. In cluster mode, the code that submits the application to the Master needs to connect
// to the launcher instead.
if (sc.deployMode == "client") {
launcherBackend.connect()
}
// The endpoint for executors to talk to us
val driverUrl = RpcEndpointAddress(
sc.conf.get("spark.driver.host"),
sc.conf.get("spark.driver.port").toInt,
CoarseGrainedSchedulerBackend.ENDPOINT_NAME).toString
val args = Seq(
"--driver-url", driverUrl,
"--executor-id", "{
{EXECUTOR_ID}}",
"--hostname", "{
{HOSTNAME}}",
"--cores", "{
{CORES}}",
"--app-id", "{
{APP_ID}}",
"--worker-url", "{
{WORKER_URL}}")
val extraJavaOpts = sc.conf.getOption("spark.executor.extraJavaOptions")
.map(Utils.splitCommandString).getOrElse(Seq.empty)
val classPathEntries = sc.conf.getOption("spark.executor.extraClassPath")
.map(_.split(java.io.File.pathSeparator).toSeq).getOrElse(Nil)
val libraryPathEntries = sc.conf.getOption("spark.executor.extraLibraryPath")
.map(_.split(java.io.File.pathSeparator).toSeq).getOrElse(Nil)
// When testing, expose the parent class path to the child. This is processed by
// compute-classpath.{cmd,sh} and makes all needed jars available to child processes
// when the assembly is built with the "*-provided" profiles enabled.
val testingClassPath =
if (sys.props.contains("spark.testing")) {
sys.props("java.class.path").split(java.io.File.pathSeparator).toSeq
} else {
Nil
}
// Start executors with a few necessary configs for registering with the scheduler
val sparkJavaOpts = Utils.sparkJavaOpts(conf, SparkConf.isExecutorStartupConf)
val javaOpts = sparkJavaOpts ++