Spark盖中盖(一篇顶五篇)-3 DAG详解

前方高能,减速慢行!

看过了Spark的核心RDD和RDD算子以后,感觉Spark是不是很神奇,竟然可以减少计算次数,优化计算。对于窄依赖是这样的,但是宽依赖怎么办?还是要等待宽依赖的结果才能继续计算。Spark引入了DAG来记录宽依赖的计算过程,这样在遇到宽依赖的时候,我们就可以一边记录当前的计算过程一边计算Stage中的窄依赖了。下面我们就来看看DAG是什么。
一、什么是DAG?
二、spark框架中DAG是做什么的?
三、DAG过程详解及源码解读
四、yarn提交任务的2种模式

一、什么是DAG?
DAG(Directed Acyclic Graph)有向无环图。
对于这个概念只有数学专业的运筹学和计算机专业的数据结构的图中讲到了DAG,所以我先普及一下理论。
图是数据结构中最复杂的,泛指无组织的结构体由关联关系形成图。DAG(有向无环图)是拓扑图的一种,比较有规律,适合做血缘关系描述。深度挖掘,机器学习,统计学都是跟图有关的,由于图不像其他数据结构一样有一个合理的组织,因此寻址的时间复杂度和空间复杂度一般都超过O(n的平方),就是要用双倍的内存或时间来查找整个图。这里给大家提供一个DAG排序的效率:500点的拓扑排序java实现要200毫秒,内存使用10M左右。或者做算法的逻辑内存转换 300毫秒 7M内存。
二、spark计算框架中DAG是做什么的?
RDD算子构建了RDD之间的关系,整个计算过程形成了一个由RDD和关系构成的DAG。
简单理解为点和线构成图。
点是某种数据结构,spark计算框架的点就是RDD(或者stage)
线就是关联关系,spark计算框架的线就是RDD算子(或者宽依赖算子)
spark计算框架中DAG分2种,getShuffleDependencies DAG 和 getMissingParentStages DAG。
getShuffleDependencies :

getShuffleDependencies

getMissingParentStages:

getMissingParentStages

仔细看上两张图,图2为图1的一部分。
这2种DAG的关系就是如此,外层的DAG遍历过程如果遇到内层 DAG直接跳入内层检索,整个过程可以看作一个大的DAG,实际作用是检索RDD数据内存位置或磁盘位置的过程。
它们的执行过程是这样的:
1,任务触发是有Action 算子触发(详见上一篇算子),查找的入口就是action算子前的最后一个RDD(finalRDD图中:RDD G),开始遍历DAG。
2,getShuffleDependencies遍历宽依赖关系,遇到窄依赖放入HashMap visit,遇到宽依赖时直接跳转到第3步。如果结束返回完成。
3,getMissingParentStages遍历窄依赖,创建resultstage(方法createResultStage),遇到窄依赖放入HashMap visit,遇到宽依赖递归第2步骤 。如果结束跳入上一步继续检索。
4,遇到可计算的stage (finalStage ),可计算指的是stage中所有RDD都可以寻址到内存或磁盘,提交stage任务到taskscheduler,taskscheduler负责分发任务到集群的worker中计算。如果结束跳入上一步继续检索。
注:2 3 步中分别维护自己的HashMap visit ,遇到宽依赖创建resultstage,这也正是上一篇中为什么宽依赖是划分stage边界的原因。

三、DAG过程详解及源码解读

DAGScheduler

getOrCreateShuffleMapStage
如上图:(只提供关键代码)
1,submitjob即任务提交到driver端,并由driver端根据RDD和算子初始化RDD的依赖关系。并调用DAGScheduler的runJob方法,runJob方法创建waiter成为DAG submitJob的开始。

  def runJob[T, U](……){
……
val waiter = submitJob(rdd, func, partitions, callSite, resultHandler, properties)
……
}```
2,submitJob方法分为3部分。
判断partition大小不在0到maxpartitions范围内抛出异常。

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)
}```
Partition大小为0,返回空对象。

  if (partitions.size == 0) {
      return new JobWaiter[U](this, jobId, 0, resultHandler)
}```
Partition大于0则由eventProcessLoop注册DAGSchedulerEventProcessLoop并由onReceive匹配submit的提交格式。

3,作业提交到入口DAGScheduler,由DAGSchedulerEventProcessloop类判断提交任务的类型,由onReceive选择出对应的Handler(如:handleJobSubmitted,MapStageSubmitted等10几个分支)。如图上半部分DAGSchedulerEventProcessloop

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)
case MapStageSubmitted(jobId, dependency, callSite, listener, properties) =>
dagScheduler.handleMapStageSubmitted(jobId, dependency, callSite, listener, properties)
case ……
}```
4,当前为submitjob,所以调用JobSubmitted ,由handleJobSubmitted方法初始化job,DAG的开始是上一节讲到的最后一个RDD(finalRDD),基于这个finalRDD创建一个finalStage,并创建一个待提交的任务ActiveJob。

var finalStage: ResultStage = null 
finalStage = createResultStage(finalRDD, func, partitions, jobId, callSite)
val job = new ActiveJob(jobId, finalStage, callSite, listener, properties)

5,DAGsort 即为上一节讲到的4个过程。

  private def getMissingAncestorShuffleDependencies(
      rdd: RDD[_]): Stack[ShuffleDependency[_, _, _]] = {
    val ancestors = new Stack[ShuffleDependency[_, _, _]]
    val visited = new HashSet[RDD[_]]
      val waitingForVisit = new Stack[RDD[_]]
    waitingForVisit.push(rdd)
    while (waitingForVisit.nonEmpty) {
      val toVisit = waitingForVisit.pop()
      if (!visited(toVisit)) {
        visited += toVisit
        getShuffleDependencies(toVisit).foreach { shuffleDep =>
          if (!shuffleIdToMapStage.contains(shuffleDep.shuffleId)) {
            ancestors.push(shuffleDep)
            waitingForVisit.push(shuffleDep.rdd)
          }         }
      }
    }
    ancestors
  }```
submitstage把整个stage打包发给TaskScheduler,并有封装成taskset,分发给集群的worker计算出结果。

四、yarn提交任务的2种模式
在yarn中任务的提交分2种方式:client 模式和cluster 模式。对应的参数是:--deploy-mode client或者cluster,默认client模式。
这两种模式的区别在于driver运行在哪里。如图:
 
![yarn](http://upload-images.jianshu.io/upload_images/4115103-5fb2bfc964a780c4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
首先我们了解一下上图中的几个模块和它们的作用:
1,  Client,spark的客户端,用来提交spark任务。
2,  Driver,用来接收client提交的任务,构建lineage(RDD血缘关系),DAG检索,序列化,注册应用,提交task执行任务,与worker节点通信(master提交完任务worker会反注册到driver端)。
3,  Master,计算框架主节点(yarn的applicationMaster)。负责任务分配和管理。
4,  Worker,计算框架工作节点,spark计算框架中的worker内部关系是:worker= executer =多个task
在Yarn中(参数是--master yarn)执行任务的2种模式:
Client模式:driver在applicationMaster端。
Cluster模式:dirver是在worker端。
到这里关于本期DAG的讲解就结束了,不知各位是否有头晕眼花手指酸的感觉,希望本文对您能有些许帮助。因本人理论水平和写作能力有限,有些表达歧义或错漏还请谅解,此理论仅为引导,您的实践才是检验真理的唯一标准。后续我们会继续聊一聊spark的其他事儿,还请关注,后会有期!

推荐阅读更多精彩内容