(代码基于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过程