Spark作为一个优秀的分布式集群内存计算框架,提供了简单接口和丰富的rdd算子供开发者调用。spark的运行速度之所以如此之快,一方面是因为它基于内存,另一方面是因为它对job,stage,task的划分并根据算子的shuffle过程将同一端的多个算子操作直接执行一条pipeline,减少了不必要的中间过程的存储消耗。根据官网的spark调度流程,我们看到如下图:
这张图非常简洁,大概描述的是,Driver进程和Cluster Manager(master,yarn,mesos等)进行通信并向集群管理器注册Application,集群管理器根据得到的Application描述和集群中的worker节点取得联系,让worker进程在节点上启动executor进程,executor内部又有多个task线程。这些资源分配的具体策略,比如executor-cores和task-cores都是在提交任务时指定的或者默认的。启动Executor后,会反向注册到Driver进程,Driver进程划分任务为一个个task,最后将task分发给Executor(Executor内部维护了一个线程池)来执行,Executor再将这些task分别放到线程中执行。
这只是大概的描述,然而实际上,Driver进程内部进行了更多细致的操作,如stage划分,task调度这些细节这张图并没有直接显示i出来。接下来,我就根据一点自己学到的这知识和相关源码来剖析一下。我们先看下面这一段代码
val conf = new SparkConf().setAppName("map").setMaster("local[2]")
var sc = new SparkContext(conf)
这是一段简单的scala代码,主要作用是初始化SparkContext。查看SparkContext源码,我们发现有这样两个字段:
private var _taskScheduler: TaskScheduler = _
@volatile private var _dagScheduler: DAGScheduler = _
继续往下我们看到
// Create and start the scheduler
val (sched, ts) = SparkContext.createTaskScheduler(this, master, deployMode)
_schedulerBackend = sched
_taskScheduler = ts
_dagScheduler = new DAGScheduler(this)
_heartbeatReceiver.ask[Boolean](TaskSchedulerIsSet)
// start TaskScheduler after taskScheduler sets DAGScheduler reference in DAGScheduler's
// constructor
_taskScheduler.start()
这段代码的主要作用其实就是在创建SparkContext对象的时候,也创建了TaskScheduler和DAGScheduler对象。其中TaskScheduler对象的创建根据指定的master参数的不同,也会执行不同的初始化策略,这一点主要是通过对master参数进行模式匹配来进行的,其实我们也很好理解,因为TaskScheduler是真正负责task任务的分发,所以不同的运行模式自然也会有不同的策略,而DAGScheduler则不同,它的主要职责就是将job划分为stage,再将stage划分为taskset,这个阶段的工作不会因为运行模式不同而不同。
在最后_taskScheduler.start(),TaskScheduler开始和集群资源管理器(master,yarn,mesos)通信并注册该application, 集群资源管理器根据得到的application信息启动worker节点上的executor进程。所以从这里我们看出来,spark的工作过程是先分配资源在进行任务执行的。这也是和hadoop不同的一点地方。
这只是初始化SparkContext的过程,接下来我们执行一个action算子操作,代码如下:
var sc = new SparkContext(conf)
val rdd = sc.parallelize(Array(1,2,3,4))
rdd.map(x=>x*10).foreach(x=>print(x+"\n"))
当代码执行到foreach算子操作时,spark将前面的代码封装成一个job开始执行。查看foreahc源代码:
def foreach(f: T => Unit): Unit = withScope {
val cleanF = sc.clean(f)
sc.runJob(this, (iter: Iterator[T]) => iter.foreach(cleanF))
}
关键是这个sc.runJob方法,我们一直进入这个runJob方法的内部,到最后在中发现这样一行代码:
dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, resultHandler, localProperties.get)
这里的dagScheduler就是我们初始化SparkContext的时候在SparkContext内部一起创建的。继续跟踪这个函数,发现在内部有这样一条代码:
val waiter = submitJob(rdd, func, partitions, callSite, resultHandler, properties)
在submitJob内部,会执行一系列的检查操作,如确保task不会不存在的partition分区上启动,在方法体的尾部有这样一段代码:
eventProcessLoop.post(JobSubmitted(
jobId, rdd, func2, partitions.toArray, callSite, waiter,
SerializationUtils.clone(properties)))
post方法内部其实调用的是eventQueue.put(event)方法,而eventQueue是LinkedBlockingDeque的一个实例。通过这个函数调用,其实已经将这个提交的job置入了阻塞的事件队列,等待执行。而eventProcessLoop是一个DAGSchedulerEventProcessLoop实例,查看这个类的源码,发现:
override def onReceive(event: DAGSchedulerEvent): Unit = {
val timerContext = timer.time()
try {
doOnReceive(event)
} finally {
timerContext.stop()
}
}
这个方法接受一个DAGSchedulerEvent参数,其实这个DAGSchedulerEvent就是一个trait,而在上面的代码中post的JobSubmitted实例对象本身就混入了这个trait,所以这个方法实质上就是接受一个来自阻塞事件队列中的事件
继续查看这个方法中的doOnReceive(evenet)方法,内部代码如下:
private def doOnReceive(event: DAGSchedulerEvent): Unit = event match {
case JobSubmitted(jobId, rdd, func, partitions, callSite, listener, properties) =>
dagScheduler.handleJobSubmitted(jobId, rdd, func, partitions, callSite, listener, properties)
........
}
这里用到了一个模式匹配。来对接受到的事件进行匹配,这里我们的事件类型为 JobSubmitted(.....),因此将执行第一个匹配到的代码,查dagScheduler.handleJobSubmitted
源码发现该方法内部包含这样两行代码:
finalStage = createResultStage(finalRDD, func, partitions, jobId, callSite)
submitStage(finalStage)
第一行代码就是用来求出根据宽窄以来划分出的最后一个stage,所以这里我们也看到,对job进行划分出stage的行为其实是DAGScheduler执行的。
继续跟踪submitStage(finalStage),发现内部代码如下:
private def submitStage(stage: Stage) {
val jobId = activeJobForStage(stage)
if (jobId.isDefined) {
logDebug("submitStage(" + stage + ")")
if (!waitingStages(stage) && !runningStages(stage) && !failedStages(stage)) {
val missing = getMissingParentStages(stage).sortBy(_.id) //求出该stage的父stage
logDebug("missing: " + missing)
if (missing.isEmpty) {
logInfo("Submitting " + stage + " (" + stage.rdd + "), which has no missing parents")
submitMissingTasks(stage, jobId.get) //如果当前stage不含父stage,则开始提交这个stage
} else {
for (parent <- missing) {
submitStage(parent) //递归执行,直到当前stage不含父stage
}
waitingStages += stage
}
}
} else {
abortStage(stage, "No active job for stage " + stage.id, None)
}
}
从上面的代码我们看出,其实对Stage的划分是一个从前往后的过程,一直递归执行,直到当前的stage不依赖上一步的stage,则提交这个stage,继续跟踪submitMissingTasks(stage, jobId.get)方法,发现该方法内部有这样一段代码:
val tasks: Seq[Task[_]] = try {
stage match {
case stage: ShuffleMapStage =>
partitionsToCompute.map { id =>
val locs = taskIdToLocations(id)
val part = stage.rdd.partitions(id)
new ShuffleMapTask(stage.id, stage.latestInfo.attemptId,
taskBinary, part, locs, stage.latestInfo.taskMetrics, properties, Option(jobId),
Option(sc.applicationId), sc.applicationAttemptId)
}
case stage: ResultStage =>
partitionsToCompute.map { id =>
val p: Int = stage.partitions(id)
val part = stage.rdd.partitions(p)
val locs = taskIdToLocations(id)
new ResultTask(stage.id, stage.latestInfo.attemptId,
taskBinary, part, locs, id, properties, stage.latestInfo.taskMetrics,
Option(jobId), Option(sc.applicationId), sc.applicationAttemptId)
}
}
} catch {............}
这里主要是通过模式匹配,得到当前stage的类型,并依据相应的策略划分出task。
在这段代码的下部,我们发现有下面这段代码:
if (tasks.size > 0) {
logInfo("Submitting " + tasks.size + " missing tasks from " + stage + " (" + stage.rdd + ")") //打印信息
...........
taskScheduler.submitTasks(new TaskSet(
tasks.toArray, stage.id, stage.latestInfo.attemptId, jobId, properties))
stage.latestInfo.submissionTime = Some(clock.getTimeMillis())
}else ............
这段代码的主要作用就是让taskScheduler对象提交task任务,让一系列task去运行。不同的master策略,对应不同的taskScheduler对象,因此也对应不同的task提交策略,
如果你这里指定的master为‘local[2]’,因此taskScheduler会直接将task提交到本机的executor中,executor在将它分配到具体的线程去执行。如果指定的不是local模式,那么可能会走网络传输将来实现task的提交。
最后,我们可以总结下:
SparkContext在初始化的时候,内部也初实例化了DAGScheduler和TaskScheduler对象,TaskScheduler在启动后,就和集群资源管理器进行通信,注册当前application, 资源管理器根据得到的application信息,根据自身的资源分配算法,通知worker节点启动相应的Executor,Executor启动后,反向注册到TaskScheduler 。当执行了一个action算子操作后,DAGScheduler内部将job划分Stage,划分策略是从后往前的递归执行的,将最后得到的没有父Stage的Stage提交,并根据Stage的不同类型,来划分task,将最后得到的task集合存储在tasks变量中,然后TaskScheduler来提交taskSet,并分发到不同的Executor中。