Spark中RDD DAG图的建立

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/lihm0_1/article/details/49665945

   RDD是spark计算的核心,是分布式数据元素的集合,具有不可变、可分区、可被并行操作的特性,基础的RDD类包含了常用的操作,如果需要特殊操作可以继承RDD基类进行自己的扩展,基础预算包括map、filter、reduce等。
  RDD包含5个主要特性:partition、针对split的算子、自身依赖哪些RDD、分区类型(默认hash)、split计算是的分区位置(例如计算HDFS block的时候,让计算分配在block所在机器会减少网络数据的传输)
 如果需要自定义一个RDD,通常需要实现如下方法:

//对一个partition做何种运算
def compute(split: Partition, context: TaskContext): Iterator[T]

//获得该RDD的所有分区
protected def getPartitions: Array[Partition]

//获得该RDD有哪些依赖
protected def getDependencies: Seq[Dependency[_]] = deps

//获得partition的最佳位置
protected def getPreferredLocations(split: Partition): Seq[String] = Nil

//使用何种分区
@transient val partitioner: Option[Partitioner] = None

    在一个作业提交给dagscheduler前,要先构建RDD的DAG图,构建过程需要建立依赖关系,在spark中有宽依赖和窄依赖之分,下面我们通过一个具体的计算链条来分析这个过程,代码如下:
val count = sc.parallelize(1 to 5, 2).map((_,1)).reduceByKey(_+_).collect().foreach(println)
   这段逻辑非常简单,构建1到5的数字集合,然后做一个wordcount操作,最后打印结果。
 执行过程从左到右,首先执行parallelize操作:
def parallelize[T: ClassTag](seq: Seq[T], numSlices: Int = defaultParallelism): RDD[T] = {
  assertNotStopped()
  new ParallelCollectionRDD[T](this, seq, numSlices, Map[Int, Seq[String]]())
}
   返回的是一个ParalleCollectionRDD的RDD,这里只是构建一个RDD的对象,还并未运行任何计算,继续往下看会发现在action触发前,也就是job submit之前是不进行任何计算的,直到job提交后才会进行,这就是RDD的延迟计算。后面进行map操作最终返回MapPartitionsRDD
def map[U: ClassTag](f: T => U): RDD[U] = {
  val cleanF = sc.clean(f)
  new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))
}
   注意上面代码的this,是上一个RDD,也就是ParallelCollectionRDD进入MapPartitionsRDD的构造函数可以看出,这个RDD其实是当前RDD的parent RDD,代码如下:
private[spark] class MapPartitionsRDD[U: ClassTag, T: ClassTag](
    prev: RDD[T],
    f: (TaskContext, Int, Iterator[T]) => Iterator[U],  // (TaskContext, partition index, iterator)
    preservesPartitioning: Boolean = false)
  extends RDD[U](prev) {
 
  override val partitioner = if (preservesPartitioning) firstParent[T].partitioner else None
 
  override def getPartitions: Array[Partition] = firstParent[T].partitions
 
  override def compute(split: Partition, context: TaskContext) =
    f(context, split.index, firstParent[T].iterator(split, context))
}
这样RDD之间的依赖关系就构建好了,进行reducebykey操作前会进行隐式转换,将RDD转化为PairRDDFunctions,然后进行reduceByKey操作。
    collect是一个action算子,上述map、reduce操作属于transform操作,只负责对RDD打转化标记,并不执行真正的计算。而action操作会导致作业提交至集群,由调度器做好切分stage后进行调度。
def collect(): Array[T] = {
  val results = sc.runJob(this, (iter: Iterator[T]) => iter.toArray)
  Array.concat(results: _*)
}
   runJob会触发作业的提交,客户端通过向AKKA服务器发送提交作业的消息,来提交作业,代码如下:
def submitJob[T, U](
    rdd: RDD[T],
    func: (TaskContext, Iterator[T]) => U,
    partitions: Seq[Int],
    callSite: CallSite,
    allowLocal: Boolean,
    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 new JobWaiter[U](this, jobId, 0, resultHandler)
  }
 
  assert(partitions.size > 0)
  val func2 = func.asInstanceOf[(TaskContext, Iterator[_]) => _]
  val waiter = new JobWaiter(this, jobId, partitions.size, resultHandler)
  eventProcessLoop.post(JobSubmitted(
    jobId, rdd, func2, partitions.toArray, allowLocal, callSite, waiter, properties))
  waiter
}
作业提交的消息中包含作业ID、最终的RDD,partitions等主要信息,注意这里的RDD是最终的RDD,因为通过这个RDD可以反推出所有依赖的RDD,所以提交时只要这一个就够了,后面我们会分析作业提交服务端是如何处理这个RDD的。


展开阅读全文

没有更多推荐了,返回首页