一、RDD概念
RDD(英文全称Resilient Distributed Dataset),即弹性分布式数据集是spark中引入的一个数据结构,是Spark中最基本的数据抽象,代表一个不可变、可分区、里面的元素可并行计算的集合。
Resilient弹性:RDD的数据可以存储在内存或者磁盘当中,RDD的数据可以分区。
Distributed分布式:RDD的数据可以分布式存储,可以进行并行计算。
Dataset数据集:一个用于存放数据的集合。
在上一篇《SparkRDD的转换与动作算子》中,介绍了SparkRDD中的一些常用算子,下面介绍RDD中的一些重要算子,是在实际是工作用的比较多,而且比较复杂的一些算子。
以下演示通过SecureCRTPortable客户端远程连接Linux服务器操作pyspark。
连接界面如下:
二、分区算子
作用:针对整个分区数据进行处理的算子。
1、mapPartitions算子
输入数据:rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10],3),先查看分区情况
>>> rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10],3)
# 查看分区
>>> rdd.glom().collect()
# 运行结果 [[1, 2, 3], [4, 5, 6], [7, 8, 9, 10]]
解释:parallelize(data,num)通过自定义列表的方式初始化RDD对象。(一般用于测试)
其中data为输入的数据,num为选择分区数。
glom() 可以将RDD以分区形式输出,查看数据在哪个分区。
getNumPartitions() 查看当前RDD有多少个分区
目前parallelize设置了3个分区,因此输出结果有3个列表。
以下通过map和mapPartitions的对比查看两个算子的区别:
需求: 对数字加一
map效果代码:
>>> rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10],3)
>>>
#自定义函数
>>> def my_add(num):
... print(f"传递进来的数据{num}")
... return num+1
...
>>> rdd.map(my_add).collect()
### 运行结果
传递进来的数据4
传递进来的数据5
传递进来的数据6
传递进来的数据1
传递进来的数据2
传递进来的数据3
传递进来的数据7
传递进来的数据8
传递进来的数据9
传递进来的数据10
[2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
总结:map被调用了10次,反复操作会导致资源消耗,浪费资源。
mapPartitions效果代码:
>>> rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10],3)
# 自定义函数
>>> def my_add(list):
... print("输入的参数",list)
...
... new_list = []
...
... for i in list:
... new_list.append(i + 1)
... return new_list
...
>>> rdd.mapPartitions(my_add).collect()
### 运行结果
输入的参数 <itertools.chain object at 0x7f96e319c940>
输入的参数 <itertools.chain object at 0x7f96e319c940>
输入的参数 <itertools.chain object at 0x7f96e3375e50>
[2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
总结:可以看出使用mapPartitions分区算子只调用了3次资源,减少了大量的重复操作,节省资源。
2、foreachPartition算子
以下通过foreach和foreachPartition的对比查看两个算子的区别:
需求: 遍历打印
foreach效果代码:
>>> rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10],3)
# 自定义函数
>>> def my_print(num):
... print(f"传递进来的数据{num}")
... print(num)
...
>>> rdd.foreach(my_print)
### 运行结果
传递进来的数据4
4
传递进来的数据5
5
传递进来的数据6
6
传递进来的数据1
1
传递进来的数据2
2
传递进来的数据3
3
传递进来的数据7
7
传递进来的数据8
8
传递进来的数据9
9
传递进来的数据10
10
foreachPartition效果代码:
>>> rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10],3)
# 自定义函数
>>> def my_print(list):
... print(f"传递进来的数据{list}")
...
... for i in list:
... print(i)
...
>>> rdd.foreachPartition(my_print)
### 运行结果
传递进来的数据<itertools.chain object at 0x7f96e319c2b0>
4
5
6
传递进来的数据<itertools.chain object at 0x7f96e319c2b0>
1
2
3
传递进来的数据<itertools.chain object at 0x7f96e3375a60>
7
8
9
10
mapPartitions和foreachPartition分区算子总结
1、 map和foreach算子都有对应的分区算子,分别是mapPartitions和foreachPartition
2、 分区算子适用于有反复消耗资源的操作,例如:文件的打开和关闭、数据库的连接和关闭等,能够减少操作的次数。
3、 如果没有反复消耗资源的操作,调用两类算子,效果一样。
三、重分区算子
作用:对RDD的分区重新进行分区操作的算子,也就是改变RDD分区数的算子。
1、repartition算子
格式:repartition(num)
作用:改变RDD分区数。既能够增大RDD分区数,也能够减小RDD分区数。但是都会导致发生Shuffle过程。
需求:增大与减小分区
代码:
>>> rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10],3)
# 查看分区
>>> rdd.glom().collect()
[[1, 2, 3], [4, 5, 6], [7, 8, 9, 10]]
# 增大分区
>>> rdd.repartition(5).glom().collect()
[[], [1, 2, 3], [7, 8, 9, 10], [4, 5, 6], []]
# 减小分区
>>> rdd.repartition(2).glom().collect()
[[1, 2, 3, 7, 8, 9, 10], [4, 5, 6]]
2、coalesce算子
格式:coalesce(num,shuffle=True|False)
作用:改变RDD分区数。但是,默认只能减小RDD分区数,不能增大,减小过程中不会发生Shuffle过程。如果想增大分区,需要将参数shuffle设置为True,但是会导致Shuffle过程。
需求:减小再增大分区
代码:
>>> rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10],3)
# 查看分区
>>> rdd.glom().collect()
[[1, 2, 3], [4, 5, 6], [7, 8, 9, 10]]
# 减小分区
>>> rdd.coalesce(2).glom().collect()
[[1, 2, 3], [4, 5, 6, 7, 8, 9, 10]]
# 增大分区
>>> rdd.coalesce(5).glom().collect()
[[1, 2, 3], [4, 5, 6], [7, 8, 9, 10]]
需求:将参数2设置为True,再增大与减少分区
代码:
>>> rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10],3)
# 将参数2设置为True,再增大分区
>>> rdd.coalesce(5,shuffle=True).glom().collect()
[[], [1, 2, 3], [7, 8, 9, 10], [4, 5, 6], []]
# 将参数2设置为True,再减小分区
>>> rdd.coalesce(2,shuffle=True).glom().collect()
[[1, 2, 3, 7, 8, 9, 10], [4, 5, 6]]
repartition 和 coalesce总结
1、 这两个算子都是用来改变RDD的分区数。
2、 repartition 既能够增大RDD分区数,也能够减小RDD分区数。但是都会导致发生Shuffle过程。
3、 默认只能减小RDD分区数,不能增大,减小过程中不会发生Shuffle过程。如果想增大分区,需要将参数shuffle设置为True,但是会导致Shuffle过程。
4、 repartition 底层实际上是调用了coalesce算子,并且将shuffle参数设置为了True。
3、partitionBy算子
格式:partitionBy(num,[fn])
作用:该算子主要是用来改变key-value键值对数据类型RDD的分区数的。num表示要设置的分区数;fn参数是可选,用来让用户自定义分区规则。
代码:
>>> rdd = sc.parallelize([(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(10,10)],5)
# 查看分区
>>> rdd.glom().collect()
[[(1, 1), (2, 2)], [(3, 3), (4, 4)], [(5, 5), (6, 6)], [(7, 7), (8, 8)], [(9, 9), (10, 10)]]
# 增大分区,尝试分为20个分区
>>> rdd.partitionBy(20).glom().collect()
[[], [(1, 1)], [(2, 2)], [(3, 3)], [(4, 4)], [(5, 5)], [(6, 6)], [(7, 7)], [(8, 8)], [(9, 9)], [(10, 10)], [], [], [], [], [], [], [], [], []]
# 减少分区,尝试分为2个分区
>>> rdd.partitionBy(2).glom().collect()
[[(2, 2), (4, 4), (6, 6), (8, 8), (10, 10)], [(1, 1), (3, 3), (5, 5), (7, 7), (9, 9)]]
# 将 key>5 放置在一个分区,剩余放置到另一个分区
>>> rdd.partitionBy(2,partitionFunc=lambda key:0 if key > 5 else 1).glom().collect()
[[(6, 6), (7, 7), (8, 8), (9, 9), (10, 10)], [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]]
# 注意: 分区编号的数据类型需要是int类型
总结:
默认情况下,根据key进行Hash取模分区。如果对默认分区规则不满意,可以传递参数fn来自定义分区规则。但是自定义分区规则函数需要满足两个条件,条件一:分区编号的数据类型需要是int类型;条件二:传递给自定义分区函数的参数是key。
四、聚合算子
(一)单值类型的聚合算子
1、reduce(fn1)
作用:根据传入函数对数据进行聚合处理
需求:求和计算, 求所有数据之和
代码:
>>> rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10],3)
# 查看分区情况
>>> rdd.glom().collect()
[[1, 2, 3], [4, 5, 6], [7, 8, 9, 10]]
# 自定义函数
>>> def my_sum(agg,curr):
... return agg+curr
...
>>> rdd.reduce(my_sum)
# 运行结果
55
2、fold(defaultAgg,fn1)
作业:根据传入函数对数据进行聚合处理,同时支持给agg设置初始值。
需求:求和计算, 求所有数据之和
代码:
>>> rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10],3)
# 查看分区情况
>>> rdd.glom().collect()
[[1, 2, 3], [4, 5, 6], [7, 8, 9, 10]]
# 自定义函数
>>> def my_sum(agg,curr):
... return agg+curr
...
>>> rdd.fold(5,my_sum)
# 运行结果
75
3、aggregate(defaultAgg, fn1, fn2)
作业:根据传入函数对数据进行聚合处理。defaultAgg设置agg的初始值,fn1对各个分区内的数据进行聚合计算,fn2 负责将各个分区的聚合结果进行汇总聚合。
需求:求和计算, 求所有数据之和
代码:
>>> rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10],3)
# 查看分区情况
>>> rdd.glom().collect()
[[1, 2, 3], [4, 5, 6], [7, 8, 9, 10]]
# 自定义函数
>>> def my_sum_1(agg,curr):
... return agg+curr
...
>>> def my_sum_2(agg,curr):
... return agg+curr
...
>>> rdd.aggregate(5,my_sum_1,my_sum_2)
# 运行结果
75
单值类型的聚合算子总结
reduce、fold、aggregate算子都能实现聚合操作。reduce的底层是fold,fold底层是aggregate。在工作中,如果能够通过reduce实现的,就优先选择reduce;否则选择fold,实在不行就选择aggregate。
(二)KV类型的聚合函数
相关的算子
1、reduceByKey(fn1)
2、foldByKey(defaultAgg, fn1)
3、aggregateByKey(defaultAgg, fn1, fn2);
以上三个与单值是一样的,只是在单值的基础上加了分组的操作而已,针对每个分组内的数据进行聚合而已。
另外有一个:groupByKey() 仅分组,不聚合统计。
问题:groupByKey() + 聚合操作 和 reduceByKey() 都可以完成分组聚合统计,谁的效率更高一些?
答: reduceByKey(),因为底层会进行局部的聚合操作,会减小后续处理的数据量。
reduceByKey:
groupByKey:
五、关联算子
关联函数,主要是针对kv类型的数据,根据key进行关联操作
相关的算子:
1、join:实现两个RDD的join关联操作
2、leftOuterJoin:实现两个RDD的左关联操作
3、rightOuterJoin:实现两个RDD的右关联操作
4、fullOuterJoin:实现两个RDD的满外(全外)关联操作
以上这些关联算子的作用跟HiveSQL是一样的。