Spark系列修炼---入门笔记12

本文详细解析了Spark中WordCount任务的运行原理,包括数据流动过程、不同阶段产生的RDD及其作用,以及如何从HDFS读取数据、单词拆分、计数等关键步骤。

核心内容:
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产生的是键值对

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只懒得睁眼的猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值