前面已经分析到了 driver 进程成功在某台 worker 上启动了,下面就开始执行我们写的那些代码了。以一个 wordcount 程序为例,代码如下:
val conf = new SparkConf()
.setAppName("WordCount")
.setMaster("local")
val sc = new SparkContext(conf)
val lines = sc.textFile("./file/localfile")
val words = lines.flatMap(line => line.split(" "))
val wordPairs = words.map(word => (word, 1))
val wordCounts = wordPairs.reduceByKey(_ + _)
wordCounts.foreach(wordCount => println(wordCount._1 + " " + wordCount._2))
首先会去初始化我们的 SparkContext 对象,在初始化 SparkContext 对象前会先创建一个 SparkConf 对象用来配置各种参数。SparkContext 对象的初始化代码在 org.apache.spark.SparkContext 的 374-594 行(spark 2.1.1 的源码中),这块代码大概做了这些事情:
1,创建 Spark 的执行环境 SparkEnv;
2,创建并初始化 SparkUI;
3,Hadoop 相关配置和 Executor 环境变量的设置;
4,创建心跳接收器,用来和 Executor 做通信;
5,创建和启动 TaskScheduler;
6,创建和启动 DAGScheduler;
7,初始化 BlockManager;
8,启动测量系统 MetricsSystem;
9,创建和启动 ExecutorAllocationManager;
10,创建和启动 ContextCleaner;
11,Spark Environment Update;
12,向系统的测量系统注册 DAGSchedulerSource,BlockManagerSource,executorAllocationManagerSource;
13,标记当前 SparkContext 为激活状态(这个代码在 SparkContext 类的最后,2237 行);
以上就是 SparkContext 初始化过程,我们最主要的是分析 TaskScheduler 和 DAGScheduler 的创建和启动过程,这篇主要来看 TaskScheduler。
创建和启动 TaskScheduler 的代码从 501 行开始,代码如下:
// 创建 TaskScheduler,DAGScheduler
val (sched, ts) = SparkContext.createTaskScheduler(this, master, deployMode)
_schedulerBackend = sched
_taskScheduler = ts
_dagScheduler = new DAGScheduler(this)
_heartbeatReceiver.ask[Boolean](TaskSchedulerIsSet)
// 开启 taskScheduler
_taskScheduler.start()
上面代码可以看出通过调用 SparkContext.createTaskScheduler 方法去创建 TaskScheduler。
createTaskScheduler 方法会根据传入的 master 参数进行模式匹配,我们使用的是 spark 的 standalone cluster 模式,对应的 master 肯定是这种的:“spark://xxxx”。
所以会匹配到 SPARK_REGEX(sparkUrl)
这里,这里的 SPARK_REGEX 的匹配规则是 val SPARK_REGEX = """spark://(.*)""".r
。
匹配成功后,会去创建 TaskSchedulerImpl 对象、StandaloneSchedulerBackend 对象,然后然后将 StandaloneSchedulerBackend 对象带入到 TaskSchedulerImpl 的 initialize 方法中进行初始化操作。最后返回 StandaloneSchedulerBackend 和 TaskSchedulerImpl 对象。
这里的 TaskSchedulerImpl 是 TaskScheduler trait 的实现类,作用是:从 DAGScheduler 接收不同的 stage 任务,并向集群提交这些任务。
以上分析对应的代码如下:
case SPARK_REGEX(sparkUrl) =>
val scheduler = new TaskSchedulerImpl(sc)
val masterUrls = sparkUrl.split(",").map("spark://" + _)
val backend = new StandaloneSchedulerBackend(scheduler, sc, masterUrls)
scheduler.initialize(backend)
(backend, scheduler)
下面主要看 scheduler.initialize(backend) 这里,执行 TaskSchedulerImpl 的 initialize 方法。这个方法要传入一个 SchedulerBackend 对象。
initialize 的过程中首先会将 StandaloneSchedulerBackend 对象持有到该 TaskSchedulerImpl 对象里。
然后去创建调度池 rootPool,这里面缓存了调度队列等相关信息。
然后再去根据 schedulingMode 去进行模式匹配使用哪种调度算法,schedulingMode 初始化时候默认值是 FIFO,所以这里默认的调度算法会匹配到 FIFO 模式。
模式匹配成功后创建 FIFOSchedulableBuilder,用来操作 rootPool 中的调度队列。
def initialize(backend: SchedulerBackend) {
this.backend = backend
// 创建调度池
rootPool = new Pool("", schedulingMode, 0, 0)
// 模式匹配调度算法
// schedulingMode 有个默认值为 FIFO,具体可以点到对应的代码看
schedulableBuilder = {
schedulingMode match {
case SchedulingMode.FIFO =>
new FIFOSchedulableBuilder(rootPool)
case SchedulingMode.FAIR =>
new FairSchedulableBuilder(rootPool, conf)
case _ =>
throw new IllegalArgumentException(s"Unsupported spark.scheduler.mode: $schedulingMode")
}
}
schedulableBuilder.buildPools()
}
初始化完成后就会返回 (backend, scheduler)
val (sched, ts) = SparkContext.createTaskScheduler(this, master, deployMode)
然后为 SparkContext 对象中对应的 _schedulerBackend 和 _taskScheduler 对象赋值,这里的 _schedulerBackend 是 StandaloneSchedulerBackend,_taskScheduler 是 TaskSchedulerImpl。
_schedulerBackend = sched
_taskScheduler = ts
接下来就是创建 DAGScheduler。
DAGScheduler 主要作用是:分析用户提交的应用,根据 RDD 之间的依赖关系创建 DAG 图,然后根据 DAG 图划分成多个 stage,为每个 stage 分一组 task 去处理一批数据。然后将这些 task 交给 TaskScheduler,TaskScheduler 会通过 ClusterManager 在集群中找到符合要求的 Worker 上的 Executor 去启动这些 task。
具体源码可以看后面的文章。
_dagScheduler = new DAGScheduler(this)
在 new DAGScheduler(this)
这个构造函数里 this 就是当前的 SparkContext 对象,后续代码里会通过 SparkContext.taskScheduler
方法拿到里面的 _taskScheduler 对象,然后会调用 TaskScheduler.setDAGScheduler
方法设置好 DAGScheduler 的引用。这里设置好之后就会开启 TaskScheduler。
_taskScheduler.start()
_taskScheduler.start()
方法中回去调用 backend.start
方法,在这里就是 StandaloneSchedulerBackend
中的 start 方法。
override def start() {
// 这里会去调用 StandaloneSchedulerBackend 的 start 方法
backend.start()
if (!isLocal && conf.getBoolean("spark.speculation", false)) {
logInfo("Starting speculative execution thread")
speculationScheduler.scheduleAtFixedRate(new Runnable {
override def run(): Unit = Utils.tryOrStopSparkContext(sc) {
checkSpeculatableTasks()
}
}, SPECULATION_INTERVAL_MS, SPECULATION_INTERVAL_MS, TimeUnit.MILLISECONDS)
}
}
StandaloneSchedulerBackend 是一个 SchedulerBackend trait 的实现类,是 TaskScheduler 调度后端接口,不同的集群部署模式会有不同的实现。
TaskScheduler 给 task 分配资源的时候实际上是通过 SchedulerBackend 去完成的。StandaloneSchedulerBackend 是用于 standalone cluster 模式下的 SchedulerBackend。作用于 driver 内,用于和 Executor 通信,Task 的资源分配。
StandaloneSchedulerBackend 的 start 方法会去调用其父类的 start 方法,也就是 CoarseGrainedSchedulerBackend 的 start 方法。
这个方法内会去创建一个 DriverEndPointRef。
override def start() {
val properties = new ArrayBuffer[(String, String)]
for ((key, value) <- scheduler.sc.conf.getAll) {
if (key.startsWith("spark.")) {
properties += ((key, value))
}
}
// 根据配置参数属性去创建 DriverEndPointRef
driverEndpoint = createDriverEndpointRef(properties)
}
createDriverEndpointRef 这个方法内部会先想 rpcEnv 上注册一个创建好的 DriverEndpoint。
protected def createDriverEndpointRef(
properties: ArrayBuffer[(String, String)]): RpcEndpointRef = {
// 向 rpcEnv 注册 createDriverEndpoint 这个方法返回的 DriverEndpoint
rpcEnv.setupEndpoint(ENDPOINT_NAME, createDriverEndpoint(properties))
}
createDriverEndpoint 这个方法是用来创建 DriverEndpoint 的,这个 DriverEndpoint 就是用来 提交 task 到 Executor,并接收 Executor 的返回结果的。
protected def createDriverEndpoint(properties: Seq[(String, String)]): DriverEndpoint = {
// 创建 DriverEndpoint
new DriverEndpoint(rpcEnv, properties)
}
至此,TaskScheduler 创建和启动已经完成。