26套Spark企业级项目实战,源码深度剖析,实时流处理,机器学习,数据分析,运行原理,性能调优,图计算,性能调优,缓存优化,监控分析SparkCore,SparkSQL,SparkStreaming,Kafka,Flume,Scale,Python视频课程
视频课程包含:
26套Spark项目实战包含:大数据之Spark高级课程,企业级项目实战,源码深度剖析,实时流处理,机器学习,数据分析,运行原理,性能调优,图计算,性能调优,缓存优化,监控分析,Spark调优,算子优化,全场景项目实战,用户行为实时分析,实时流量监控系统,实时电影推荐系统,爱奇艺实时流处理项目,RDD数据集,精准广告推送实战,ML机器学习,PySpark,SparkCore,SparkSQL,SparkStreaming,Kafka,Flume,Scale,Python高级视频课程。。。
26套精品课程介绍:
1、26套精品是掌柜最近整理出的最新课程,都是当下最火的技术,最火的课程,也是全网课程的精品;
2、26套资源包含:全套完整高清视频、完整源码、配套文档;
3、知识也是需要投资的,有投入才会有产出(保证投入产出比是几百上千倍),如果有心的朋友会发现,身边投资知识的大都是技术经理或者项目经理,工资一般相对于不投资的也要高出很多;
总目录:26套Spark企业级项目实战,源码深度剖析,实时流处理,机器学习,数据分析,推荐系统,图计算,缓存优化,监控分析SparkCore,SparkSQL,SparkStreaming,Kafka,Flume,Scale,Python视频课程
第一套:【系统学习】快速掌握Spark 2.0(新特性、含真实项目、纯Scala语言开发、CDH5.7)视频教程
第二套:【系统学习】Spark全面精讲系统培训课程(基于Spark2版本+含Spark调优+超多案例)视频教程
第三套:【系统学习】Spark高薪就业课(SparkCore,SparkSQL,SparkStreaming,运维与监控)视频教程
第四套:【项目实战】Spark从入门到上手实战(Scacle编程+SparkCore实战+SparkSQL+SparkStreaming)视频教程
第五套:【系统学习】学习Scala进击大数据Spark生态圈,进击Spark生态圈必备,迈向“高薪”的基石视频教程
第六套:【综合学习】Spark2全面深度剖析--知识点,源码,调优,JVM,图计算,项目实战视频教程
第七套:【系统学习】Spark核心解密源码剖析,调度流程源码剖析,算子优化,缓存优化视频教程
第八套:【项目实战】大数据全栈高手速成--Spark2.0精讲(全场景项目实战)视频教程
第九套:【项目实战】大数据Spark实战项目大数据实战之精准广告推送实战,完全实战化学习大数据开发视频教程
第十套:【项目实战】Spark企业级实战项目:知名手机厂商用户行为实时分析系统视频教程
第十一套:【项目实战】Spark企业级实战项目:道路交通实时流量监控预测系统视频教程
第十二套:【项目实战】Spark企业级实战项目:离线和实时电影推荐系统直播回放(视频+文档+代码)视频教程
第十三套:【项目实战】以慕课网日志分析为例进入大数据SparkSQL的世界视频教程
第十四套:【项目实战】基于Spark2.x新闻网大数据实时分析可视化系统项目实战视频教程
第十五套:【项目实战】Spark企业级大数据项目实战,项目集成Hadoop,Spark,HBase,Kafka,Oracle,ElasticSearch大数据技术视频教程
第十六套:【项目实战】爱奇艺实时流处理项目实战 (Spark Streaming)企业级真实案例项目实战视频教程
第十七套:【项目实战】实时流处理SparkStreaming项目实战(Flume+KafkaSpark Streaming打造通用流处理平台)视频教程
第十八套:【系统学习】大数据Spark “蘑菇云”行动,spark2.x,spark Streaming 视频教程
第十九套:【项目实战】PySpark基于Python的Spark企业级大数据分析,以实际数据分析为驱动讲解,项目实战视频课程
第二十套:【系统学习】PySpark大数据处理及机器学习Spark2.3深入学习高级视频课程
第二十一套:【项目实战】深入学习大数据分析Spark2.X+Python 精华实战,实战Spark与分散式机器学习课程
第二十二套:【项目实战】全面掌握Spark2.0 ML机器学习,ML的应用开发和定制开发视频教程
第二十三套:【项目实战】大数据之基于Spark的机器学习-智能客户系统项目实战视频教程
第二十四套:【项目实战】深入Spark与kafka整合之实时流计算机器学习实战视频教程
第二十五套:【项目实战】Spark机器学习班,运行原理,性能调优,图计算,存储调度与监控分析视频教程
第二十六套:【项目实战】深入浅出Spark2.X用户行为分析机器学习,RDD数据集,SparkSQL数据存储,实时流分析项目实战视频教程
作业执行源码分析
当我们的代码执行到了action(行动)操作之后就会触发作业运行。在Spark调度中最重要的是DAGScheduler和TaskScheduler两个调度器,其中,DAGScheduler负责任务的逻辑调度,
将作业拆分为不同阶段的具有依赖关系的任务集。TaskScheduler则负责具体任务的调度执行。
提交作业
WordCount.scala执行到wordSort.collect()
才会触发作业执行,在RDD的源码的collect方法触发了SparkContext的runJob方法来提交作业。SparkContext的runJob方法经过几次调用后,
进入DAGScheduler的runJob方法,其中SparkContext中调用DAGScheduler类runJob方法代码如下:
def runJob[T, U: ClassTag](rdd: RDD[T],func: (TaskContext, Iterator[T]) => U,partitions: Seq[Int], resultHandler: (Int, U) => Unit): Unit = { if (stopped.get()) { throw new IllegalStateException("SparkContext has been shutdown") } val callSite = getCallSite val cleanedFunc = clean(func) ... // 调用DAGScheduler的runJob进行处理 dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, resultHandler, localProperties.get) progressBar.foreach(_.finishAll()) // 做checkpoint,以后再说 rdd.doCheckpoint() }
在DAGScheduler类内部会进行一列的方法调用,首先是在runJob方法里,调用submitJob方法来继续提交作业,这里会发生阻塞,知道返回作业完成或失败的结果;然后在submitJob方法里,创建了一个JobWaiter对象,
并借助内部消息处理进行把这个对象发送给DAGScheduler的内嵌类DAGSchedulerEventProcessLoop进行处理;最后在DAGSchedulerEventProcessLoop消息接收方法OnReceive中,接收到JobSubmitted样例类完成模式匹配后,
继续调用DAGScheduler的handleJobSubmitted方法来提交作业,在该方法中进行划分阶段。
def submitJob[T, U](rdd: RDD[T],func: (TaskContext, Iterator[T]) => U,partitions: Seq[Int], callSite: CallSite, resultHandler: (Int, U) => Unit, properties: Properties): JobWaiter[U] = { // Check to make sure we are not launching a task on a partition that does not exist. val maxPartitions = rdd.partitions.length partitions.find(p => p >= maxPartitions || p < 0).foreach { p => throw new IllegalArgumentException( "Attempting to access a non-existent partition: " + p + ". " + "Total number of partitions: " + maxPartitions) } val jobId = nextJobId.getAndIncrement() if (partitions.size == 0) { // Return immediately if the job is running 0 tasks return new JobWaiter[U](this, jobId, 0, resultHandler) } val func2 = func.asInstanceOf[(TaskContext, Iterator[_]) => _] val waiter = new JobWaiter(this, jobId, partitions.size, resultHandler) eventProcessLoop.post(JobSubmitted( jobId, rdd, func2, partitions.toArray, callSite, waiter, SerializationUtils.clone(properties))) waiter }
划分调度阶段
Spark调度阶段的划分是由DAGScheduler实现的,DAGScheduler会从最后一个RDD出发使用广度优先遍历整个依赖树,从而划分调度阶段,
调度阶段的划分是以是否为宽依赖进行的,即当某个RDD的操作是Shuffle时,以该Shuffle操作为界限划分成前后两个调度阶段
代码实现是在DAGScheduler的handleJobSubmitted方法中。
private[scheduler] def handleJobSubmitted(jobId: Int,finalRDD: RDD[_],func: (TaskContext, Iterator[_]) => _,partitions: Array[Int], callSite: CallSite, listener: JobListener, properties: Properties) { // 第一步:使用最后一个RDD创建一个finalStage var finalStage: ResultStage = null try { // 创建一个stage,并将它加入DAGScheduler内部的内存缓冲中, newResultStage的时候就已经得到了他所有的ParentStage finalStage = newResultStage(finalRDD, func, partitions, jobId, callSite) } catch {...} // 第二步:用finalStage,创建一个Job val job = new ActiveJob(jobId, finalStage, callSite, listener, properties) clearCacheLocs() logInfo("Got job %s (%s) with %d output partitions".format( job.jobId, callSite.shortForm, partitions.length)) logInfo("Final stage: " + finalStage + " (" + finalStage.name + ")") logInfo("Parents of final stage: " + finalStage.parents) logInfo("Missing parents: " + getMissingParentStages(finalStage)) // 第三步:将job加入内存缓存中 val jobSubmissionTime = clock.getTimeMillis() jobIdToActiveJob(jobId) = job activeJobs += job finalStage.setActiveJob(job) val stageIds = jobIdToStageIds(jobId).toArray val stageInfos = stageIds.flatMap(id => stageIdToStage.get(id).map(_.latestInfo)) listenerBus.post( SparkListenerJobStart(job.jobId, jobSubmissionTime, stageInfos, properties)) // 第四步:使用submitStage方法来提交最后一个stage, // 最后的结果就是,第一个stage提交,其它stage都在等待队列中 submitStage(finalStage) // 提交等待的stage submitWaitingStages() } // newResultStage会调用getParentStagesAndId得到所有的父类stage以及它们的idprivate def newResultStage(rdd: RDD[_],func: (TaskContext, Iterator[_]) => _,partitions: Array[Int], jobId: Int, callSite: CallSite): ResultStage = { // 得到所有的父stage val (parentStages: List[Stage], id: Int) = getParentStagesAndId(rdd, jobId) val stage = new ResultStage(id, rdd, func, partitions, parentStages, jobId, callSite) stageIdToStage(id) = stage updateJobIdStageIdMaps(jobId, stage) stage }// 继续调用getParentStagesprivate def getParentStagesAndId(rdd: RDD[_], firstJobId: Int): (List[Stage], Int) = { val parentStages = getParentStages(rdd, firstJobId) val id = nextStageId.getAndIncrement() (parentStages, id) }private def getParentStages(rdd: RDD[_], firstJobId: Int): List[Stage] = { val parents = new HashSet[Stage] val visited = new HashSet[RDD[_]] // We are manually maintaining a stack here to prevent StackOverflowError // caused by recursively visiting val waitingForVisit = new Stack[RDD[_]] def visit(r: RDD[_]) { if (!visited(r)) { visited += r for (dep <- r.dependencies) { dep match { // 所依赖RDD操作类型是ShuffleDependency,需要划分ShuffleMap调度阶段, // 以getShuffleMapStage方法为入口,向前遍历划分调度阶段 case shufDep: ShuffleDependency[_, _, _] => parents += getShuffleMapStage(shufDep, firstJobId) case _ => waitingForVisit.push(dep.rdd) } } } } // 从最后一个RDD向前遍历这个依赖树,如果该RDD依赖树存在ShuffleDependency的RDD, // 则存在父调度阶段,反之,不存在 waitingForVisit.push(rdd) while (waitingForVisit.nonEmpty) { visit(waitingForVisit.pop()) } parents.toList }// private def getShuffleMapStage(shuffleDep: ShuffleDependency[_, _, _],firstJobId: Int): ShuffleMapStage = { shuffleToMapStage.get(shuffleDep.shuffleId) match { case Some(stage) => stage case None => // We are going to register ancestor shuffle dependencies getAncestorShuffleDependencies(shuffleDep.rdd).foreach { dep => if (!shuffleToMapStage.contains(dep.shuffleId)) { // 如果shuffleToMapStage中没有,那么就new一个shuffleMapStage shuffleToMapStage(dep.shuffleId) = newOrUsedShuffleStage(dep, firstJobId) } } // Then register current shuffleDep val stage = newOrUsedShuffleStage(shuffleDep, firstJobId) shuffleToMapStage(shuffleDep.shuffleId) = stage stage } }// 可以对比这个算法和getParentStages,该方法返回所有的宽依赖private def getAncestorShuffleDependencies(rdd: RDD[_]): Stack[ShuffleDependency[_, _, _]] = { val parents = new Stack[ShuffleDependency[_, _, _]] val visited = new HashSet[RDD[_]] val waitingForVisit = new Stack[RDD[_]] def visit(r: RDD[_]) { if (!visited(r)) { visited += r for (dep <- r.dependencies) { dep match { // 所依赖RDD操作类型是ShuffleDependency,作为划分ShuffleMap调度阶段界限 case shufDep: ShuffleDependency[_, _, _] => if (!shuffleToMapStage.contains(shufDep.shuffleId)) { parents.push(shufDep) } case _ => } waitingForVisit.push(dep.rdd) } } } waitingForVisit.push(rdd) while (waitingForVisit.nonEmpty) { visit(waitingForVisit.pop()) } parents }
只看代码还是会头大,我们结合一个图来讲解上面的代码:如下图,有7个RDD,分别是rddA~rddG,它们之间有5个操作,其划分调度阶段如下:
在SparkContext中提交运行时,会调用DAGScheduler的handleJobSubmitted进行处理,在该方法中会先找到最后一个RDD(即rddG),并调用getParentStages方法
在getParentStages方法判断rddG的依赖RDD树中是否存在shuffle操作,在该例子中发现join操作为shuffle操作,则获取该操作的RDD为rddB和rddF
使用getAncestorShuffleDependencies方法从rddB向前遍历,发现该依赖分支上没有其他的宽依赖,调用newOrUsedShuffleStage方法生成调度阶段ShuffleMapStage0
使用getAncestorShuffleDependencies方法从rddB向前遍历,发现groupByKey宽依赖操作,以此为分界划分rddC和rddD为ShuffleMapStage1, rddE和rddF为ShuffleMapStage2
最后生成rddG的ResultStage3。
总结,语句
finalStage = newResultStage(finalRDD, func, partitions, jobId, callSite)
在生成finalStage的同时,建立起所有调度阶段的依赖关系,最后通过finalStage生成一个作业实例,在该作业实例中按照顺序提交调度阶段进行执行。
提交调度阶段
通过handleJobSubmitted方法中的submitStage(finalStage)
来提交作业。在submitStage方法中调用getMissingParentStages
方法获取finalStage父调度阶段,如果不存在父调度阶段,则使用submitMissingTasks
方法提交执行,如果存在父调度阶段,
则把该调度阶段存放到waitingStages列表中,同时递归调用submitStage。
/** * 提交stage的方法 * stage划分算法的入口,stage划分算法是由submitStage()与getMissingParentStages()共同组成 */ private def submitStage(stage: Stage) { val jobId = activeJobForStage(stage) if (jobId.isDefined) { logDebug("submitStage(" + stage + ")") if (!waitingStages(stage) && !runningStages(stage) && !failedStages(stage)) { // getMissingParentStages获取当前stage的父stage val missing = getMissingParentStages(stage).sortBy(_.id) logDebug("missing: " + missing) // 直到最初的stage,它没有父stage,那么此时,就会去首先提交第一个stage,stage0 // 其余的stage,都在waitingStages里 if (missing.isEmpty) { logInfo("Submitting " + stage + " (" + stage.rdd + "), which has no missing parents") submitMissingTasks(stage, jobId.get) } else { for (parent <- missing) { // 递归调用submitStage方法去提交父stage submitStage(parent) } // 并且当前stage放入waitingStages等待执行的stage队列中 waitingStages += stage } } } else { abortStage(stage, "No active job for stage " + stage.id, None) } } private def getMissingParentStages(stage: Stage): List[Stage] = { val missing = new HashSet[Stage] val visited = new HashSet[RDD[_]] // We are manually maintaining a stack here to prevent StackOverflowError // caused by recursively visiting val waitingForVisit = new Stack[RDD[_]] def visit(rdd: RDD[_]) { if (!visited(rdd)) { visited += rdd val rddHasUncachedPartitions = getCacheLocs(rdd).contains(Nil) if (rddHasUncachedPartitions) { // rdd的依赖 for (dep <- rdd.dependencies) { dep match { // 如果是宽依赖,那么就创建一个shuffleMapStage case shufDep: ShuffleDependency[_, _, _] => val mapStage = getShuffleMapStage(shufDep, stage.firstJobId) // 判断是否可用,也就是判断父stage有没有结果, 看源码可以发现就是判断_numAvailableOutputs == numPartitions // _numAvailableOutputs就是每个task成功后会+1 if (!mapStage.isAvailable) { missing += mapStage } // 如果是窄依赖,那么将依赖的rdd放入栈中 case narrowDep: NarrowDependency[_] => waitingForVisit.push(narrowDep.rdd) } } } } } // 首先往栈中推入了stage最后一个rdd waitingForVisit.push(stage.rdd) while (waitingForVisit.nonEmpty) { visit(waitingForVisit.pop()) } missing.toList }
同样的,我们画图来讲解提交调度阶段,如下图
提交任务
在submitStage中会执行submitMissingTasks方法中,会根据调度阶段partition个数生成对应个数task,这些任务组成一个任务集提交到TaskScheduler进行处理。
对于ResultStage生成ResultTask,对于ShuffleMapStage生成ShuffleMapTask。
private def submitMissingTasks(stage: Stage, jobId: Int) { ... // 为stage创建指定数量的task // 这里有一个很关键,就是task最佳位置的计算 val tasks: Seq[Task[_]] = try { stage match { case stage: ShuffleMapStage => partitionsToCompute.map { id => // 给每个partition创建一个task // 给每个task计算最佳位置 val locs = taskIdToLocations(id) val part = stage.rdd.partitions(id) new ShuffleMapTask(stage.id, stage.latestInfo.attemptId, taskBinary, part, locs, stage.latestInfo.taskMetrics, properties) } case stage: ResultStage => val job = stage.activeJob.get 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) } } } catch {...} if (tasks.size > 0) { logInfo("Submitting " + tasks.size + " missing tasks from " + stage + " (" + stage.rdd + ")") stage.pendingPartitions ++= tasks.map(_.partitionId) logDebug("New pending partitions: " + stage.pendingPartitions) // 提交taskSet,Standalone模式,使用的是TaskSchedulerImpl taskScheduler.submitTasks(new TaskSet( tasks.toArray, stage.id, stage.latestInfo.attemptId, jobId, properties)) stage.latestInfo.submissionTime = Some(clock.getTimeMillis()) } else { // Because we posted SparkListenerStageSubmitted earlier, we should mark // the stage as completed here in case there are no tasks to run // 如果调度阶段不存在任务标记,则标记调度阶段已经完成 markStageAsFinished(stage, None) ... } }
当TaskSchedulerImpl收到发送过来的任务集时,在submitTasks方法中构建一个TaskSetManager的实例,用于管理这个任务集的生命周期,而该TaskSetManager会放入系统的调度池中,根据系统设置的调度算法进行调度,
TaskSchedulerImpl.submitTasks方法代码如下:
override def submitTasks(taskSet: TaskSet) { val tasks = taskSet.tasks logInfo("Adding task set " + taskSet.id + " with " + tasks.length + " tasks") this.synchronized { // 为每一个taskSet创建一个taskSetManager // taskSetManager在后面负责,TaskSet的任务执行状况的监视和管理 val manager = createTaskSetManager(taskSet, maxTaskFailures) val stage = taskSet.stageId val stageTaskSets = taskSetsByStageIdAndAttempt.getOrElseUpdate(stage, new HashMap[Int, TaskSetManager]) // 把manager加入内存缓存中 stageTaskSets(taskSet.stageAttemptId) = manager val conflictingTaskSet = stageTaskSets.exists { case (_, ts) => ts.taskSet != taskSet && !ts.isZombie } if (conflictingTaskSet) { throw new IllegalStateException(s"more than one active taskSet for stage $stage:" + s" ${stageTaskSets.toSeq.map{_._2.taskSet.id}.mkString(",")}") } // 将该任务集的管理器加入到系统调度池中,由系统统一调配,该调度器属于应用级别 // 支持FIFO和FAIR两种,默认FIFO schedulableBuilder.addTaskSetManager(manager, manager.taskSet.properties) ... } // 在创建SparkContext,创建TaskScheduler的时候,创建了StandaloneSchedulerBackend,这个backend是负责 // 创建AppClient,向Master注册Application的, 详见Spark运行时消息通信 backend.reviveOffers() }
StandaloneSchedulerBackend的reviveOffers方法是继承于父类CoarseGrainedSchedulerBackend,该方法会向DriverEndpoint发送消息,调用makeOffers方法。在该方法中先会获取集群中可用的Executor,
然后发送到TaskSchedulerImpl中进行对任务集的任务分配运行资源,最后提交到launchTasks方法中。CoarseGrainedSchedulerBackend.DriverEndpoint.makeOffers代码如下:
private def makeOffers() { // 获取集群中可用的Executor列表 val activeExecutors = executorDataMap.filterKeys(executorIsAlive) // workOffers是每个Executor可用的cpu资源数量 val workOffers = activeExecutors.map { case (id, executorData) => new WorkerOffer(id, executorData.executorHost, executorData.freeCores) }.toSeq // 第一步:调用TaskSchedulerImpl的resourceOffers()方法,执行任务分配算法,将各个task分配到executor上去 // 第二步:分配好task到executor之后,执行自己的launchTasks方法,将分配的task发送LaunchTask消息到对应executor上去,由executor启动并执行 launchTasks(scheduler.resourceOffers(workOffers)) }
第一步:在TaskSchedulerImpl的resourceOffers()方法里进行非常重要的步骤--资源分配, 在分配过程中会根据调度策略对TaskSetMannager进行排序,
然后依次对这些TaskSetManager按照就近原则分配资源,按照顺序为PROCESS_LOCAL NODE_LOCAL NO_PREF RACK_LOCAL ANY
def resourceOffers(offers: Seq[WorkerOffer]): Seq[Seq[TaskDescription]] = synchronized { // Mark each slave as alive and remember its hostname // Also track if new executor is added var newExecAvail = false for (o <- offers) { executorIdToHost(o.executorId) = o.host executorIdToTaskCount.getOrElseUpdate(o.executorId, 0) if (!executorsByHost.contains(o.host)) { executorsByHost(o.host) = new HashSet[String]() executorAdded(o.executorId, o.host) newExecAvail = true } for (rack <- getRackForHost(o.host)) { hostsByRack.getOrElseUpdate(rack, new HashSet[String]()) += o.host } } // 首先,将可用的executor进行shuffle val shuffledOffers = Random.shuffle(offers) // tasks,是一个序列,其中的每个元素又是一个ArrayBuffer // 并且每个子ArrayBuffer的数量是固定的,也就是这个executor可用的cpu数量 val tasks = shuffledOffers.map(o => new ArrayBuffer[TaskDescription](o.cores)) val availableCpus = shuffledOffers.map(o => o.cores).toArray // 从rootPool中取出排序了的TaskSetManager // 在创建完TaskScheduler StandaloneSchedulerBackend之后,会执行initialize()方法,其实会创建一个调度池 // 这里就是所有提交的TaskSetManager,首先会放入这个调度池中,然后再执行task分配算法的时候,会从这个调度池中,取出排好队的TaskSetManager val sortedTaskSets = rootPool.getSortedTaskSetQueue // 如果有新加入的Executor,需要重新计算数据本地性 for (taskSet <- sortedTaskSets) { logDebug("parentName: %s, name: %s, runningTasks: %s".format( taskSet.parent.name, taskSet.name, taskSet.runningTasks)) if (newExecAvail) { taskSet.executorAdded() } } // Take each TaskSet in our scheduling order, and then offer it each node in increasing order // of locality levels so that it gets a chance to launch local tasks on all of them. // NOTE: the preferredLocality order: PROCESS_LOCAL, NODE_LOCAL, NO_PREF, RACK_LOCAL, ANY var launchedTask = false for (taskSet <- sortedTaskSets; maxLocality <- taskSet.myLocalityLevels) { do { // 对当前taskset尝试使用最小本地化级别,将taskset的task,在executor上进行启动 // 如果启动不了,就跳出这个do while,进入下一级本地化级别,一次类推 launchedTask = resourceOfferSingleTaskSet( taskSet, maxLocality, shuffledOffers, availableCpus, tasks) } while (launchedTask) } if (tasks.size > 0) { hasLaunchedTask = true } return tasks }
第二步:分配好资源的任务提交到CoarseGrainedSchedulerBackend的launchTasks方法中,在该方法中会把任务一个个发送到Worker节点上的CoarseGrainedExecutorBacken,
然后通过其内部的Executor来执行任务
// 根据分配好的tasks,在executor上启动相应的taskprivate def launchTasks(tasks: Seq[Seq[TaskDescription]]) { for (task <- tasks.flatten) { // 序列化 val serializedTask = ser.serialize(task) if (serializedTask.limit >= maxRpcMessageSize) { scheduler.taskIdToTaskSetManager.get(task.taskId).foreach { taskSetMgr => try { var msg = ... taskSetMgr.abort(msg) } catch { case e: Exception => logError("Exception in error callback", e) } } } else { val executorData = executorDataMap(task.executorId) executorData.freeCores -= scheduler.CPUS_PER_TASK // 向Worker节点的CoarseGrainedExecutorBackend发送消息执行Task executorData.executorEndpoint.send(LaunchTask(new SerializableBuffer(serializedTask))) } } }
我们继续通过图解来解释以上代码的调用过程,如下图所示:
在提交stage中,第一次调用的是ShuffleMapStage0和ShuffleMapStage1,假设都只有两个partition,ShuffleMapStage0是TaskSet0,
ShuffleMapStage1是TaskSet1,每个TaskSet都有两个任务在执行。TaskScheduler收到发送过来的任务集TaskSet0和TaskSet1,在submitTasks方法中分别构建TaskSetManager0和TaskSetManager1,并把它们两放到系统的调度池,
根据系统设置的调度算法进行调度(FIFO或者FAIR)在TaskSchedulerImpl的resourceOffers方法中按照就近原则进行资源分配,使用CoarseGrainedSchedulerBackend的launchTasks方法把任务发送到Worker节点上的
CoarseGrainedExecutorBackend调用其Executor来执行任务当ShuffleMapStage2同理,ResultStage3生成的是ResultTask
执行任务
当CoarseGrainedExecutorBackend接收到LaunchTask消息时,会调用Executor的launchTask方法进行处理。在Executor的launchTask方法中,初始化一个TaskRunner来封装任务,
它用于管理任务运行时的细节,再把TaskRunner对象放入到ThreadPool中去执行。TaskRunner.run的前半部分代码如下:
override def run(): Unit = { val taskMemoryManager = new TaskMemoryManager(env.memoryManager, taskId) val deserializeStartTime = System.currentTimeMillis() // 设置当前类加载器,使用类加载器的原因,用反射的方式来动态加载一个类,然后创建这个类的对象 Thread.currentThread.setContextClassLoader(replClassLoader) val ser = env.closureSerializer.newInstance() logInfo(s"Running $taskName (TID $taskId)") // 向DriverEndpoint发送状态更新信息 execBackend.statusUpdate(taskId, TaskState.RUNNING, EMPTY_BYTE_BUFFER) var taskStart: Long = 0 startGCTime = computeTotalGcTime() try { // 反序列化 val (taskFiles, taskJars, taskProps, taskBytes) = Task.deserializeWithDependencies(serializedTask) // Must be set before updateDependencies() is called, in case fetching dependencies // requires access to properties contained within (e.g. for access control). Executor.taskDeserializationProps.set(taskProps) updateDependencies(taskFiles, taskJars) task = ser.deserialize[Task[Any]](taskBytes, Thread.currentThread.getContextClassLoader) task.localProperties = taskProps task.setTaskMemoryManager(taskMemoryManager) // If this task has been killed before we deserialized it, let's quit now. Otherwise, // continue executing the task. if (killed) { throw new TaskKilledException } logDebug("Task " + taskId + "'s epoch is " + task.epoch) env.mapOutputTracker.updateEpoch(task.epoch) // Run the actual task and measure its runtime. taskStart = System.currentTimeMillis() var threwException = true // value对于shuffleMapTask来说,其实就是MapStatus,封装了shuffleMapTask计算的数据,输出的位置 val value = try { val res = task.run( // 具体实现在ShuffleMapTask和ResultTask中 taskAttemptId = taskId, attemptNumber = attemptNumber, metricsSystem = env.metricsSystem) threwException = false res } finally {...} ...
对于ShuffleMapTask, 它的计算结果会写到BlockManager之中,最终返回给DAGScheduler的是一个MapStatus。该对象管理了ShuffleMapTask的运算结果存储到BlockManager里的相关存储信息,而不是计算结果本身,
这些存储信息将会成为下一阶段的任务需要获得的输入数据时的依据。ShuffleMapTask.runTask代码如下:
override def runTask(context: TaskContext): MapStatus = { // 反序列化获取rdd和rdd的依赖 // 通过broadcast variable拿到rdd的数据 val deserializeStartTime = System.currentTimeMillis() val ser = SparkEnv.get.closureSerializer.newInstance() val (rdd, dep) = ser.deserialize[(RDD[_], ShuffleDependency[_, _, _])]( ByteBuffer.wrap(taskBinary.value), Thread.currentThread.getContextClassLoader) _executorDeserializeTime = System.currentTimeMillis() - deserializeStartTime var writer: ShuffleWriter[Any, Any] = null try { // 从shuffleManager中获取shuffleWriter val manager = SparkEnv.get.shuffleManager writer = manager.getWriter[Any, Any](dep.shuffleHandle, partitionId, context) // rdd.iterator传入当前task要处理的哪个partition,执行我们自己定义的算子或者是函数 // 如果rdd已经cache或者checkpoint,那么直接读取,否则计算,计算结果保存在本地系统的blockmanager中 writer.write(rdd.iterator(partition, context).asInstanceOf[Iterator[_ <: Product2[Any, Any]]]) // 返回结果MapStatus,其实就是blockmanager相关信息 writer.stop(success = true).get } catch {...}
ResultTask的runTask方法如下,它返回的是func函数的计算结果
override def runTask(context: TaskContext): U = { // Deserialize the RDD and the func using the broadcast variables. val deserializeStartTime = System.currentTimeMillis() val ser = SparkEnv.get.closureSerializer.newInstance() val (rdd, func) = ser.deserialize[(RDD[T], (TaskContext, Iterator[T]) => U)]( ByteBuffer.wrap(taskBinary.value), Thread.currentThread.getContextClassLoader) _executorDeserializeTime = System.currentTimeMillis() - deserializeStartTime func(context, rdd.iterator(partition, context)) }
获取执行结果
对于Executor的计算结果,会根据结果的大小有不同的策略
生成结果大于1GB,直接丢弃,可以通过spark.driver.maxResultSize来配置
生成结果大小在[128MB-200kB, 1GB], 会把结果以taskId为编号存入到BlockManager中,然后把编号通过Netty发送给DriverEndpoint,Netty传输框架最大值和预留空间的差值
生成结果大小在[0, 128MB-200KB],通过Netty直接发送到DriverEndpoint。
具体执行在TaskRunner的run方法后半部分:
// 对生成的结果序列化,并将结果放入DirectTaskResult中 val resultSer = env.serializer.newInstance() val beforeSerialization = System.currentTimeMillis() val valueBytes = resultSer.serialize(value) val afterSerialization = System.currentTimeMillis() // Note: accumulator updates must be collected after TaskMetrics is updated val accumUpdates = task.collectAccumulatorUpdates() val directResult = new DirectTaskResult(valueBytes, accumUpdates) val serializedDirectResult = ser.serialize(directResult) val resultSize = serializedDirectResult.limit // directSend = sending directly back to the driver val serializedResult: ByteBuffer = { if (maxResultSize > 0 && resultSize > maxResultSize) { // 大于1G,直接丢弃 ser.serialize(new IndirectTaskResult[Any](TaskResultBlockId(taskId), resultSize)) } else if (resultSize > maxDirectResultSize) { val blockId = TaskResultBlockId(taskId) env.blockManager.putBytes( blockId, new ChunkedByteBuffer(serializedDirectResult.duplicate()), StorageLevel.MEMORY_AND_DISK_SER) // IndirectTaskResult间接结果 ser.serialize(new IndirectTaskResult[Any](blockId, resultSize)) } else { // 直接发送 serializedDirectResult } } // CoraseGrainedExecutorBackend的statusUpdate方法 execBackend.statusUpdate(taskId, TaskState.FINISHED, serializedResult)