Spark快速大数据分析系列之第四章键值对操作

来源:互联网 发布:淘宝一次性烟嘴过滤器 编辑:程序博客网 时间:2024/05/08 03:01

动机

  1. 键值对形式的RDD提供了新的强大的操作接口
  2. 键值对形式的RDD具有一个重要特性:分区。一些情况下可以显著提升性能

创建Pair RDD

  1. 读取外部数据时:如果外部数据本身是键值对形式的,读取回来的RDD也是键值对形式
  2. 个普通的 RDD 转为 pair RDD 时,可以使用map()函数
    val pairs = lines.map(x => (x.split(" ")(0), x))    //x是字符串,把字符串的一个作为键,把字符串作为值

Pair RDD的转化操作

pair RDD的转化操作:

键值对基本上可以使用所有普通RDD的转化操作,不过操作对象变成了二元组(键,值)。




针对两个pair RDD的转化操作



聚合操作(转化操作)

聚合具有相同键的元素

  1. reduceByKey(func): 于reduce()类似,接受一个函数,用该函数对数值进行合并。而reduceByKey()不是对整个RDD进行数值合并,是对具有相同的键值进行合并。返回各键和对应键归约出来的结果值组成的新的 RDD
  2. foldByKey():与 fold() 相当类似;它们都使用一个与 RDD 和合并函数中的数据类型相同的零值作为初始值。与 fold() 一样,foldByKey() 操作所使用的合并函数对零值与另一个元素进行合并,结果仍为该元素。
  3. combineByKey(createCombiner, mergeVal, mergeComb) :通过一个例子来说明其作用:
    求每个键对应的平均值createCombiner = (lambda el: (el, 1))                 //对每个元素el的进行的操作mergeVal = (lambda aggregated, el : (aggregated[0]+el,aggregated[1]+1))   //作用范围:每个分区。对分区中的元素进行统计,每个分区返回一个统计结果mergeComb = (lambda aggregated, el :(aggregated[0]+el[0],aggregated[1]+1))   //作用范围:整个RDD。把每个分区的统计结果进行整合sumCount = nums.combineByKey(createCombiner, mergeVal, mergeComb) sumCount.map(lambda key, xy: (key, xy[0]/xy[1])).collectAsMap()

Pair RDD的行动操作

和转化操作一样,所有基础 RDD 支持的传统行动操作也都在 pair RDD 上可用。


并行度调优

每个 RDD 都有固定数目的分区,分区数决定了在 RDD 上执行操作时的并行度。可以通过修改RDD的分区数进行并行调优

修改RDD分区数的方法

  1. 聚合或分组操作时,可以通过第二个参数给定分区数(本章所讨论的大部分操作都可以接受第二个参数)
    val data = Seq(("a", 3), ("b", 4), ("a", 1))sc.parallelize(data).reduceByKey((x, y) => x + y)  //默认并行度sc.parallelize(data).reduceByKey((x, y) => x + y,10) //给定并行度

  2. 通过repartition() 函数修改分区数,注意:该方法会通过网络进行数据混洗,非常耗能。他有一个优化版本coalesce()。

查看分区数: scala和java通过rdd.partitions.size() 以及 Python 中的 rdd.getNumPartitions 查看 RDD 的分区数

数据分区(进阶)

RDD是通过对键的哈希值进行分区的,一个分区可能包括一个或者多个哈希值的键,但同样哈希值的键肯定在同一个分区内。

在基于键的操作中,每个键都存在同一个分区中,减少了通讯开销,极大的提高了整体性能。如下面的例子:

  1. 两个RDD:userData和events,两个RDD都没有进行分区。
    执行:
    joined = userData.join(events) //进行连接操作
    操作时会进行两次网络混洗:  



  2. 两个RDD:userData和events,对userData进行分区操作,events不进行分区操作。

    对userData进行分区操作:

val userData = sc.sequenceFile[Key, valuce]("××").partitionBy(new HashPartitioner(100))// 构造100个分区.persist()

同样执行
joined = userData.join(events) //进行连接操作
操作时只会进行一次网络混洗:

获取RDD的分区方式

通过访问RDD的partitioner属性。

rdd.partitions查看每个分区的分区方式rdd.partitions.size()查看分区个数

从分区中获益的操作

就 Spark 1.0 而 言, 能 够 从 数 据 分 区 中 获 益 的 操 作 有

cogroup()、groupWith()、 join()、leftOuterJoin()、 rightOuterJoin()groupByKey()、reduceByKey()、combineByKey() 以及 lookup()。

影响分区方式的操作

  • 改变键值的转化操作不存在特定的分区方式:map()
  • 不改变键值的转化操作不改变分区方式:mapValues() 和flatMapValues();可以代替map()
  • 为结果 RDD 设好分区方式的操作:cogroup()、groupWith()、join() 、 lef tOuterJoin() 、 rightOuterJoin() 、 groupByKey() 、 reduceByKey() 、combineByKey()、partitionBy()、sort()、mapValues()(如果父 RDD 有分区方式的话) 、flatMapValues()(如果父 RDD 有分区方式的话) ,以及 filter()(如果父 RDD 有分区方式的话)。其他所有的操作生成的结果都不会存在特定的分区方式。
  • 二元操作:输出数据的分区方式取决于父 RDD 的分区方式。默认情况下,结果会采用哈希分区,分区的数量和操作的并行度一样。不过,如果其中的一个父 RDD 已经设置过分区方式,那么结果就会采用那种分区方式;如果两个父 RDD 都设置过分区方式,结果 RDD 会采用第一个父 RDD 的分区方式。


自定义分区方式

要实现自定义的分区器,你需要继承 org.apache.spark.Partitioner 类并实现下面三个方法。

  • numPartitions: Int:返回创建出来的分区数。
  • getPartition(key: Any): Int:返回给定键的分区编号(0 到 numPartitions-1)。
  • equals():Java 判断相等性的标准方法。这个方法的实现非常重要,Spark 需要用这个方法来检查你的分区器对象是否和其他分区器实例相同,这样 Spark 才可以判断两个
    RDD 的分区方式是否相同。

class DomainNamePartitioner(numParts: Int) extends Partitioner {        override def numPartitions: Int = numParts        override def getPartition(key: Any): Int = {                val domain = new Java.net.URL(key.toString).getHost()                val code = (domain.hashCode % numPartitions)                if(code < 0) {                        code + numPartitions // 使其非负                }else{                code                }        }// 用来让Spark区分分区函数对象的Java equals方法        override def equals(other: Any): Boolean = other match {                case dnp: DomainNamePartitioner =>                dnp.numPartitions == numPartitions                case _ =>                false        }}