某小伙的Spark奇妙之旅-DAG

Spark DAG

在学习Spark的过程中,会遇到SparkDag这个概念

Dag是一个有向无环图的缩写,他的意思是把Spark中调用各种RDD的过程,转化成一种Dag的形式

那么为什么要转化成DAG呢?

其实不是为什么要转化成DAG,而是spark的那种调度机制十分的适合DAG,因为spark的rdd调用是lazy的,所以他需要先记录每个rdd之间的依赖关系,防止执行过程中出错了可以根据那个依赖关系取溯源

既然每个RDD之间都是有依赖关系了,除了最开始的rdd之外,那么就很符合DAG(有向无环图)这个理念了

什么是DAG,就是一种图结构,多个顶点之间通过边连成的图

为什么是有向的图

由于不同顶点(rdd)是由依赖关系的,所以顶点之间的连线的边是有方向的

为什么是无环的图

如果是有环的话,说明不同顶点之间可能存在循环依赖比如:A->B->C->A,这样的话,就会导致死循环,找不到执行的切入点

如果是无环的话,可以通过判断每个顶点是否有依赖父级顶点,来筛选出最开始的切入顶点

在这里插入图片描述

RDD之间的关系会生成这样的一个图,记录了每个RDD之间的依赖关系,可以发现你要执行RDD5不可能依赖RDD6,因为RDD6是通过RDD4和RDD5的结果转换得来的,所以他们之间形成的链必须是无环的,所以完全符合DAG的概念

那生成这样的一个图之后有什么用呢?

这就涉及到了执行计划,光是记录每个RDD之间的依赖关系并不能直接调度执行,而是需要每个RDD之间的先后执行顺序,将图的关系依赖,转化成线性先后排序

在DAG中有一种独特的排序方法,叫做拓扑排序

什么是拓扑排序?

在有向图中,边都是有向的,所以存在每个顶点都拥有入度和出度这样的属性来表示是否有边指向你(是否有依赖别人)

拓扑排序就是找出入度为0,作为顶点,将其以及其输出的边都是删除,作为第一个执行的顶点

在这里插入图片描述

在这里插入图片描述

以此类推,就可以通过这种方式,将每个RDD的执行顺序给记录下来,如图如果遇到两个顶点同时没有输入时,可以通过其它方式进行优先级判定,但是其实以及不影响了,以为他们之间没有依赖关系,所以不影响结果

下面我们从Spark的源码角度来看DAG到底是如何实现的

以一个wordCount的例子来看

object WordCount {
  def main(args: Array[String]): Unit = {

    val spark = SparkSession
      .builder
      .master("local[6]")
      .appName("Spark Examples")
      .getOrCreate()


    val sc = spark.sparkContext
    sc.setLogLevel("WARN")
    val rdd1: RDD[String] = sc.textFile("examples/data/",3)
    val rdd2 = rdd1.flatMap(item=>item.split(" "))
    val rdd3 = rdd2.map(item=>(item,1))
    val rdd4 = rdd3.reduceByKey((curr,agg)=>curr+agg)
    sc.setCheckpointDir("D:\\ProgramFiles\\spark-2.2.0-src\\spark-2.2.0\\examples\\src\\main\\scala\\it\\luke\\spark\\Data\\checkpointDir")
    rdd3.checkpoint()
    val res = rdd4.collect()
    res.foreach(println(_))

  }
}

最开始textfile进行读取文件时构建了一个hadoopRdd

 def textFile(
      path: String,
      minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {
    //defaultMinPartitions 如果不传值默认最小分区数
    assertNotStopped()
     //通过hadoopfile构建hadoopRDD
    hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text], minPartitions)
     //将读取到的数据通过map转化成MapPartitionsRDD
      .map(pair => pair._2.toString)
      .setName(path)
  }
@DeveloperApi
class HadoopRDD[K, V](
    sc: SparkContext,
    broadcastedConf: Broadcast[SerializableConfiguration],
    initLocalJobConfFuncOpt: Option[JobConf => Unit],
    inputFormatClass: Class[_ <: InputFormat[K, V]],
    keyClass: Class[K],
    valueClass: Class[V],
    minPartitions: Int)
// RDD[(K, V)](sc, Nil) 继承了一个RDD抽象类并传入了当前的sc对象和Nil空序列
  extends RDD[(K, V)](sc, Nil) with Logging {
  ...
  }

从RDD的构造函数种可以看出,传入的第二个参数是用来确定依赖关系用的,由于是初始化,所以默认没有依赖关系,所以为空

//由于传入了Nil空序列,所以当前的依赖为空
abstract class RDD[T: ClassTag](
    @transient private var _sc: SparkContext,
    @transient private var deps: Seq[Dependency[_]]
  ) extends Serializable with Logging

回过头来看构建完HadoopRDD后又调用了map(这是在之前的textfile里面调用的)

来看一下map是如何构建MapPartitionsRDD的?

 def map[U: ClassTag](f: T => U): RDD[U] = withScope {
    val cleanF = sc.clean(f)
    //这里构建MapPartitionsRDD,将当前的RDD(this)作为构建他的前一个记录依赖关系
    new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))
  }
private[spark] class MapPartitionsRDD[U: ClassTag, T: ClassTag](
    var prev: RDD[T],
    f: (TaskContext, Int, Iterator[T]) => Iterator[U],  // (TaskContext, partition index, iterator)
    preservesPartitioning: Boolean = false)
  //将传入的RDD 作为构建新rdd的父级RDD记录依赖关系RDD[U](prev)
  extends RDD[U](prev) {

  override val partitioner = if (preservesPartitioning) firstParent[T].partitioner else None
  ....
  }
  /** Construct an RDD with just a one-to-one dependency on one parent
    * 构造一个RDD,只对一个父一个一对一的依赖*/
  def this(@transient oneParent: RDD[_]) =
    this(oneParent.context, List(new OneToOneDependency(oneParent)))
//这里新建了一个对象,作为存储依赖对象

这里新建了OneToOneDependency这个类,并将父级的RDD 传入

/**
 * :: DeveloperApi ::
 * Represents a one-to-one dependency between partitions of the parent and child RDDs.
  * 表示父RDDs和子RDDs分区之间的一对一依赖关系  也就是窄依赖
  这里主要是是重写了父类的方法
 */
@DeveloperApi
class OneToOneDependency[T](rdd: RDD[T]) extends NarrowDependency[T](rdd) {
  override def getParents(partitionId: Int): List[Int] = List(partitionId)
}

我们可以看看继承的类NarrowDependency(rdd)


abstract class NarrowDependency[T](_rdd: RDD[T]) extends Dependency[T] {
  /**
   * Get the parent partitions for a child partition.
   * @param partitionId a partition of the child RDD
   * @return the partitions of the parent RDD that the child partition depends upon
   */
  def getParents(partitionId: Int): Seq[Int]
//作为一个rdd的容器,存储了父级RDD的引用
  override def rdd: RDD[T] = _rdd
}

走到这步,这个RDD基本将依赖关系存储完了,捋一下,

最开始通过textfile构建的是HadoopRDD,

然后将采集到的数据集通过map生成了一个新的MapPartitionsRDD

所以当前的RDD为MapPartitionsRDD 且依赖于上一个hadoopRDD

我们通过调试来一下

在这里插入图片描述

这样一个RDD的依赖链就构建完成了,后续的多个RDD也是如此如此这般这般

在多个RDD构建完之后,他们之间的依赖关系就形成了DAG有向无环图

然后在对RDD进行拓扑,就可以得到每个任务的执行先后顺序了

后续在调用action算子的时候,就可以高效的调用了

思考:这种是通过动态判断的依赖关系,如果提供一种通道,让调用者可以传入依赖关系,是不是就可以提高效率,减少对依赖关系的遍历判断?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值