Spark core 核心知识之再聊RDD

前言

  • 本文主要是记录在学习spark core 中的一些核心概念以及用法,对spark core 中的东西做出自己的总结。文章中可能会有一些错误,但鉴于是作者结合官网做出总结,仅做参考,涉及到不对以及不清楚的地方还请谅解。
  • spark 的学习,我们可以参照官网spark 官网,spark的官网可以说是写的比较好的了,涉及到的知识还是非常全面的,通过官网,我们可以进行简单的案例使用,以及在概念上有一个清楚的认知(虽然官网都是英文介绍,但大概的读一读也能理解)

Spark Core 之RDD

  • 关于RDD的理解,作者以前有一篇博客介绍过链接地址,RDD为Spark Core 中的核心概念,对RDD的理解和掌握多少就可以间接的体现你对Spark Core 了解情况。
  • 概念认知: RDD 的全称为 Resilient Distributed Datasets ,用一句话概括:RDD是一种基于分布式计算而产生的一种弹性的,分布式的,不可变的,抽象的数据集概念,当RDD在做Transformation时,一个RDD会变成另外一个新的RDD,这体现了它的不可变;RDD与转换后的RDD会记录着依赖关系(窄依赖,宽依赖,会在后面解释),当其中一个RDD损坏或丢失,它会根据这种依赖关系进行重新计算得到,这即为弹性;何为分布式,分布式必然是大数据处理的必要场景,RDD的分布特性体现在它的可分区,一个RDD是由多个分区构成,基于大数据的分布式存储,对同一份数据进行分块和多维度的存储,这就导致同一份数据可以被存放在不同的节点上,在计算的时候就可以根据不同节点上的分区数据做分布式计算;至于抽象,,涉及到哲学思想,暂不讨论。
  • 特点: 其实RDD的特点在介绍它的概念的时候基本上都有涉及,这里再做一个概述:
    RDD在源码中的英文描述:

    • A list of partitions
    • A function for computing each split
    • A list of dependencies on other RDDs
    • Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
    • Optionally, a list of preferred locations to compute each split on (e.g. block locations for
      an HDFS file)

      1. 首先RDD是由一组分区构成,RDD的分区机制在不同场景会有所不同,在大数据的场景下,一个数据的block会被默认加载为一个分区,但在使用parallelize()方法创建RDD时,它的分区数会和你申请的core的数量相等。

      2. 对RDD做计算就等于对RDD的每个分区做计算,这就是分布式或并行计算的体现。

      3. RDD与通过转换得到的其他RDD会保留依赖关系(宽依赖,窄依赖)
      4. 可选项,对于以key-value形式的RDD通过一些算子对RDD重新分区
      5. 可选项,preferred locations,数据本地性,如果数据被加载到其他节点进行计算,这必然会导致计算变慢,移动数据,不如移动计算,这也是大数据的思想之一。
  • 创建RDD: RDD的创建方式可分俩类:一,通过并行化集合(Parallelized Collections),此种方式可以传递一个数据或一个集合,通过传入参数也可以指定RDD分区;二,通过外部数据集(External Datasets),这种方式支持任何一种Hadoop iInputformat 格式的创建,如textfile,SequenceFile.具体参照此篇博客

  • RDD的操作: RDD的操作算子也可以分俩类:一,Transformations,RDD的转换操作为Lzay操作,不管你在代码中写多少transformation操作,没有Action都只是无用的代码;二,Actions, 遇到一个action即为一个job,有action操作的spark 应用程序才算是真正的应用程序。对于Action操作,这里要拎出一个Action 算子:collect(),这个算子会将RDD的结果以数组的形式返回到Driver端,如果数据量很大,而Driver端内存不足时,会出现OOM(内存溢出)的情况,所以此算子慎用。具体的其他常用算子介绍参照此篇博客
  • RDD结果写出: RDD的结果写出可借助几个Action算子:saveAsTextFile(path),saveAsSequenceFile(path),saveAsObjectFile(path)
  • RDD的Shuffle操作: Shuffle字面意思为洗牌,在MapReduce作业中也会有Shuffle的产生,Shuffle是一个比较消耗作业性能的操作,在不管是在MapReduce作业还是在Spark作业中,应尽可能的避免Shuffle的产生,那么Shuffle怎样去理解呢,其实我给他概括为一句话,Shuffle可以理解为数据的重新组合和分发,在 MR和Spark作业中,数据是分布在不同的task中,在遇到ReduceByKey,GroupByKey,join等算子的时候,相同Key 的数据并不会刚好在一个task中,而Shuffle的作用就是将不同task 里的相同的Key的数据分发到指定的task 中,这就是Shuffle的产生,因为在此过程中会有网络和磁盘的IO,所以消耗性能是肯定的,不过并不是一味的意味着Shuffle的操作是不好的,在有些场景下,比如数据倾斜,我们可以借助Shuffle来解决数据倾斜的情况。利用Shuffle的这个特点我们可以对Spark应用程序进行这方面的调优此篇博客也有介绍
  • RDD的Cache机制: 在Spark 应用程序中,如果对某个RDD需要重复使用,那么我们可以使用Cache或Persist 方法来把该RDD缓存在内存中(Cache底层默认调用的Persist方法),这样会使得将来对该RDD的计算变得更快,可以直接从内存中读取数据,而不再是每次都是从源头去计算,这种Cache机制有着高容错的特点,在RDD的某个分区丢失时,它会依据依赖关系进行重新计算并缓存在内存中。既然可以缓存数据,那么缓存数据在哪里,以什么方式都是要被定义的,Spark 中定义了以下StorageLevel来控制存储级别和存储方式:

    • MEMORY_ONLY:默认存储级别,以反序列化的方式只使用内存存储Java对象,如果内存不够导致RDD不能缓存全部数据,那么将会缓存部分分区数据,其他分区数据可以在计算时进行重算得到。
    • MEMORY_AND_DISK:以反序列化的方式在内存和磁盘存储RDD的全部数据
    • MEMORY_ONLY_SER (Java and Scala):以序列化的方式只在内存中缓存RDD数据,这虽然会减少RDD在内存中占用的空间,但反序列化会有CPU的占用,如果集群负载较高,不建议使用此种方式。
    • MEMORY_AND_DISK_SER(Java and Scala):以序列化的方式在内存和磁盘缓存RDD数据
    • DISK_ONLY:只在磁盘上缓存数据,不建议使用,因为从磁盘读取数据可能还不如直接使用重新计算的速度快。
    • MEMORY_ONLY_2, MEMORY_AND_DISK_2, etc.:以多副本的形式和对应的存储级别把数据缓存多份。
      不得不说的是Spark中的缓存操作也是Lazy的,而和它相反的unpersist()操作是eager的,缓存的使用是调优的关键,对于缓存的使用和详细介绍也可以查看此篇博客
  • RDD中的Broadcast Variables: 广播变量,什么是广播变量?广播变量就是将较小的共享数据以只读的形式发到分布式缓存中去,做到每个Executor都会保留一份数据,而不是每个task一份数据,从而减少缓存数据量。其实说到广播变量,我们在Hive中就使用过,Hive中的join一般都是有Shuffle 的ReduceJoin,可以使用MapJoin来代替普通的join,因为MapJoin中不会有Shuffle的产生,所以可以提高不少的性能,MapJoin就是使用的广播变量的原理,所以又叫Broadcast Join。使用广播变量代码案例:

    package com.venus.spark
    
    import org.apache.spark.{SparkConf, SparkContext}
    
    object broadcastJoinApp {
      def main(args: Array[String]): Unit = {
        val conf=new SparkConf().setMaster("local[2]").setAppName("BCApp")
        val sc = new SparkContext(conf)
        val smallRDD=sc.parallelize(Array(
          ("1","zhangshan"),
          ("2","lisi"),
          ("3","wangwu")
        )).collectAsMap()
        val smallBroadcast=sc.broadcast(smallRDD)
        val bigRDD=sc.parallelize(Array(
            ("1","school1","male"),
            ("2","school2","female"),
            ("3","school3","male"),
            ("4","school4","female")
        )).map(x=>(x._1,x))
    
        bigRDD.mapPartitions(partitions=>{
          val broadcastValue=smallBroadcast.value
          for((key,value)<- partitions if(broadcastValue.contains(key)))
            yield(key,broadcastValue.getOrElse(key,""),value._2,value._3)
    
        }).collect().foreach(println)
    
      }
    
  • RDD的宽,窄依赖: 一句话概括:

    • 窄依赖:子RDD的一个分区只通过父RDD最多一个分区得到
    • 宽依赖:子RDD的一个分区可以通过多个父RDD的分区得到,详细介绍可以通过该篇博客

猜你喜欢

转载自blog.csdn.net/Realoyou/article/details/81038364
今日推荐