Spark总结之RDD(三)

38 篇文章 3 订阅
16 篇文章 0 订阅

Spark总结之RDD(三)

1. 背景

  1. Spark作为一个分布式数据处理引擎,针对数据处理做了高层级的抽象,如RDD、dataSet、dataFrame、DStream。
  2. 本文关于RDD的总结,会从使用,源码等角度进行对比和分析。
  3. 一定一定注意,RDD只是一个抽象数据集合,本身不存储数据,只是记录要处理的数据来源,要做的数据处理逻辑等信息。
  4. 时刻注意,RDD中的代码执行到底是在driver端执行还是在executor上执行。一定需要区分清楚,因为在driver端执行,就意味着需要加载到内存中处理,而内存往往是有限制的,很可能性能瓶颈就出现在这样的代码中!!!!

2. RDD算子分类

2.1 groupByKey

  1. 顾名思义,就是按照Key进行分组。 注意在大数据处理中,MapReduce和Spark的很多思想都接近,例如map阶段就是把数据转换为key value形式,reduce阶段就是把map阶段数据进行聚合处理
  2. groupByKey,也类似,就是把数据按照key相同的进行划分,分到一个组里面。所以这里是肯定有shuffle的。因为需要把数据按照一定规则分到一个区去。
  3. 注意,使用groupByKey其实隐含了一个条件,就是数据需要是key value形式的。对偶元组形式的数据也是可以,因为spark本身为我们做了implicit隐式转换
    val conf: SparkConf = new SparkConf().setAppName(GroupBykeyTest.getClass.getSimpleName).setMaster("local[*]")

    val sc = new SparkContext(conf)

    // 按照key进行分组,所以是对偶元组形式
    val tupleList = List(("xixi", 1), ("mimi", 2), ("haha", 3), ("haha", 100), ("mimi", 2))

    val rdd1: RDD[(String, Int)] = sc.parallelize(tupleList, 2)

    val res: RDD[(String, Iterable[Int])] = rdd1.groupByKey()

    // 实际上key value形式的rdd可以调用groupByKey是因为有隐式转换,转换为了PairRDDFunctions
    val pairedRdd = new PairRDDFunctions[String, Int](rdd1)
    
    // 打印
    println(res.collect().toBuffer)

    sc.stop()
  1. 运行结果
ArrayBuffer((haha,CompactBuffer(3, 100)), (xixi,CompactBuffer(1)), (mimi,CompactBuffer(2, 2)))
  1. 源码查看
    在这里插入图片描述

上述就是方法说明,已经说了,groupByKey并不是很高效,因为会有大量的shuffle操作。
如果只是做聚合,可以使用aggregateByKey或者reduceByKey,这2个因为会在分区上做局部聚合,效率更高。

在这里插入图片描述

上述可以看出,groupByKey调用的是combineByKeyWithClassTag,其中有5个参数
val createCombiner = (v: V) => CompactBuffer(v) 这个可以看做是针对每个分区的value保存值的初始数组,生成一个CompactBuffer
val mergeValue = (buf: CompactBuffer[V], v: V) => buf += v 这个可以看做是中间阶段进行聚合时的操作,注意使用的是+=,也就是没有生成新的集合,而是使用传入的CompactBuffer
val mergeCombiners = (c1: CompactBuffer[V], c2: CompactBuffer[V]) => c1 ++= c2 这里是不同分组之间的数据操作函数,这里可以看出是进行拼接处理 ++=,集合之间使用++=,就是拼接
val bufs = combineByKeyWithClassTag[CompactBuffer[V]](
createCombiner, mergeValue, mergeCombiners, partitioner, mapSideCombine = false) 这里就是调用combineByKeyWithClassTag方法,注意最后2个参数,传入了一个分区器partitioner,传入了mapSideCombine 为false。前面三个参数分别是之前创建的三个函数,初始值处理、中间时值处理、聚合时值处理
bufs.asInstanceOf[RDD[(K, Iterable[V])]] 这里做了一次强转,转成了一种RDD,内部数据是key value形式,value还是迭代器形式

在这里插入图片描述

三个参数的英文说明

  • createCombiner, which turns a V into a C (e.g., creates a one-element list)
  • mergeValue, to merge a V into a C (e.g., adds it to the end of a list)
  • mergeCombiners, to combine two C’s into a single one.
    注意这里可以看成是最终创建了一个ShuffledRDD对象,后续可以看到,涉及到shuffle的算子操作,最终都是创建了一个ShuffledRDD对象。(暂时还没看到例外,如有,可以评论中私信我)
    serializer: Serializer = null 这个是序列化器,这里是null。 注意在大数据处理中,因为一般都是分布式处理,节点和节点之间,不管是主从节点还是从节点之间,都会有大量的通信。很多时候都是传输对象,这时候就需要序列化器来将对象序列化,方便传输。
    clean 方法就是检查是否包含不可序列化的对象在这里插入图片描述

在这里插入图片描述

combineByKeyWithClassTag 这个方法属于PairRDDFunctions这个类
所以也可以看出, 为什么需要将普通RDD隐式转换为PairRDDFunctions,因为这样就可以增强RDD,带来更多的功能和方法。

综上,groupByKey是调用了combineByKeyWithClassTag,combineByKeyWithClassTag最后是创建了一个ShuffledRDD对象。
当我们选择算子来实现目标时,一定时刻注意,算子最终是运行在driver端还是executor端的,如果是运行在driver端往往会有较大隐患。同时需要考察是否会shuffle,shuffle次数和shuffle涉及到的数据量如何。因为海量数据情况下, 这几个因素会极大影响数据处理总量,进而影响处理性能

2.2 groupBy

groupBy相比groupByKey要更加灵活,因为可以指定分组的字段。

  1. 代码
val conf: SparkConf = new SparkConf().setAppName(GroupByTest.getClass.getSimpleName).setMaster("local[*]")

    val sc = new SparkContext(conf)

    // groupBy 根据,,,进行分组
    val list = List(("cx", true, 10), ("ws", true, 2020), ("qw", false, -203), ("yu", true, 999), ("tr", false, 2020))
    val rdd1: RDD[(String, Boolean, Int)] = sc.makeRDD(list, 2)

    val res: RDD[(Int, Iterable[(String, Boolean, Int)])] = rdd1.groupBy(e => e._3)

    // 打印
    println(res.collect().toBuffer)

    sc.stop()
  1. 运行结果
ArrayBuffer((10,CompactBuffer((cx,true,10))), (2020,CompactBuffer((ws,true,2020), (tr,false,2020))), (-203,CompactBuffer((qw,false,-203))), (999,CompactBuffer((yu,true,999))))
  1. 源码
    在这里插入图片描述
    在这里插入图片描述

从上可以看出,groupBy本质还是调用groupByKey,只是将需要转换的元素使用传入的函数处理之后,作为key,原本元素作为value,进行groupByKey操作

2.3 combineByKey

  1. 顾名思义,就是根据key进行combine组合,源码上来看,groupBy本质是调用groupByKey,而groupByKey最后调用的是combineByKeyWithClassTag.
  2. combineByKey也是类似作用,而且本质还是调用了combineByKeyWithClassTag。
  3. combineByKeyWithClassTag本质是创建了一个ShuffledRDD对象
val conf: SparkConf = new SparkConf().setAppName(CombineByKeyTest.getClass.getSimpleName).setMaster("local[*]")

    val sc = new SparkContext(conf)

    // conbineByKey
    val list = List(("cx", 10), ("cx", 2020), ("qw", -203), ("yu", 999), ("tr", 2020))
    val rdd1: RDD[(String, Int)] = sc.makeRDD(list, 2)

    val f1 = (v: Int) => ArrayBuffer(v)
    val f2 = (ab: ArrayBuffer[Int], v: Int) => ab += v
    val f3 = (a1: ArrayBuffer[Int], a2: ArrayBuffer[Int]) => a1 ++= a2

    // 注意,combineByKey和combineByKeyWithClassTag使用很相近,传入参数和类型接近
    val res: RDD[(String, ArrayBuffer[Int])] = rdd1.combineByKey(f1, f2, f3, new HashPartitioner(rdd1.partitions.size), false)

    println(res.collect().toBuffer)

    sc.stop()

运行结果

ArrayBuffer((tr,ArrayBuffer(2020)), (yu,ArrayBuffer(999)), (qw,ArrayBuffer(-203)), (cx,ArrayBuffer(10, 2020)))
  1. 源码
    在这里插入图片描述
    在这里插入图片描述
  • createCombiner, which turns a V into a C (e.g., creates a one-element list)
  • mergeValue, to merge a V into a C (e.g., adds it to the end of a list)
  • mergeCombiners, to combine two C’s into a single one.
    上述参数可以看出,第一个参数就是创建一个单元素的集合,可以看作是聚合的第一个初始值
    第二个参数是中间值操作,可以视为是每个分区中元素添加到这个初始值集合中的操作
    第三个元素,分区之间数据聚合

还是此前所说,Spark中的RDD从行为上可以分为transformation、action、既不是transformation也不是action三大类。
但从API接口设计来看,可以看成是底层RDD,高层RDD,高层级的RDD往往是通过调用底层RDD来实现功能的。
这样其实有很大好处,底层RDD实现之后,通过组合可以得到更加丰富的上层RDD

2.4 reduceByKey

  1. 顾名思义,就是根据key进行reduce操作,这个reduce操作是会先局部聚合,再归并每个分区聚合的结果。所以性能上会很好,符合分布式计算的思想,类似归并排序
val conf: SparkConf = new SparkConf().setAppName(ReduceByKeyTest.getClass.getSimpleName).setMaster("local[*]")

    val sc = new SparkContext(conf)

    // reduceBykey
    val list = List(("xixi", 100), ("haha", 200), ("yiyi", 300), ("haha", 400))

    val rdd1: RDD[(String, Int)] = sc.parallelize(list, 2)

    val res: RDD[(String, Int)] = rdd1.reduceByKey((a1, a2) => a1 + a2)

    // 打印
    println(res.collect().toBuffer)

    sc.stop()

运行结果

ArrayBuffer((yiyi,300), (haha,600), (xixi,100))
  1. 源码
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

从源码可以看出,reduceByKey最终是调用了combineByKeyWithClassTag,
第一个参数,(v: V) => v,没有做什么操作,只是取出每个分区第一个元素存起来
第二个参数和第三个参数一样,都是传入的func,也就是每个分区的聚合操作以及分区聚合后的结果聚合操作是一样
combineByKeyWithClassTag最终是生成了一个ShuffledRDD,
所以reduceByKey肯定有shuffle操作,按照正常思路,要把数据按照key进行聚合,肯定涉及到数据的重新打散和划分,只是打散的操作规则依赖于Partitioner分区器。
而combineByKeyWithClassTag属于PairRDDFunctions这个类

2.5 aggregateByKey

  1. 顾名思义,这是聚合函数,通过key进行聚合。
  2. 第一个参数是初始值,对每个分区都起作用,但在分区聚合之后的结果再聚合时,不会起作用。就是只会针对分区聚合起作用。
    val conf: SparkConf = new SparkConf()
                              .setAppName(AggregateByKeyTest.getClass.getSimpleName)
                              .setMaster("local[*]")

    val sc = new SparkContext(conf)

    // aggregateByKey
    val list = List(("xixi", 100), ("yiyi", 200),    ("yiyi", 300), ("haha", 400))

    val rdd1: RDD[(String, Int)] = sc.makeRDD(list, 2)

    // 可以传3个参数,第一个是初始值,第二个是局部聚合函数,第二个是每个分组聚合的函数
    val res: RDD[(String, Int)] = rdd1.aggregateByKey(1000)(Math.max(_, _), _ + _)
    println(res.collect().toBuffer)


    val res2: RDD[(String, Int)] = rdd1.aggregateByKey(10000)(_ + _, _ + _)
    println(res2.collect().toBuffer)

    sc.stop()

运行结果

ArrayBuffer((yiyi,2000), (haha,1000), (xixi,1000))

ArrayBuffer((yiyi,20500), (haha,10400), (xixi,10100))
  1. 源码
    在这里插入图片描述
    在这里插入图片描述

可以看到,aggregateByKey最终是调用了combineByKeyWithClassTag
aggregateByKey的第一个参数是初始值,对每个分区作用一次
第二个参数是分区器,可以不传入,就会使用默认的分区器在这里插入图片描述
内部代码,针对初始值,做了一次序列化,让每个分区都能有一个相同的初始值,也就是clone了多份
然后最终调用combineByKeyWithClassTag
调用combineByKeyWithClassTag的第一个参数是(v: V) => cleanedSeqOp(createZero(), v),这里是创建一个0元素的数组,然后把第一个初始值放进去
第二个参数是cleanedSeqOp,也就是针对分区中数据的操作
第三个是combOp,也就是针对每个分区操作之后结果的聚合操作
第四个是分区器

2.6 foldByKey

  1. 顾名思义,和scala集合的fold一样,数据折叠,或者叫数据聚合。
  2. 有多个重载方法,这里使用其中一个。注意这里也有初始值,并且也只对每个分区作用一次。
val conf: SparkConf = new SparkConf()
      .setAppName(FoldByKeyTest.getClass.getSimpleName)
      .setMaster("local[*]")

    val sc = new SparkContext(conf)

    // aggregateByKey
    val list = List(("xixi", 100), ("yiyi", 200),    ("yiyi", 300), ("haha", 400))

    val rdd1: RDD[(String, Int)] = sc.makeRDD(list, 2)

    val res: RDD[(String, Int)] = rdd1.foldByKey(1000)(_ + _)

    println(res.collect().toBuffer)

    sc.stop()

运行结果

ArrayBuffer((yiyi,2500), (haha,1400), (xixi,1100))
  1. 源码
    在这里插入图片描述
    在这里插入图片描述

上述源码可以看出,最终foldByKey是调用了combineByKeyWithClassTag
调用combineByKeyWithClassTag,第一个参数是(v: V) => cleanedFunc(createZero(), v) 注意,这里把初始值以及每个分区第一个value放进去了,这也是为什么初始值会对每个key生效一次的原因
第二个参数cleanedFunc,每个分区的值处理逻辑函数
第三个参数是分区之间结果聚合操作,也一样。

2.7 distinct

  1. zip回顾
    拉链效果,也就是数据变成一队一队的结果。
val conf: SparkConf = new SparkConf()
      .setAppName(DistinctTest1.getClass.getSimpleName)
      .setMaster("local[*]")

    val sc = new SparkContext(conf)

    // zip
    val rdd1: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4, 5, 6, 7, 8), 2)
    val rdd2: RDD[String] = sc.makeRDD(List("a", "b", "c", "d", "e", "f", "g", "h"), 2)

    val res: RDD[(Int, String)] = rdd1.zip(rdd2)

    println(res.collect().toBuffer)

    sc.stop()

运行结果

ArrayBuffer((1,a), (2,b), (3,c), (4,d), (5,e), (6,f), (7,g), (8,h))

注意分区数和元素个数必须相同,否则就会除左
在这里插入图片描述
在这里插入图片描述
源码
在这里插入图片描述

从源码可以看出,为什么要求元素个数和分区数相同,因为使用的是zipPartitions,针对每个分区进行匹配

  1. distinct
    顾名思义,这是去重的算子。
val conf: SparkConf = new SparkConf()
      .setAppName(DistinctTest2.getClass.getSimpleName)
      .setMaster("local[*]")

    val sc = new SparkContext(conf)

    // distinct
    val rdd1: RDD[Int] = sc.makeRDD(List(1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6))

    val res: RDD[Int] = rdd1.distinct()

    println(res.collect().toBuffer)

    sc.stop()

运行结果

ArrayBuffer(1, 2, 3, 4, 5, 6)

源码
在这里插入图片描述
在这里插入图片描述

从源码可以看出,大致是2种情况,第一种是使用map去重
第二种是先把数据转换为key value,value是null。然后使用reduceByKey进行聚合,这样一来,相同的key就会被聚合到一起,然后通过map(_._1)将key取出,得出去重后的结果。

总结:RDD算子分为高层级和底层级RDD,高级别的一般是通过使用底层RDD调用实现的。在代码实现时,会优先使用分布式思想,尽量将计算分散到不同节点上处理,最终结果汇总在一起。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值