关于Spark中RDD的思考和总结

     (代码基于Spark-core 1.2.0)

    本来这篇想结合自己的经验讨论shuffle,但是shuffle讨论之前还是准备先讨论一下关于RDD的问题。 网上介绍RDD的我看过的有:

0、 http://www.cs.berkeley.edu/~matei/papers/2012/nsdi_spark.pdf Spark paper 这个是设计时候的paper

1、 https://www.zybuluo.com/jewes/note/35032 

2、 一个介绍RDD甚好的PPT,之后我会发在我的云盘。 

    RDD学名Resilient Distributed Dataset,"Resillient"表示它的容错能力,“Distributed”说明它是分布式的实现,"Dataset"说明它是基于集合操作的, RDD更准确的说是一个接口,主要由5个接口或者说特性组成:

compute             :在Action调用,生成一个Job时,RDD如果没有Cache的时候调用这个函数

getPartitions       :Array[Partitions] 每个分区由 index标示

getDependencies    : RDD的父RDDs, 通常RDD有parent RDD,除了HadoopRDD以及类似RDD,这些是数据源头,所有RDD的关系构成RDD的lineage,一般翻译成血缘关系

getPreferredLocations 

partitioner

    在这里不会详细介绍RDD的方法面面,Spark的运行启动时的先后顺序,以及运行时各个模块的交互在网上能找到很多的文章,如果有时间,不妨认真看看51CTO上王家林老师的免费课程:

http://edu.51cto.com/index.php?do=lession&id=30815 我刚接触Spark时看了这个视频,收获很多。

    总体而言,Spark的主要接口分为transformation以及action,action触发Job的执行。 job执行时

1、sparkContext调用dagscheduler划分Stage,Stage划分的依据是shuffle,shuffle前后属于不同的Stage。 2、Stage封装成TaskSet,提交给taskScheduler。

3、taskScheduler调用backend,获取可用的Executor信息, 对于每一个TaskSet中的Task分配一个Executor的core。

4、CoarseGrainedSchedulerBackend调用launchTasks(tasks: Seq[Seq[TaskDescription]]),每个ExecutorActor会收到launchTask消息,在线程池中增加执行这个Task的TaskRunner线程。

5、TaskRunner反序列化TaskDescription,执行Task, Task的子类包括ResultTask,和ShuffleMapTask, ResultTask 的主要工作是执行 func(context, rdd.iterator(partition, context)) func是我们写的Spark程序的action函数。ShuffleMapTask则是拉取Shuffle保存的数据,主要的逻辑是 fetchIterator-> iterator.read。

    每个人看源码时碰到想到的问题会是不一样的,我碰到的两个具体问题分别是:

1、 每个rdd的compute方法是如何工作的。 

2、 每个stage有parent stages, 但是为何stage包含的是Option[shuffleDependency],而rdd包含一个对象Seq[Dependency]

    这两个问题难度一般,主要是对于rdd和stage生成过程不清晰导致的。 rdd的生成过程是从前向后

val text = sc.textFile("some/path/hold/data")
val rdd 1 = text.flatmap(_.split(" "))
val rdd2 = rdd1.map(w=>(w,1))
val rdd3 = rdd2.reduceByKey(_+_)

    那么存在 text->rdd1->rdd2->rdd3的lineage。而stage的产生是DagScheduler从后向前划分产生的。从后向前的代码依次为: 

var finalStage: Stage = null
    try {
      // New stage creation may throw an exception if, for example, jobs are run on a
      // HadoopRDD whose underlying HDFS files have been deleted.
      finalStage = newStage(finalRDD, partitions.size, None, jobId, callSite)

private def newStage(
      rdd: RDD[_],
      numTasks: Int,
      shuffleDep: Option[ShuffleDependency[_, _, _]],
      jobId: Int,
      callSite: CallSite)
    : Stage =
  {
    val parentStages = getParentStages(rdd, jobId)
    val id = nextStageId.getAndIncrement()
    val stage = new Stage(id, rdd, numTasks, shuffleDep, parentStages, jobId, callSite)
    stageIdToStage(id) = stage
    updateJobIdStageIdMaps(jobId, stage)
    stage
  }

private def getParentStages(rdd: RDD[_], jobId: 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
        // Kind of ugly: need to register RDDs with the cache here since
        // we can't do it in its constructor because # of partitions is unknown
        for (dep <- r.dependencies) {
          dep match {
            case shufDep: ShuffleDependency[_, _, _] =>
              parents += getShuffleMapStage(shufDep, jobId)
            case _ =>
              waitingForVisit.push(dep.rdd)
          }
        }
      }
    }
    waitingForVisit.push(rdd)
    while (!waitingForVisit.isEmpty) {
      visit(waitingForVisit.pop())
    }
    parents.toList
  }
 private def getShuffleMapStage(shuffleDep: ShuffleDependency[_, _, _], jobId: Int): Stage = {
    shuffleToMapStage.get(shuffleDep.shuffleId) match {
      case Some(stage) => stage
      case None =>
        // We are going to register ancestor shuffle dependencies
        registerShuffleDependencies(shuffleDep, jobId)
        // Then register current shuffleDep
        val stage =
          newOrUsedStage(
            shuffleDep.rdd, shuffleDep.rdd.partitions.size, shuffleDep, jobId,
            shuffleDep.rdd.creationSite)
        shuffleToMapStage(shuffleDep.shuffleId) = stage
 
        stage
    }
  }
private def newOrUsedStage(
      rdd: RDD[_],
      numTasks: Int,
      shuffleDep: ShuffleDependency[_, _, _],
      jobId: Int,
      callSite: CallSite)
    : Stage =
  {
    val stage = newStage(rdd, numTasks, Some(shuffleDep), jobId, callSite)
  // (之后代码省略)

     从代码可以看出 Stage类里的shuffleDep从后往前生成的逻辑,结论是 newStage完成了所有stage的划分逻辑! 看了这个函数名第一反应以为只是new Stage的作用。 这样逻辑大致理清了,剩下的关键点是 shuffleDependency是谁创建的。 shuffleDependency一定和rdd的生成有关,所以随便打开一个发生了shuffle的RDD类 就看到: 

(CoGroupedRDD)
override def getDependencies: Seq[Dependency[_]] = {
    rdds.map { rdd: RDD[_ <: Product2[K, _]] =>
      if (rdd.partitioner == Some(part)) {
        logDebug("Adding one-to-one dependency with " + rdd)
        new OneToOneDependency(rdd)
      } else {
        logDebug("Adding shuffle dependency with " + rdd)
        new ShuffleDependency[K, Any, CoGroupCombiner](rdd, part, serializer)
      }
    }
  }

    至此,DAGScheduler生成Stage问题梳理清楚了。 

     第二个问题是compute的作用。这个比较简单

(RDD)
final def iterator(split: Partition, context: TaskContext): Iterator[T] = {
    if (storageLevel != StorageLevel.NONE) {
      SparkEnv.get.cacheManager.getOrCompute(this, split, context, storageLevel)
    } else {
      computeOrReadCheckpoint(split, context)
    }
  }

private[spark] def computeOrReadCheckpoint(split: Partition, context: TaskContext): Iterator[T] =
  {
    if (isCheckpointed) firstParent[T].iterator(split, context) else compute(split, context)
  }

   

     比如MappedRDD 

override def compute(split: Partition, context: TaskContext) =
    firstParent[T].iterator(split, context).map(f)
}

    又如ShuffledRDD

override def compute(split: Partition, context: TaskContext): Iterator[(K, C)] = {
    val dep = dependencies.head.asInstanceOf[ShuffleDependency[K, V, C]]
    SparkEnv.get.shuffleManager.getReader(dep.shuffleHandle, split.index, split.index + 1, context)
      .read()
      .asInstanceOf[Iterator[(K, C)]]
  }

     这里插一句 shuffledRDD是shuffle完成之后,向shuffleManager去取数据,而写数据则是在ShuffleMapTask中。 这将在以后一节详细介绍。

下节介绍Spark的Shuffle过程

    

猜你喜欢

转载自desmoon.iteye.com/blog/2190339