04-Spark核心编程-RDD-01

96 阅读8分钟

一、什么是RDD?

RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是Spark中最基本的数据处理模型。代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行计算的集合。

  1. 弹性
    • 存储的弹性:内存与磁盘的自动切换;
    • 容错的弹性:数据丢失可以自动恢复;
    • 计算的弹性:计算出错重试机制;
    • 分片的弹性:可根据需要重新分片。
  2. 分布式:数据存储在大数据集群不同节点上
  3. 数据集:RDD封装了计算逻辑,并不保存数据
  4. 数据抽象:RDD是一个抽象类,需要子类具体实现
  5. 不可变:RDD封装了计算逻辑,是不可以改变的,想要改变,只能产生新的RDD,在新的RDD里面封装计算逻辑
  6. 可分区、并行计算

二、五大核心属性

  1. 分区列表

    RDD数据结构中存在分区列表,用于执行任务时并行计算,是实现分布式计算的重要属性。

  2. 分区计算函数

    Spark在计算时,是使用分区函数对每一个分区进行计算

  3. RDD之间的依赖关系

    RDD是计算模型的封装,当需求中需要将多个计算模型进行组合时,就需要将多个RDD建立依赖关系

  4. 分区器

    当数据为KV类型数据时,可以通过设定分区器自定义数据的分区。

  5. 首选位置

    计算数据时,可以根据计算节点的状态选择不同的节点位置进行计算。

三、执行原理

从计算的角度来讲,数据处理过程中需要计算资源(内存 & CPU)和计算模型(逻辑)。执行时,需要将计算资源和计算模型进行协调和整合。

Spark框架在执行时,先申请资源,然后将应用程序的数据处理逻辑分解成一个一个的计算任务。然后将任务发到已经分配资源的计算节点上, 按照指定的计算模型进行数据计算。最后得到计算结果。

RDD是Spark框架中用于数据处理的核心模型,接下来我们看看,在Yarn环境中,RDD的工作原理:

1) 启动Yarn集群环境

image.png

2) Spark通过申请资源创建调度节点和计算节点

image.png

3) Spark框架根据需求将计算逻辑根据分区划分成不同的任务

image.png

4) 调度节点将任务根据计算节点状态发送到对应的计算节点进行计算

image.png

四、基础编程

  1. 创建RDD
    在Spark中创建RDD可以分为四种:
    • 从集合中创建RDD:makeRDD 和 parallelize 两种方法。

      val sparkConf = new SparkConf().setMaster("local[*]").setAppName("spark")
      val sc = new SparkContext(sparkConf)
      val rdd1 = sc.parallelize(List(1,2,3,4))
      val rdd2 = sc.makeRDD(List(1,2,3,4))
      rdd1.collect().foreach(println)
      rdd2.collect().foreach(println)
      sc.stop()
      

      从底层代码实现来讲,makeRDD方法其实就是parallelize方法:

      def makeRDD[T: ClassTag](
          seq: Seq[T],
          numSlices: Int = defaultParallelism): RDD[T] = withScope {
              parallelize(seq, numSlices)
          }
      )
      
    • 从外部存储创建RDD
      由外部存储系统的数据集创建RDD包括:本地的文件系统,所有Hadoop支持的数据集,比如HDFS、HBase等。

      val sparkConf = new SparkConf().setMaster("local[*]").setAppName("spark")
      val sc = new SparkContext(sparkConf)
      val fileRDD: RDD[String] = sc.textFile("input")  // 读取外部文件,可以是一个目录
      fileRDD.collect().foreach(println)
      sc.stop()
      
    • 从其他RDD创建
      主要是通过一个RDD运算完后,再产生新的RDD。详情请参考后续章节

    • 直接创建RDD(new)
      使用new的方式直接构造RDD,一般由Spark框架自身使用。

  2. 转换算子
    RDD根据数据处理方式的不同将算子整体上分为单Value类型、双Value类型和Key-Value类型。
    • 单value算子
      1. map 算子
        算子可以说是整个 Spark 环境最常用的算子之一,它主要是针对数据结构进行转换。这里的转换可以是类型的转换,也可以是值的转换。
        函数签名:

        def map[U: ClassTag](f: T => U): RDD[U]
        

        示例:

        // 对集合中的每个元素进行 *2 处理
        val dataRDD: RDD[Int] = sparkContext.makeRDD(List(1,2,3,4))
        val dataRDD1: RDD[Int] = dataRDD.map(
            num => {
                num * 2
            }
        )
        
      2. mapPartitions算子
        与 map 算子类似,只不过map是针对数据逐条处理,而 mapPartitions 按照分区处理数据。
        函数签名:

        def mapPartitions[U: ClassTag](
            f: Iterator[T] => Iterator[U],
            preservesPartitioning: Boolean = false): RDD[U]
        )
        

        示例:

        // 过滤出集合中的偶数
        val dataRDD1: RDD[Int] = dataRDD.mapPartitions(
            datas => {
                datas.filter(_%2 == 0)
            }
        )
        
        // 求每个分区中的最大值
        val newRDD = rdd.mapPartitions(
            iter => {  // scala  iterator max min sum
                List(iter.max).iterator
            }
        )
        

        注意:由于 mapPartitions 算子是针对分区处理数据,当一个分区内的数据量过大时,可能会导致OOM(内存溢出),此时为了保证程序的运行,应使用map算子,即使会降低程序的运行效率;或者数据重新分区,打散数据

      3. mapPartitionsWithIndex算子
        该算子与 mapPartitions 类似,只不过在处理数据的时候可以获取分区索引。 函数签名:

        def mapPartitionsWithIndex[U: ClassTag](
            f: (Int, Iterator[T]) => Iterator[U],
              preservesPartitioning: Boolean = false): RDD[U]
        )
        

        示例:

        // 获取第二个分区的数据
        val list = List(1,2,3,4,5,6)
        // 分区索引编号从0开始
        // 获取的数据应该编号为1
        val rdd = sc.makeRDD(list, 3)  // 将数据打散为三个分区
        // mapPartitionsWithIndex : (index, iterator) => iterator
            val newRDD = rdd.mapPartitionsWithIndex(
                (index, iter) => {
                if ( index == 1 ) {
                    iter
                } else {
                    // null
                    Nil.iterator
                    //iter.filter(false)
                }
            }
        )
        

        注意:由于该算子也是对分区数据处理,仍然可能出现OOM(内存溢出的情况)。

      4. flatMap 将处理的数据进行扁平化后再进行映射处理,所以算子也称之为扁平映射。
        函数签名:

        def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U]
        

        示例:

        val dataRDD = sparkContext.makeRDD(List(List(1,2),List(3,4)), 1)
        val dataRDD1 = dataRDD.flatMap(
            list => list
        )
        
        // 将List(List(1,2),3,List(4,5))进行扁平化操作
        val list = List(List(1,2),3,List(4,5))
        val rdd = sc.makeRDD(list)
        rdd.flatMap(
            data => {
                data match {  // 模式匹配,由于集合中存在不同类型的数据,需要对不同类型扁平化
                    case list:List[_] => list
                    case d => List(d)
                }
            }
        ).collect.foreach(println)
        
      5. groupBy
        将数据根据指定的规则进行分组, 分区默认不变,但是数据会被打乱重新组合,我们将这样的操作称之为shuffle。极限情况下,数据可能被分在同一个分区中。
        一个组的数据在一个分区中,但是并不是说一个分区中只有一个组。
        函数签名:

        def groupBy[K](f: T => K)(implicit kt: ClassTag[K]): RDD[(K, Iterable[T])]
        

        示例:

        // 按奇数和偶数分组
        val dataRDD = sparkContext.makeRDD(List(1,2,3,4), 1)
        val dataRDD1 = dataRDD.groupBy(
            _%2
        )
        
      6. filter 顾名思义,按规则过滤数据: 函数签名:

        def filter(f: T => Boolean): RDD[T]
        

        示例:

        // 过滤出偶数
        val dataRDD = sparkContext.makeRDD(List(1,2,3,4), 1)
        val dataRDD1 = dataRDD.filter(_%2 == 0)
        

        注意:当数据进行筛选过滤后,分区不变,但是分区内的数据可能不均衡,生产环境下,可能会出现数据倾斜。

      7. sample
        根据指定的规则从数据集中抽取数据。
        函数签名:

        def sample(
          withReplacement: Boolean,
          fraction: Double,
          seed: Long = Utils.random.nextLong): RDD[T]
        )
        

        示例:

        val dataRDD = sparkContext.makeRDD(List(1,2,3,4), 1)
        // 抽取数据不放回(伯努利算法)
        // 伯努利算法:又叫0、1分布。例如扔硬币,要么正面,要么反面。
        // 具体实现:根据种子和随机算法算出一个数和第二个参数设置几率比较,小于第二个参数要,大于不要
        // 第一个参数:抽取的数据是否放回,false:不放回
        // 第二个参数:抽取的几率,范围在[0,1]之间,0:全不取;1:全取;
        // 第三个参数:随机数种子
        val dataRDD1 = dataRDD.sample(false, 0.5)
        // 抽取数据放回(泊松算法)
        // 第一个参数:抽取的数据是否放回,true:放回;false:不放回
        // 第二个参数:重复数据的几率,范围大于等于0.表示每一个元素被期望抽取到的次数
        // 第三个参数:随机数种子
        val dataRDD2 = dataRDD.sample(true, 2)
        

        该算子的作用是对大数据集进行数据抽取,检测数据倾斜的问题:

        val listRDD: RDD[Int] = sc.makeRDD(List(1, 1, 1, 1, 2, 1, 1, 1, 2, 1, 2, 1, 3, 2, 1, 3, 1, 3, 1, 3, 2))
        val sampleRDD: RDD[Int] = listRDD.sample(true, 1)
        val result: RDD[(Int, Iterable[Int])] = sampleRDD.groupBy(data => data)
        
      8. distinct
        去重算子。
        函数签名:

        def distinct()(implicit ord: Ordering[T] = null): RDD[T]
        def distinct(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]
        

        示例:

        val dataRDD = sparkContext.makeRDD(List(1,2,3,4,1,2), 1)
        val dataRDD1 = dataRDD.distinct()
        val dataRDD2 = dataRDD.distinct(2)  // 重新设置数据分区数,引发shuffle操作。
        
      9. coalesce
        缩减分区,用于大数据集过滤后,提高小数据集的执行效率,多配合filter使用。 函数签名:

        def coalesce(numPartitions: Int, 
            shuffle: Boolean = false, 
            partitionCoalescer: Option[PartitionCoalescer] = Option.empty)
            (implicit ord: Ordering[T] = null)
        : RDD[T]
        

        示例:

        val dataRDD = sparkContext.makeRDD(List(1,2,3,4,1,2), 6)
        val dataRDD1 = dataRDD.coalesce(2)
        
      10. repartition
        该操作内部其实执行的是coalesce操作,参数shuffle的默认值为true。无论是将分区数多的RDD转换为分区数少的RDD,还是将分区数少的RDD转换为分区数多的RDD,repartition操作都可以完成,因为无论如何都会经shuffle过程。
        函数签名:

        def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]
        

        示例:

        val dataRDD = sparkContext.makeRDD(List(1,2,3,4,1,2), 2)
        val dataRDD1 = dataRDD.repartition(4)
        
      11. sortBy
        该操作用于排序数据。在排序之前,可以将数据通过指定规则进行处理,之后按照指定规则处理的结果进行排序,默认为正序排列。默认排序后新产生的RDD的分区数与原RDD的分区数一致。
        函数签名:

        def sortBy[K](
            f: (T) => K,
            ascending: Boolean = true,
            numPartitions: Int = this.partitions.length)
            (implicit ord: Ordering[K], ctag: ClassTag[K]): RDD[T]
        )
        

        示例:

        val dataRDD = sparkContext.makeRDD(List(1,2,3,4,1,2), 2)
        val dataRDD1 = dataRDD.sortBy(num=>num, false, 4) // 第二个参数默认为true,升序;第三个参数默认为之前的分区数,可以手动修改分区数,引起shuffle操作。