核心内容:
1、Spark中WordCount的运行原理
今天又彻底研究了一下Spark中WordCount的运行原理,在运行逻辑上与Hadoop中的MapReduce有很大的相似之处,今天从数据流动的角度解析Spark的WordCount,即从数据流动的角度来分析数据在Spark中是如何被处理的。
直接分析程序:
val lines:RDD[String] = sc.textFile("C:\\word.txt",1)
textFile操作产生的RDD:
查看源码:
def textFile(
path: String,
minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {
assertNotStopped()
hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text],
minPartitions).map(pair => pair._2.toString)
}
……
def hadoopFile[K, V](
path: String,
inputFormatClass: Class[_ <: InputFormat[K, V]],
keyClass: Class[K],
valueClass: Class[V],
minPartitions: Int = defaultMinPartitions): RDD[(K, V)] = withScope {
assertNotStopped()
// A Hadoop configuration can be about 10 KB, which is pretty big, so broadcast it.
val confBroadcast = broadcast(new SerializableConfiguration(hadoopConfiguration))
val setInputPathsFunc = (jobConf: JobConf) => FileInputFormat.setInputPaths(jobConf, path)
new HadoopRDD(
this,
confBroadcast,
Some(setInputPathsFunc),
inputFormatClass,
keyClass,
valueClass,
minPartitions).setName(path)
}
……
def map[U: ClassTag](f: T => U): RDD[U] = withScope {
val cleanF = sc.clean(f)
new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))
}
从shell以及源码中我们可以看出,textFile操作产生了两个RDD:HadoopRDD与MapPartitionsRDD,这两个RDD在此处的作用:
HadoopRDD:在这里之所以会先产生HadoopRDD,是因为处理数据的第一步:我们当然是先要从HDFS中抓取数据了,所以会先产生HadoopRDD;HadoopRDD会从HDFS上读取分布式文件,并将输入文件以数据分片的方式存在于集群之上,假如我们集群现在有4个节点,于是我们将数据分成4个数据分片(当然,这是一种粗略的划分,);HadoopRDD会帮助我们从磁盘上读取数据,在计算的时候会将数据放在内存中,并且会以分布式的方式放在内存中。
Spark中分片的策略:默认情况下分片的大小与block块的大小是相同的,假设我们现在有4个数据分片(partition),每个数据分片有128M左右。
MapPartitionsRDD:MapPartitionsRDD是基于HadoopRDD产生的RDD,MapPartitionsRDD将HadoopRDD产生的数据分片(partition)去掉相应行的key,只留value,map函数的逻辑其实是很简单的,我们只对每一行读取的数据的内容感兴趣,而对索引并不感兴趣。在这里让我联想到了在MapReduce中map函数的编写逻辑:拿到日志中的一行数据…….
从上面的操作中我们不难发现:textFile操作产生了2个RDD,看来在Spark当中一个操作可以一个或多个RDD。
接着分析程序:
val words = lines.flatMap(line=>line.split(" "))
flatMap操作产生的RDD:
查看源码:
def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U] = withScope {
val cleanF = sc.clean(f)
new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.flatMap(cleanF))
}
从shell中以及源码中我们可以发现,flatMap操作产生了一个RDD:MapPartitionsRDD,MapPartitionsRDD在这里面的作用其实是很简单的:对每个Partition中的每一行内容进行单词切分并合并成一个大的单词实例的集合。
res2: Array[String] = Array(Hello, Spark, Hello, Scala, Hello, Hadoop, Hello, Hbase, Spark, Hadoop, Java, Spark)
接着分析代码:
val pairs:RDD[(String,Int)] = words.map(word=>(word,1))
此处map操作产生的RDD:
察看源码:
def map[U: ClassTag](f: T => U): RDD[U] = withScope {
val cleanF = sc.clean(f)
new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))
}
从shell操作以及源码中可以发现,map操作产生了一个RDD:MapPartitionsRDD,MapPartitionsRDD在这里面的作用为:在我们单词拆分的基础上对我么的单词计数为1。
res5: Array[(String, Int)] = Array((Hello,1), (Spark,1), (Hello,1), (Scala,1), (Hello,1), (Hadoop,1), (Hello,1), (Hbase,1), (Spark,1), (Hadoop,1), (Java,1), (Spark,1))
接着分析:
val wordCounts:RDD[(String,Int)] = pairs.reduceByKey(_+_)
查看reduceByKey操作产生的RDD:
从shell操作中我们可以看出reduceByKey这个步骤产生了一个RDD: ShuffledRDD,但是我们查看一下源码:
def reduceByKey(partitioner: Partitioner, func: (V, V) => V): RDD[(K, V)] = self.withScope {
combineByKeyWithClassTag[V]((v: V) => v, func, func, partitioner)
}
……
def reduceByKey(func: (V, V) => V, numPartitions: Int): RDD[(K, V)] = self.withScope {
reduceByKey(new HashPartitioner(numPartitions), func)
}
……
def combineByKeyWithClassTag[C](
createCombiner: V => C,
mergeValue: (C, V) => C,
mergeCombiners: (C, C) => C,
partitioner: Partitioner,
mapSideCombine: Boolean = true,
serializer: Serializer = null)(implicit ct: ClassTag[C]): RDD[(K, C)] = self.withScope {
require(mergeCombiners != null, "mergeCombiners must be defined") // required as of Spark 0.9.0
if (keyClass.isArray) {
if (mapSideCombine) {
throw new SparkException("Cannot use map-side combining with array keys.")
}
if (partitioner.isInstanceOf[HashPartitioner]) {
throw new SparkException("Default partitioner cannot partition array keys.")
}
}
val aggregator = new Aggregator[K, V, C](
self.context.clean(createCombiner),
self.context.clean(mergeValue),
self.context.clean(mergeCombiners))
if (self.partitioner == Some(partitioner)) {
self.mapPartitions(iter => {
val context = TaskContext.get()
new InterruptibleIterator(context, aggregator.combineValuesByKey(iter, context))
}, preservesPartitioning = true)
} else {
new ShuffledRDD[K, V, C](self, partitioner)
.setSerializer(serializer)
.setAggregator(aggregator)
.setMapSideCombine(mapSideCombine)
}
}
从源码中我们可以发现reduceByKey这个步骤实际上产生了两个RDD:MapPartitionsRDD与ShuffledRDD,这两个RDD的作用:
MapPartitionsRDD:进行本地级别(local)的归并操作,并且把统计后的结果按照分区(分区就是将上一阶段的结果分为几个标志交给下一阶段进行处理)策略放到不同的File,这个步骤发生在Stage1的末尾端,减少网络的传输;并将当前阶段作为stage1阶段的内容,放在本地磁盘上,供shuffle阶段使用。本地归并之后就进入了发生网络传输的shuffle。
ShuffledRDD :进行全局reduce级别的归并操作。
从上面可以看出,reduceByKey含有两个阶段:第一个是本地级别的Reduce,一个是全局级别的Reduce,而第一个级别是我们容易忽视的。
在这里面再次强调一下:本地归并属于stage1,即父stage,内部进行基于内存的迭代,不需要每次操作都有读写磁盘的操纵,所以相比于MapReduce速度要快很多。
好,接着分析:
wordCounts.saveAsTextFile("zmy/dirspark/")
我们此时查看一下源码:
def saveAsTextFile(path: String): Unit = withScope {
val nullWritableClassTag = implicitly[ClassTag[NullWritable]]
val textClassTag = implicitly[ClassTag[Text]]
val r = this.mapPartitions { iter =>
val text = new Text()
iter.map { x =>
text.set(x.toString)
(NullWritable.get(), text)
}
}
……
从源码中我们可以可以发现,从将数据保存到HDFS的角度讲,ShuffledRDD之后实际上还有一个MapPartitionsRDD,这个MapPartitionsRDD的作用:我们将Stage2产生的结果输出到我们HDFS中的时候数据的输出要符合一定的格式,而我们现在的结果只有value,没有Key,所以MapPartitionsRDD帮助我们生成相应的Key。
好了,到现在为止在细节上已经分析完了!接下来我们查一下最终的结果:
[root@hadoop11 ~]# hadoop fs -cat /zmy/dirspark/part-00000
(Hello,4)
(Java,1)
(Scala,1)
[root@hadoop11 ~]# hadoop fs -cat /zmy/dirspark/part-00001
(Spark,3)
(Hbase,1)
(Hadoop,2)
从运行结果我们可以看出,输出文件有2个,即分区的时候按照Hash值分为了2个类型。
接下来们从整体上去认识Spark中的WordCount,检查一下DAG:
我们接下来在具体看一下每个阶段的具体详情:第一个Stage:
第二个Stage:
好,接下来我们总结一下:在Spark的WordCount运行的过程当中,共包含2个Stage:
第一个Stage产生的RDD:
HadoopRDD、 MapPartitionsRDD、MapPartitionsRDD、MapPartitionsRDD、MapPartitionsRDD(reduceByKey所产生)
第二个Stage产生的RDD:
ShuffledRDD(reduceByKey所产生)、MapPartitionsRDD
当然,在最后来一道大菜,WordCount的数据运行过程,我们从数据流动的角度进行思考:
图1所示:(网友版)
图二所示:(自己所画)
接下来将其余的一些知识点总结一下:
1、Spark的三个特点:分布式、基于内存(部分基于磁盘),迭代。所谓分布式无论是计算还是数据都是分布式的。
2、在Spark当中是通过某个函数在内部产生相应的RDD,进而实现相应的功能,而一个操作可以产生1个或多个RDD。
3、在Spark的WordCount运行过程中,HadoopRDD产生的是键值对