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中。