第11课: 彻底解密WordCount运行原理

第11课: 彻底解密WordCount运行原理

本节彻底解析wordcount运行原理:

1,从数据流动视角解密WordCount,使用Spark作单词计数统计,数据到底是怎么流动的。

2,从RDD依赖关系的视角解密WordCount。Spark中的一切操作都是RDD,后面的RDD对前面的RDD有依赖关系。

3,DAG与血统Lineage的思考。

接下来我们讲解运行wordcount程序。首先建立一个文本文件helloSpark.txt,将文本文件放到文件目录data/wordcount/里面,helloSpark.txt的文本内容如下:

1.         Hello Spark Hello Scala

2.         Hello Hadoop

3.         Hello Flink

4.         Spark is Awesome

 

我们在IDEA中编写wordcount.scala的代码如下:

1.          package com.dt.spark.sparksql

2.         importorg.apache.spark.SparkConf

3.         import org.apache.spark.SparkContext

4.         import org.apache.spark.rdd.RDD

5.         /**

6.           * 使用Scala开发本地测试的SparkWordCount程序

7.           * @author DT大数据梦工厂

8.           * 新浪微博:http://weibo.com/ilovepains/

9.           */

10.      object WordCount {

11.        def main(args: Array[String]){

12.          /**

13.            * 第1步:创建Spark的配置对象SparkConf,设置Spark程序的运行时的配置信息,例如说通过setMaster来设置程序要链接的Spark集群的Master的URL,如果设置

14.      为local,则代表Spark程序在本地运行,特别适合于机器配置条件非常差(例如只有1G的内存)的初学者    

15.            */

16.          val conf = new SparkConf() //创建SparkConf对象

17.          conf.setAppName("Wow,My First SparkApp!") //设置应用程序的名称,在程序运行的监控界面可以看到名称

18.          conf.setMaster("local") //此时,程序在本地运行,不需要安装Spark集群

19.       

20.          /**

21.            * 第2步:创建SparkContext对象

22.            * SparkContext是Spark程序所有功能的唯一入口,无论是采用Scala、Java、Python、R等都必须有一个SparkContext

23.            * SparkContext核心作用:初始化Spark应用程序运行所需要的核心组件,包括DAGScheduler、TaskScheduler、SchedulerBackend 同时还会负责Spark程序往Master注册程序等,SparkContext是整个Spark应用程序中最为至关重要的一个对象

24.            */

25.          val sc = new SparkContext(conf) //创建SparkContext对象,通过传入SparkConf实例来定制Spark运行的具体参数和配置信息

26.       

27.          /**

28.            * 第3步:根据具体的数据来源(HDFS、HBase、Local FS、DB、S3等)通过SparkContext来创建RDD

29.            *RDD的创建基本有三种方式:根据外部的数据来源(例如HDFS)、根据Scala集合、由其它的RDD操作

30.            * 数据会被RDD划分成为一系列的Partitions,分配到每个Partition的数据属于一个Task的处理范畴

31.            */

32.         

33.          val lines =sc.textFile("data/wordcount/helloSpark.txt", 1) //读取本地文件并设置为一个Partion

34.       

35.          /**

36.            * 第4步:对初始的RDD进行Transformation级别的处理,例如map、filter等高阶函数等的编程,来进行具体的数据计算

37.            *    第4.1步:讲每一行的字符串拆分成单个的单词

38.            */

39.          val words = lines.flatMap { line =>line.split(" ")} //对每一行的字符串进行单词拆分并把所有行的拆分结果通过flat合并成为一个大的单词集合

40.          /**

41.            * 第4步:对初始的RDD进行Transformation级别的处理,例如map、filter等高阶函数等的编程,来进行具体的数据计算

42.            *    第4.2步:在单词拆分的基础上对每个单词实例计数为1,也就是word =>(word, 1)

43.            */

44.          val pairs = words.map { word => (word,1) }

45.       

46.          /**

47.            * 第4步:对初始的RDD进行Transformation级别的处理,例如map、filter等高阶函数等的编程,来进行具体的数据计算

48.            *    第4.3步:在每个单词实例计数为1基础之上统计每个单词在文件中出现的总次数

49.            */

50.          val wordCountsOdered =pairs.reduceByKey(_+_).map(pair => (pair._2,pair._1)).sortByKey(false).map(pair => (pair._2, pair._1)) //对相同的Key,进行Value的累计(包括Local和Reducer级别同时Reduce)

51.          wordCountsOdered.collect.foreach(wordNumberPair=> println(wordNumberPair._1 + " : " + wordNumberPair._2))

52.          sc.stop()

53.       

54.        }

55.      }

 

在IDEA中运行程序,wordcount的运行结果如下:

1.          ......

2.          17/05/21 21:19:07 INFO DAGScheduler: Job 0finished: collect at WordCount.scala:60, took 0.957991 s

3.         Hello : 4

4.         Spark : 2

5.         Awesome : 1

6.         Flink : 1

7.         is : 1

8.         Scala : 1

9.         Hadoop : 1

10.      ……

 

下面我们从数据流动的视角分析数据到底是怎么处理的,我们绘制一张WordCount数据处理过程图,由于图片较大,为了书稿排版清晰,将原图分成了2张图阅读。

图 3- 1 wordcount 图1

图 3- 2 wordcount 图1

数据在生产环境下中默认在HDFS中进行分布式存储,如果在分布式集群中,我们的机器会分成不同的节点对数据进行处理,这里我们在本地测试,我们的重点是数据怎么流动的。处理的第一步抓取到我们的数据,读取数据会生成HadoopRDD。

在wordcount.scala中,点击sc.textFile进入Spakr框架SparkContext.scala的textFile源码:

1.           def textFile(

2.               path: String,

3.               minPartitions: Int =defaultMinPartitions): RDD[String] = withScope {

4.             assertNotStopped()

5.             hadoopFile(path, classOf[TextInputFormat],classOf[LongWritable], classOf[Text],

6.               minPartitions).map(pair =>pair._2.toString).setName(path)

7.           }

这里有hadoopFile,我们看一下hadoopFile的源码,new出来一个HadoopRDD,HadoopRDD从Hdfs上读取分布式数据,并且以数据分片的方式存在于集群之中。所谓的数据分片就是把我们要处理的分成不同的部分,例如在集群中有4个节点,粗略的划分可以认为将数据分成4个部分,四条语句就分成4个部分,例如Hello Spark在第一台机器上,Hello Hadoop在第二台机器上,Hello Flink在第三台机器上,Spark is Awesome在第四台机器上。HadoopRDD帮助我们从磁盘上读取数据,计算的时候会分布式的放入内存中,Spark运行在Hadoop之上,要借助Hadoop来读取数据。

Spark的特点包括:分布式、基于内存(部分基于磁盘)、可迭代;默认分片策略Block多大,分片就多大。但这种说法不完全准确,因为分片记录可能跨两个Block,所以一个分片不会严格地等于Block的大小,例如HDFS的Block大小是128MB的话,分片可能多几个字节或少几个字节。分片不一定小于128MB,因为如果最后一条记录跨两个Block的话,分片会把最后一条记录放在前一个分片中。这里HadoopRDD用了4个数据分片,设想为128M左右。

hadoopFile的源码如下:

1.          def hadoopFile[K, V](

2.               path: String,

3.               inputFormatClass: Class[_ <:InputFormat[K, V]],

4.               keyClass: Class[K],

5.               valueClass: Class[V],

6.               minPartitions: Int =defaultMinPartitions): RDD[(K, V)] = withScope {

7.             assertNotStopped()

8.          

9.             // This is a hack to enforce loadinghdfs-site.xml.

10.          // See SPARK-11227 for details.

11.          FileSystem.getLocal(hadoopConfiguration)

12.       

13.          // A Hadoop configuration can be about 10KB, which is pretty big, so broadcast it.

14.          val confBroadcast = broadcast(newSerializableConfiguration(hadoopConfiguration))

15.          val setInputPathsFunc = (jobConf: JobConf)=> FileInputFormat.setInputPaths(jobConf, path)

16.          new HadoopRDD(

17.            this,

18.            confBroadcast,

19.            Some(setInputPathsFunc),

20.            inputFormatClass,

21.            keyClass,

22.            valueClass,

23.            minPartitions).setName(path)

24.        }

 

SparkContext.scala的textFile源码中,调用hadoopFile方法之后进行了map转换操作,map对读取的每一行数据进行转换,读入的数据是一个Tuple,Key值为索引,Value值为每行数据的内容,生成MapPartitionsRDD,这里map(pair=> pair._2.toString)是基于HadoopRDD产生的Partition去掉的行Key产生的Value,第二个元素是读取的每行数据内容。MapPartitionsRDD是Spark框架产生的,运行中可能产生一个RDD,也可能产生2个RDD,例如textFile中Spark框架就产生了2个RDD,HadoopRDD和MapPartitionsRDD。我们看一下map的源码:

1.           def map[U: ClassTag](f: T => U): RDD[U] =withScope {

2.             val cleanF = sc.clean(f)

3.             new MapPartitionsRDD[U, T](this, (context,pid, iter) => iter.map(cleanF))

4.           }

 

我们看一下wordcount业务代码,对读取的每行数据进行flatMap转换。这里flatMap是对RDD中的每一个partitioin的每一行数据内容进行单词切分,如有4个partition分别进行单词切分,将“Hello Spark”切分成单词“Hello”和“Spark”,对每一个Partition中的每一行进行单词切分并合并成一个大的单词实例的集合。flatMap转换生成的仍然是MapPartitionsRDD:

RDD.scala的flatMap源码如下:

1.           def flatMap[U: ClassTag](f: T =>TraversableOnce[U]): RDD[U] = withScope {

2.             val cleanF = sc.clean(f)

3.             new MapPartitionsRDD[U, T](this, (context,pid, iter) => iter.flatMap(cleanF))

4.           }

 

继续wordcount业务代码,words.map { word => (word, 1) }通过map转换将单词切分以后将单词计数为1。例如将单词“Hello”和“Spark”变成(Hello,1),(Spark,1)。这里生成了MapPartitionsRDD。

RDD.scala的map源码如下:

1.           def map[U: ClassTag](f: T => U): RDD[U] =withScope {

2.             val cleanF = sc.clean(f)

3.             new MapPartitionsRDD[U, T](this, (context,pid, iter) => iter.map(cleanF))

4.           }

 

继续wordcount业务代码,计数之后进行一个关键的reduceByKey操作,对全局的数据进行计数统计。reduceByKey对相同的Key,进行Value的累计(包括Local和Reducer级别同时Reduce)。reduceByKey在MapPartitionsRDD之后,在Local reduce级别本地进行了统计,这里也是MapPartitionsRDD。例如在本地将(Hello,1),(Spark,1),(Hello,1),(Scala,1)汇聚成(Hello,2),(Spark,1),(Scala,1)。Shuffle之前的Local Reduce操作,主要负责本地局部统计,并且把统计以后的结果按照分区策略放到不同的file。举一个简单例子,如果下一个阶段Stage是3个并行度,每个Partition进行local reduce以后,将自己的数据分成3种类型,最简单的方式是根据HashCode按3取模。

PairRDDFunctions.scala的reduceByKey源码如下:

1.           def reduceByKey(func: (V, V) => V):RDD[(K, V)] = self.withScope {

2.             reduceByKey(defaultPartitioner(self), func)

3.           }

图 3- 4 第一个stage

至此,前面所有的操作都是一个Stage,一个Stage意味着什么:完全基于内存操作。父Stage:Stage内部的操作是基于内存迭代的,也可以进行Cache,这样速度快很多。不同于Hadoop的Map Redcue,Hadoop Map Redcue每一次都要经过磁盘。


reduceByKey在Local reduce本地汇聚以后生成的MapPartitionsRDD仍属于父Stage;然后reduceByKey展开真正的Shuffle操作,Shuffle是Spark甚至整个分布式系统的性能瓶颈,Shuffle会产生ShuffleRDD,ShuffledRDD就变成于另一个Stage,为什么是变成另外一个Stage?因为要传网络,网络传输不能在内存中进行迭代。我们看一下源码:

从wordcount业务代码pairs.reduceByKey(_+_) 看一下PairRDDFunctions.scala的reduceByKey源码:

1.            def reduceByKey(partitioner: Partitioner,func: (V, V) => V): RDD[(K, V)] = self.withScope {

2.             combineByKeyWithClassTag[V]((v: V) => v,func, func, partitioner)

3.           }

reduceByKey内部调用了combineByKeyWithClassTag方法,我们看一下PairRDDFunctions.scala的combineByKeyWithClassTag源码:

1.          def combineByKeyWithClassTag[C](

2.               createCombiner: V => C,

3.               mergeValue: (C, V) => C,

4.               mergeCombiners: (C, C) => C,

5.               partitioner: Partitioner,

6.               mapSideCombine: Boolean = true,

7.               serializer: Serializer = null)(implicitct: ClassTag[C]): RDD[(K, C)] = self.withScope {

8.             require(mergeCombiners != null,"mergeCombiners must be defined") // required as of Spark 0.9.0

9.             if (keyClass.isArray) {

10.            if (mapSideCombine) {

11.              throw new SparkException("Cannotuse map-side combining with array keys.")

12.            }

13.            if(partitioner.isInstanceOf[HashPartitioner]) {

14.              throw newSparkException("HashPartitioner cannot partition array keys.")

15.            }

16.          }

17.          val aggregator = new Aggregator[K, V, C](

18.            self.context.clean(createCombiner),

19.            self.context.clean(mergeValue),

20.            self.context.clean(mergeCombiners))

21.          if (self.partitioner == Some(partitioner)){

22.            self.mapPartitions(iter => {

23.              val context = TaskContext.get()

24.              new InterruptibleIterator(context,aggregator.combineValuesByKey(iter, context))

25.            }, preservesPartitioning = true)

26.          } else {

27.            new ShuffledRDD[K, V, C](self,partitioner)

28.              .setSerializer(serializer)

29.              .setAggregator(aggregator)

30.              .setMapSideCombine(mapSideCombine)

31.          }

32.        }

 

在combineByKeyWithClassTag方法中就new出来了ShuffledRDD。

前面假设有4台机器进行并行计算,每台机器在自己的内存中进行迭代计算,现在产生Shuffle,数据就要进行分类,MapPartitionsRDD数据根据Hash已经分好类,我们就取抓取MapPartitionsRDD中的数据。我们从第一台机器中获取的内容为(Hello,2),从第二台机器中获取的内容为(Hello,1),从第三台机器中获取的内容为(Hello,1),把所有的Hello都抓过来。同样的,我们把其它的数据(Hadoop,1)(Flink,1)...都抓过来。

这就是Shuffle的过程,根据数据的分类拿到自己需要的数据。注意,MapPartitionsRDD是属于第一个Stage,是父Stage,内部基于内存进行迭代,不需要操作都要读写磁盘,所以速度非常快;从计算算子的角度讲,reduceByKey发生在哪里?reduceByKey发生的计算过程包括2个RDD,一个是MapPartitionsRDD,一个是ShuffledRDD,ShuffledRDD要产生网络通信。

reduceByKey之后,我们将结果收集起来,进行全局级别的reduce产生reduceByKeyd最后结果,如将(Hello,2),(Hello,1),(Hello,1) 在内部变成了(Hello,4),其它的数据也类似统计。这里reduceByKey之后,如果我们通过Collect将数据收集起来,会产生MapPartitionsRDD。从Collect的角度讲,MapPartitionsRDD的作用将结果收集一下,发送给Driver;从saveAsTextFile输出到Hdfs的角度讲,例如输出(Hello,4)其中Hello是key,4是Value吗?不是的!这里(Hello,4)就是Value,那需要弄一个key出来。

我们看一下RDD.scala的saveAsTextFile方法:

1.            defsaveAsTextFile(path: String): Unit = withScope {

2.             //https://issues.apache.org/jira/browse/SPARK-2075

3.             //

4.             // NullWritable is a `Comparable` in Hadoop1.+, so the compiler cannot find an implicit

5.             // Ordering for it and will use the default`null`. However, it's a `Comparable[NullWritable]`

6.             // in Hadoop 2.+, so the compiler will callthe implicit `Ordering.ordered` method to create an

7.             // Ordering for `NullWritable`. That's whythe compiler will generate different anonymous

8.             // classes for `saveAsTextFile` in Hadoop1.+ and Hadoop 2.+.

9.             //

10.          // Therefore, here we provide an explicitOrdering `null` to make sure the compiler generate

11.          // same bytecodes for `saveAsTextFile`.

12.          val nullWritableClassTag =implicitly[ClassTag[NullWritable]]

13.          val textClassTag =implicitly[ClassTag[Text]]

14.          val r = this.mapPartitions { iter =>

15.            val text = new Text()

16.            iter.map { x =>

17.              text.set(x.toString)

18.              (NullWritable.get(), text)

19.            }

20.          }

21.          RDD.rddToPairRDDFunctions(r)(nullWritableClassTag,textClassTag, null)

22.            .saveAsHadoopFile[TextOutputFormat[NullWritable,Text]](path)

23.        }

 

RDD.scala的saveAsTextFile方法其中的iter.map {x =>  text.set(x.toString)   (NullWritable.get(), text) 在这里Key是转换成Null,Value就是内容本身(Hello,4)。saveAsHadoopFile中TextOutputFormat要求输出的是key-value的格式,而我们处理的是内容。回顾一下,之前我们在textFile读入数据的时候,读入split分片将key去掉了,计算的是value。因此在输出的时候,我们需将丢失的key重新弄进来,这里key对我们没有意义,但key对Spark框架有意义,只有value对我们有意义。第一次计算的时候我们把key丢弃了,所以最后往HDFS写结果的时候需要生成key,这是符合对称法则和能量守恒形式之美。

总结:

第一个Stage有哪些RDD?HadoopRDD、MapPartitionsRDD、MapPartitionsRDD、MapPartitionsRDD、MapPartitionsRDD

第二个Stage有哪些RDD?ShuffledRDD、MapPartitionsRDD

 

 




你可能感兴趣的:(SparkInBeiJing)