Spark(四)Spark 键值对操作

目录:

4、键值对操作

4.1、创建PairRDD

4.2、PairRDD的转化操作

4.2.1、聚合操作

4.2.2、数据分组

4.2.3、连接

4.2.4、数据排序

4.3、PairRDD的行动操作

4.4、数据分区

4.4.1、获取RDD的分区方式

4.4.2、从分区中或获益的操作

4.3.3、影响分区方式的操作

4.4.4、示例:PageRank


4、键值对操作

Spark为包含键值对类型的RDD提供了一些专有的操作。这些RDD被称为pairRDD。PairRDD是很多程序的构成要素,因为它们提供了并行操作各个键和跨节点重新进行数据分组的操作接口。例如,pairRDD提供reduceByKey()方法,可以分别归约每个键对应的数据,还有join()方法,可以把两个RDD中的键相同的元素组合到一起,合并为一个RDD。

4.1、创建PairRDD

Java中由于没有自带的二元组类型,因此Spark的JavaAPI让用户使用scale.Tuple2类来创建二元组。可以通过new Tuple2(e1, e2)来创建一个新的二元组。并且可以通过 ._1() 和 ._2() 方法访问其中的元素。

使用Java从内存数据创建pairRDD的话,需要使用SparkContext.parallelizePairs()。

在Java中使用第一个单词作为键创建出一个pairRDD。

PairFunction<String, String, String> keyData = 
new PairFunction<String, String, String>() {
	@Override
	public Tuple2<String, String> call(String x) throws Exception {
		return new Tuple2(x.split(" "), x);
	}
};
JavaPairRDD<String, String> pairs = lines.mapToPair(keyData);

4.2、PairRDD的转化操作

PairRDD可以使用所有标准RDD上的可用的转化操作。由于pairRDD中包含二元组,所以需要传递的函数应当操作二元组而不是独立的元素。

表4-1 :PairRDD的转化操作(以键值对集合 {(1, 2), (3, 4), (3, 6)} 为例)

表4-2:针对两个pair RDD的转化操作(rdd =  {(1, 2), (3, 4), (3, 6)} other =  {(3, 9)} )

对第二个元素进行筛选(筛选掉长度超过20个字符的行)

Function<Tuple2<String, String>, Boolean> longWordFilter = 
				 new Function<Tuple2<String, String>, Boolean>(){
	@Override
public Boolean call(Tuple2<String, String> keyValue) throws Exception {
		return (keyValue._2.length() < 20);
	}
};
JavaPairRDD<String, String> result = pairs.filter(longWordFilter);

4.2.1、聚合操作

reduceByKey() 与 reduce() 相当类似;它们都接收一个函数,并使用该函数对值进行合并。reduceByKey() 会为数据集中的每个键进行并行的归约操作,每个归约操作会将键相同的值合并起来。因为数据集中可能有大量的键,所以 reduceByKey() 没有被实现为向用户程序返回一个值的行动操作。实际上,它会返回一个由各键和对应键归约出来的结果值组成的新的 RDD。

foldByKey() 则与 fold() 相当类似;它们都使用一个与 RDD 和合并函数中的数据类型相同的零值作为初始值。与 fold() 一样, foldByKey() 操作所使用的合并函数对零值与另一个元素进行合并,结果仍为该元素。

java实现单词计数

1、Java版本jdk1.8以下

2、Java版本jdk1.8:可以使用lambda表达式简化代码

如何处理每个元素的。由于 combineByKey() 会遍历分区中的所有元素,因此每个元素的键要么还没有遇到过,要么就 和之前的某个元素的键相同。

         如果这是一个新的元素,combineByKey()会使用一个叫做createCombiner()的函数来创建那个键对应的累加器的初始值。需要注意的是,这个过程会在每个分区中第一次出现各个键时发生,而不是在整个RDD中第一次出现各个键时发生。

         如果这是一个在处理当前分区之前已经遇到的键,它会使用mergeValue()方法将该键的累加器对应的当前值域新的值进行合并。

         由于每个分区都是独立处理的,因此对于同一键可以有多个累加器。如果有两个或者更多的分区都有对应同一键的累加器,就需要使用用户提供的mergeCombiners()方法将各个分区的结果合并。

         combineByKey()有多个参数分别对应聚合操作的各个阶段,因而非常适合用来解释聚合操作各个阶段的功能划分。为了更好的演示combineByKey()是如何工作的,下面展示如何在java中计算各键对应的平均值。

在Java中使用combineByKey()求各个键对应的平均值

并行度调优:

Spark是怎样确定如何分割工作的? 每个RDD都有固定数目的分区,分区数决定了在RDD上操作时的并行度。在执行聚合或分组操作时,可以要求Spark使用给定的分区数。Spark始终尝试根据的大小推断出一个有意义的默认值,对并行度进行调优来获取更好的性能表现。

大多数操作符都能接受第二个参数,这个参数用来指定分组结果或聚合结果的RDD的分区数。

//设置并行度

sc.parallelize(data).reduceByKey(x, y -> x + y, 10)

Spark 提供了 repartition() 函数。它会把数据通过网络进行混洗,并创建出新的分区集合对数据进行重新分区是代价相对比较大的操作.Spark 中也有一个优化版的 repartition(),叫作 coalesce()。你可以使用 Java Scala 中的 rdd. partitions.size() 查看 RDD 的分区数,并确保调 coalesce() 时将 RDD 合并到比现在的分区数更少的分区中。

4.2.2、数据分组

如果数据已经以预期的方式提取了键,groupByKey()就会使用RDD中的键来对数据进行分组。对一个由类型K的键和类型V的值组成的RDD,所得到的结果RDD类型会是[K, Iterable[V]]。

groupByKey()可以用于未成对的数据上,也可以根据键相同以外的条件进行分组,它可以接收一个函数,对源RDD中的每个元素使用该函数,将返回结果作为键再进行分组。

对单RDD 的数据进行分组,可使用 cogroup() 函数对多个共享同一个键的 RDD 进行分组。对两个键的类型均为 K 而值的类型分别为V 和 W 的 RDD 进行 cogroup() 时,得到的结果 RDD 类型为 [(K, (Iterable[V], Iterable[W]))]。如果其中的 一个 RDD 对于另一个 RDD 中存在的某个键没有对应的记录,那么对应的迭代器则为空。 cogroup() 提供了为多个 RDD 进行数据分组的方法。

cogroup() 不仅可以用于实现连接操作,还可以用来求键的交集。

4.2.3、连接

         将有键的数据与另一组有键的数据一起使用时对键值对数据执行的最有效的操作之一。连接数据可能是pairRDD最常用的操作之一。连接方式多种多样:右外连接、左外连接、交叉连接以及内连接。

         普通的join操作符表示内连接。只有在两个pairRDD中都存在的键才叫输出。当一个输入对应的某个键有多个值时,生成的pairRDD会包括来自两个输入RDD的每一组相对应的记录。

         “连接”是数据库术语,表示将两张表根据数据相同的值来组合字段。

         leftOuterJoin(other)和rightOuterJoin(other)都会根据键连接两个RDD,但是允许结果中存在其中的一个pairRDD所缺失的键。

         使用leftOuterJoin()产生的pairRDD中,源RDD的每一个键都有对应的记录。每一个键相应的值是由一个源RDD中的值与一个包含第二个RDD的值的Option(在Java中为Optional)对象组成的二元组。

4.2.4、数据排序

如果键有已定义的顺序,就可以对这种键值对的RDD进行排序。当把数据排好序后,后续对数据进行collect()或save()等操作都会得到有序的数据。

我们经常要将RDD排序排列,因此sortByKey()函数接收一个叫做ascending的参数,表示我们是否要想让结果按升序排序(默认为true)。以提供自定义的比较函数。

下面例子会将证书转为字符串,然后使用字符串比较函数来对RDD进行排序。

class IntegerComparator implements Comparator<Integer> { 
	public int compare(Integer a, Integer b) { 
		return String.valueOf(a).compareTo(String.valueOf(b)); 
	} 
} 
rdd.sortByKey(comp);

4.3、PairRDD的行动操作

所有基础RDD支持的传统行动操作也都在pairRDD上可用。pairRDD提供一些额外的行动操作,可以让我们充分利用数据的键值对特性。

pairRDD的行动操作(以键值对集合{(1, 2}, (3, 4), (3, 6)}为例)

函数

描述

示例

结果

countByKey()

对每个键对应的元素分别计数

rdd.countByKe()

{(1, 1), (3, 2)}

collectAsMap()

将结果以映射表的形式返回,以便查询

rdd.collectAsMap()

Map{(1, 2),(3, 4),(3, 6)}

lookup(key)

返回给定键对应的所有值

rdd.lookup(3)

[4, 6]

4.4、数据分区

1. 什么是分区

RDD 内部的数据集合在逻辑上(以及物理上)被划分成多个小集合,这样的每一个小集合被称为分区。RDDprdd作为一个分布式的数据集,是分布在多个worker节点上的。如下图所示,RDD1有五个分区(partition),他们分布在了四个worker nodes 上面,RDD2有三个分区,分布在了三个worker nodes上面。

  

2. 为什么要分区

分区的个数决定了并行计算的粒度。多个分区并行计算,能够充分利用计算资源。

3. 如何手动分区

java的分区可以这样(parallelize)

JavaRDDrdd = sc.parallelize(list, 2); // 这个是分区用了,指定创建得到的 RDD 分区个数为 2

pairs.partitions().size() 分区数量查看

对数据在节点间的分区进行控制。在分布式程序中,通信的代价是很大的,因此控制数据分布以获得最少的网络传输可以极大的提升整体性能和单点的程序需要为记录选择合适的数据结构一样, Spark程序可以通过控制RDD分区的方式来减少通信开销。

只有当数据集多次在诸如连接这种基于键的操作中使用时,分区才会有帮助。Spark中所有的键对RDD都可以进行分区。系统会根据一个针对键的函数对元素进行分组。Spark可以确保同一组的键出现在同一个节点上。

你可以使用哈希分区将一个RDD分成了100个分区,此时键的哈希值对100取模的结果相同的记录会被放在一个节点上。你也可以使用范围分区法,将键在同一个范围区间的记录都放在同一个节点上。

默认情况下,连接操作会将两个数据集中的所有键的哈希值都求出来,将该哈希值相同的记录通过网络传到同一台机器上,然后再那台机器上对所有键相同的记录进行连接操作。因为userData表比每分钟出现的访问日志表events要大得多,所有要浪废时间做很多额外工作:在每次调用时都对userData表进行哈希值计算和跨节点数据混洗,虽然这些从来都不会变化。

解决:在程序开始时,对userDat a表使用partitionBy()转化操作,将这些表转为哈希分区。可以通过向partitionBy()传递一个spark.HashPartirion对象来实现该操作。

4.4.1、获取RDD的分区方式

在Scale和Java中可以使用RDD的partitioner属性(java中使用partitioner()方法来获取RDD的分区方式。

在Sparkshell中使用partitioner属性不仅是检验各种Spark操作如何影响分区方式的一种好方法,还可以在你的程序中检查想要使用的操作是否会生成正确的结果。

4.4.2、从分区中或获益的操作

Spark的许多操作都引入了将数据根据键跨节点进行混洗的过程。所有这些操作都会 从数据分区中获益(减少网络传输)。

对于像 reduceByKey() 这样只作用于单个RDD 的操作,运行在未分区的 RDD 上的时候会导致每个键的所有对应值都在每台机器上进行本地计算,只需要把本地最终归约出的结果值从各工作节点传回主节点,所以原本的网络开销就不算大。

而对于诸如 cogroup() join() 这样的二元操作,预先进行数据分区会导致其中至少一个 RDD(使用已知分区器的那个 RDD)不发生数据混洗。如果两个 RDD 使用同样的分区方式,并且它们还缓存在 同样的机器上(比如一个 RDD 是通过 mapValues() 从另一个 RDD 中创建出来的,这两个 RDD 就会拥有相同的键和分区方式),或者其中一个 RDD 还没有被计算出来,那么跨节 点的数据混洗就不会发生了。

4.3.3、影响分区方式的操作

Spark 内部知道各操作会如何影响分区方式,并将会对数据进行分区的操作的结果 RDD 自动设置为对应的分区器。

转化操作的结果并不一定会按已知的分区方式分区,这时输出的 RDD 可能就会没有设置分区器。

另外两个操作 mapValues() 和 flatMapValues() 作为替代方法,它们可以保证每个二元组的键保持不变。

其他所有的操作生成的结果都不会存在特定的分区方式。

4.4.4、示例:PageRank

PageRank是一种从RDD分区中获益的更复杂的算法。是用来根据外部文档指向一个文档的链接,对集合中每个文档的重要程度赋一个度量值。该算法可以用于对网页进行排序,当然,也可以用于排序科技文章或社交网络中有影响的用户。

PageRank 是执行多次连接的一个迭代算法,因此它是 RDD 分区操作的一个很好的用例。算法会维护两个数据集:一个由 (pageID, linkList) 的元素组成,包含每个页面的相邻页面的列表;另一个由 (pageID, rank) 元素组成,包含每个页面的当前排序值。它按如下步骤进行计算。

(1) 将每个页面的排序值初始化为 1.0 。

(2) 在每次迭代中,对页面 p ,向其每个相邻页面(有直接链接的页面)发送一个值为rank(p)/numNeighbors(p) 的贡献值。

(3) 将每个页面的排序值设为 0.15 + 0.85 * contributionsReceived 。

 

猜你喜欢

转载自blog.csdn.net/MyronCham/article/details/85699282
今日推荐