动机
- 键值对形式的RDD提供了新的强大的操作接口
- 键值对形式的RDD具有一个重要特性:分区。一些情况下可以显著提升性能
创建Pair RDD
- 读取外部数据时:如果外部数据本身是键值对形式的,读取回来的RDD也是键值对形式
- 个普通的 RDD 转为 pair RDD 时,可以使用map()函数
val pairs = lines.map(x => (x.split(" ")(0), x)) //x是字符串,把字符串的一个作为键,把字符串作为值
Pair RDD的转化操作
pair RDD的转化操作:
键值对基本上可以使用所有普通RDD的转化操作,不过操作对象变成了二元组(键,值)。
针对两个pair RDD的转化操作
聚合操作(转化操作)
聚合具有相同键的元素
- reduceByKey(func): 于reduce()类似,接受一个函数,用该函数对数值进行合并。而reduceByKey()不是对整个RDD进行数值合并,是对具有相同的键值进行合并。返回各键和对应键归约出来的结果值组成的新的 RDD
- foldByKey():与 fold() 相当类似;它们都使用一个与 RDD 和合并函数中的数据类型相同的零值作为初始值。与 fold() 一样,foldByKey() 操作所使用的合并函数对零值与另一个元素进行合并,结果仍为该元素。
- 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分区数的方法
- 聚合或分组操作时,可以通过第二个参数给定分区数(本章所讨论的大部分操作都可以接受第二个参数)
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) //给定并行度
- 通过repartition() 函数修改分区数,注意:该方法会通过网络进行数据混洗,非常耗能。他有一个优化版本coalesce()。
查看分区数: scala和java通过rdd.partitions.size() 以及 Python 中的 rdd.getNumPartitions 查看 RDD 的分区数
数据分区(进阶)
RDD是通过对键的哈希值进行分区的,一个分区可能包括一个或者多个哈希值的键,但同样哈希值的键肯定在同一个分区内。
在基于键的操作中,每个键都存在同一个分区中,减少了通讯开销,极大的提高了整体性能。如下面的例子:
- 两个RDD:userData和events,两个RDD都没有进行分区。
执行:
操作时会进行两次网络混洗:joined = userData.join(events) //进行连接操作
- 两个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
}
}