Spark源码分析(1) 从WordCount示例看Spark延迟计算原理

转自:http://blog.csdn.net/josephguan/article/details/25649239

WordCount示例:

val file = spark.textFile("hdfs://...")
val counts = file.flatMap(line => line.split(" "))
                 .map(word => (word, 1))
                 .reduceByKey(_ + _)
counts.saveAsTextFile("hdfs://...")

在执行counts.saveAsTextFile("hdfs://...")前,spark其实没有进行真正的运算,只是在构造计算的过程——有点类似Scala中的lazy运算。当执行counts.saveAsTextFile才真正开始了整个计算。

源代码分析:

从Spark源码看,这种延迟计算的实现原理是如此的简单、明了!简单来说,原理如下:
每个运算(如flatMap、map)其实返回的都是一个RDD对象,可以认为最后形成了一个RDD对象的队列;直到最后需要计算时(例如调用了saveAsTextFile)才开始逐一调用各个RDD对象的compute方法,完成实际的运算。

具体分析:

1. spark.textFile("hdfs://...")返回了什么?
  文件:/core/src/main/scala/org/apache/spark/SparkContext.scala

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. def textFile(path: String, minSplits: Int = defaultMinSplits): RDD[String] = {  
  2.   hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text],  
  3.     minSplits).map(pair => pair._2.toString)  
  4. }  
  5.   
  6. def hadoopFile[K, V](...): RDD[(K, V)] = {  
  7.   new HadoopRDD(...)  
  8. }   

  可见,执行spark.textFile("hdfs://...")其实并没有真正的打开文件或做什么实际的操作,而只是返回了一个HadoopRDD的对象。

2. map、flatMap做了什么?
  文件:core/src/main/scala/org/apache/spark/rdd/RDD.scala

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. def map[U: ClassTag](f: T => U): RDD[U] = new MappedRDD(this, sc.clean(f))  
  2. def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U] =  
  3.   new FlatMappedRDD(this, sc.clean(f))  

  就是这么简单,map返回一个MappedRDD,flatMap返回一个FlatMappedRDD对象。他们都是RDD对象的子类。

3. reduceByKey做了什么?
  reduceByKey不是RDD的一个成员函数,它定义在PairRDDFunctions类中。RDD会被隐式转换为PairRDDFunctions。

  注:隐式转换的方法定义在文件:/core/src/main/scala/org/apache/spark/SparkContext.scala中。
  implicit def rddToPairRDDFunctions[K: ClassTag, V: ClassTag](rdd: RDD[(K, V)]) =
    new PairRDDFunctions(rdd)

  reduceByKey的定义在文件:/core/src/main/scala/org/apache/spark/rdd/PairRDDFunctions.scala

  

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. def reduceByKey(func: (V, V) => V): RDD[(K, V)] = {  
  2.     reduceByKey(defaultPartitioner(self), func)  
  3.   }  
  4.   def reduceByKey(partitioner: Partitioner, func: (V, V) => V): RDD[(K, V)] = {  
  5.     combineByKey[V]((v: V) => v, func, func, partitioner)  
  6.   }  
  7.   def combineByKey[C](createCombiner: V => C,  
  8.       mergeValue: (C, V) => C,  
  9.       mergeCombiners: (C, C) => C,  
  10.       partitioner: Partitioner,  
  11.       mapSideCombine: Boolean = true,  
  12.       serializerClass: String = null): RDD[(K, C)] = {  
  13.     require(mergeCombiners != null"mergeCombiners must be defined"// required as of Spark 0.9.0  
  14.     if (getKeyClass().isArray) {  
  15.       if (mapSideCombine) {  
  16.         throw new SparkException("Cannot use map-side combining with array keys.")  
  17.       }  
  18.       if (partitioner.isInstanceOf[HashPartitioner]) {  
  19.         throw new SparkException("Default partitioner cannot partition array keys.")  
  20.       }  
  21.     }  
  22.     val aggregator = new Aggregator[K, V, C](createCombiner, mergeValue, mergeCombiners)  
  23.     if (self.partitioner == Some(partitioner)) {  
  24.       self.mapPartitionsWithContext((context, iter) => {  
  25.         new InterruptibleIterator(context, aggregator.combineValuesByKey(iter, context))  
  26.       }, preservesPartitioning = true)  
  27.     } else if (mapSideCombine) {  
  28.       val combined = self.mapPartitionsWithContext((context, iter) => {  
  29.         aggregator.combineValuesByKey(iter, context)  
  30.       }, preservesPartitioning = true)  
  31.       val partitioned = new ShuffledRDD[K, C, (K, C)](combined, partitioner)  
  32.         .setSerializer(serializerClass)  
  33.       partitioned.mapPartitionsWithContext((context, iter) => {  
  34.         new InterruptibleIterator(context, aggregator.combineCombinersByKey(iter, context))  
  35.       }, preservesPartitioning = true)  
  36.     } else {  
  37.       // Don't apply map-side combiner.  
  38.       val values = new ShuffledRDD[K, V, (K, V)](self, partitioner).setSerializer(serializerClass)  
  39.       values.mapPartitionsWithContext((context, iter) => {  
  40.         new InterruptibleIterator(context, aggregator.combineValuesByKey(iter, context))  
  41.       }, preservesPartitioning = true)  
  42.     }  
  43.   }  

  这个过程有点复杂,但简单的从函数值上我们可以看到,reduceByKey最终返回的结果是RDD[(K, V)]。其实,细致分析下去,最终返回的是个MapPartitionsRDD对象:上面的代码最终都是生成一个ShuffledRDD对象;然后调用该对象的mapPartitionsWithContext方法,这个方法返回MapPartitionsRDD对象。如下:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. def mapPartitionsWithContext[U: ClassTag](  
  2.     f: (TaskContext, Iterator[T]) => Iterator[U],  
  3.     preservesPartitioning: Boolean = false): RDD[U] = {  
  4.   val func = (context: TaskContext, index: Int, iter: Iterator[T]) => f(context, iter)  
  5.   new MapPartitionsRDD(this, sc.clean(func), preservesPartitioning)  
  6. }  

4. 是时候开始真正的计算了!——compute()
 通过上面的几个步骤,我们可以看到,其实我们还什么都没有做,只是构建了一些列的RDD对象!每个RDD对象都有一个Parent,如下:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. /** Construct an RDD with just a one-to-one dependency on one parent */  
  2. def this(@transient oneParent: RDD[_]) =  
  3.   this(oneParent.context , List(new OneToOneDependency(oneParent)))  


  通过这个Parent,实际上我们把一个个RDD对象串联了起来!这一串RDD,就是我们搭建起来的计算过程!具体如下:
 HadoopRDD->FlatMappedRDD->MappedRDD->MapPartitionsRDD


  在执行saveAsTextFile最终会调用runJob,在runJob的过程中会调用RDD的compute(具体过程另行分析)。它是RDD的一个abstract函数,由各个子类具体实现。我们以MappedRDD为例,看一下compute都做了什么事:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. override def compute(split: Partition, context: TaskContext) =  
  2.   firstParent[T].iterator(split, context).map(f)  

  它首先调用了parent RDD的iterator(split, context),然后就执行了map运算。可见,在这里我们才真正执行了map运算——这就实现了所谓的延迟计算!

  等等,前面还有个firstParent[T].iterator(split, context),猜猜它是干什么的? ——很简单,我们要想让前面搭建起来的运算过程顺序执行,必须得让“parent”RDD先compute完成,然后才执行用我们当前这个RDD要做的运算啊。所以他就是迭代的调用之前所有parent RDD的compute方法。如下:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. /** 
  2.  * Internal method to this RDD; will read from cache if applicable, or otherwise compute it. 
  3.  */  
  4. final def iterator(split: Partition, context: TaskContext): Iterator[T] = {  
  5.   if (storageLevel != StorageLevel.NONE) {  
  6.     SparkEnv.get.cacheManager.getOrCompute(this, split, context, storageLevel)  
  7.   } else {  
  8.     computeOrReadCheckpoint(split, context)  
  9.   }  
  10. }  

总结:

这类延迟计算过程在Spark中称为“Transformation操作”。Transformation操作主要有:

map(func):对调用map的RDD数据集中的每个element都使用func,然后返回一个新的RDD,这个返回的数据集是分布式的数据集
filter(func) : 对调用filter的RDD数据集中的每个元素都使用func,然后返回一个包含使func为true的元素构成的RDD
flatMap(func):和map差不多,但是flatMap生成的是多个结果
mapPartitions(func):和map很像,但是map是每个element,而mapPartitions是每个partition
mapPartitionsWithSplit(func):和mapPartitions很像,但是func作用的是其中一个split上,所以func中应该有index
sample(withReplacement,faction,seed):抽样
union(otherDataset):返回一个新的dataset,包含源dataset和给定dataset的元素的集合
distinct([numTasks]):返回一个新的dataset,这个dataset含有的是源dataset中的distinct的element
groupByKey(numTasks):返回(K,Seq[V]),也就是hadoop中reduce函数接受的key-valuelist
reduceByKey(func,[numTasks]):就是用一个给定的reduce func再作用在groupByKey产生的(K,Seq[V]),比如求和,求平均数
sortByKey([ascending],[numTasks]):按照key来进行排序,是升序还是降序,ascending是boolean类型
join(otherDataset,[numTasks]):当有两个KV的dataset(K,V)和(K,W),返回的是(K,(V,W))的dataset,numTasks为并发的任务数
cogroup(otherDataset,[numTasks]):当有两个KV的dataset(K,V)和(K,W),返回的是(K,Seq[V],Seq[W])的dataset,numTasks为并发的任务数
cartesian(otherDataset):笛卡尔积就是m*n

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值