Spark三大数据结构之RDD算子
RDD算子
简单介绍
RDD(Resilient Distributed Dataset)是Apache Spark中的基本数据结构,它代表一个分布式、不可变的数据集。RDD提供了一系列的算子(操作符),用于在分布式计算中对数据进行转换和操作。这些算子可以分为两类:转换算子(Transformation)和动作算子(Action)。
转换算子:功能的补充和封装,负责将旧的RDD封装为新的RDD,这些算子是惰性求值的,只有在执行动作算子时才会触发计算。
动作算子:触发任务的调度和执行。
RDD转换算子
1. 单value类型
1.1 map
- 功能: 对RDD的每个元素应用一个函数,生成一个新的RDD。
- 示例: 从服务器日志数据
apache.log
中获取用户请求URL的资源路径。
1.2 mapPartitions
- 功能: 以分区为单位进行数据转换操作。
- 优点: 可以以分区为单位进行数据转换操作,适用于数据量大的情况。
- 缺点: 需要将整个分区的数据加载到内存进行引用,可能导致内存溢出。
1.3 mapPartitionsWithIndex
- 功能: 分区索引加上了分区号,以分区为单位发送到计算节点进行处理。
- 示例: 获取第二个数据分区的数据。
1.4 flatMap
- 功能: 将处理的数据进行扁平化后再进行映射,也称为扁平映射。
- 示例: 将
List(List(1,2),3,List(3,4))
进行扁平化操作。
1.5 glom
- 功能: 将同一分区的数据直接转换为相同类型的内存数组进行处理,分区不变。
- 示例: 计算所有分区最大值求和(分区内取最大值,分区间最大值求和)。
1.6 groupBy
- 功能: 根据指定的规则进行分组,生成元组
(key, values)
。 - 注意: 会导致数据打乱重新组合,存在shuffle操作。
1.7 filter
- 功能: 根据给定条件筛选出满足条件的元素,生成一个新的RDD。
1.8 sample
- 功能: 根据指定的规则从数据集中抽取数据。
1.9 distinct
- 功能: 将数据集中重复的数据去重。
1.10 coalesce
- 功能: 缩减分区,用于提高小数据集的执行效率。
1.11 repartition
- 功能: 扩大或缩减分区,可选择是否进行shuffle。
2. 双value类型
2.1 intersection
- 功能: 对源RDD和参数RDD求交集。
2.2 union
- 功能: 对源RDD和参数RDD求并集。
2.3 subtract
- 功能: 以一个RDD元素为主,去除两个RDD中重复元素,将其他元素保留下来。
2.4 zip
- 功能: 将两个RDD中的元素按位置组合成一个新的RDD。
3. key-value类型
3.1 partitionBy
- 功能: 将数据按照指定的Partitioner重新进行分区。
3.2 reduceByKey
- 功能: 对具有相同键的元素进行分组并进行聚合。
3.3 groupByKey
- 功能: 将数据源的数据根据key对value进行分组。
3.4 aggregateByKey
- 功能: 将数据根据不同的规则进行分区内计算和分区间计算。
3.5 foldByKey
- 功能: 当分区内计算规则和分区间计算规则相同时,可以简化为foldByKey。
3.6 combineByKey
- 功能: 对key-value型RDD进行通用的聚集操作。
3.7 join
- 功能: 将两个RDD中的元素按键进行合并。
3.8 leftOuterJoin
- 功能: 类似于SQL语句的左外连接。
3.9 cogroup
- 功能: 在类型为(K,V)和(K,W)的RDD上调用,返回一个(K,(Iterable[V], Iterable[W]))类型的RDD。
行动算子
1. reduce
- 功能: 聚集RDD中的所有元素,先聚合分区内数据,再聚合分区间数据。
- 示例:
rdd.reduce((x, y) => x + y)
将RDD中的所有元素相加。
2. collect
- 功能: 在驱动程序中,以数组的形式返回数据集的所有元素。
- 示例:
val array = rdd.collect()
将RDD中的所有元素收集到数组中。
3. count
- 功能: 返回RDD中元素的个数。
- 示例:
val count = rdd.count()
返回RDD中元素的数量。
4. first
- 功能: 返回RDD中的第一个元素。
- 示例:
val firstElement = rdd.first()
返回RDD中的第一个元素。
5. take
- 功能: 返回一个由RDD的前n个元素组成的数组。
- 示例:
val first5Elements = rdd.take(5)
返回RDD的前5个元素组成的数组。
6. takeOrdered
- 功能: 返回该RDD排序后的前n个元素组成的数组。
- 示例:
val top3 = rdd.takeOrdered(3)
返回RDD排序后的前3个元素组成的数组。
7. aggregate
- 功能: 分区的数据通过初始值和分区内的数据进行聚合,然后再和初始值进行分区间的数据聚合,计算规则可以不同。
- 示例:
rdd.aggregate(0)(_ + _, _ + _)
计算RDD中所有元素的和。
8. fold
- 功能: aggregate的简化版操作,分区内外计算规则相同。
- 示例:
rdd.fold(0)(_ + _)
计算RDD中所有元素的和。
9. countByKey
- 功能: 统计每种key的个数,返回一个Map。
- 示例:
val keyCountMap = pairRDD.countByKey()
统计键值对RDD中每个键的数量。
10. save算子
- 功能: 将数据保存到不同格式的文件中。
- 示例:
rdd.saveAsTextFile("hdfs://path/to/save")
将RDD保存为文本文件。rdd.saveAsObjectFile("hdfs://path/to/save")
将RDD保存为序列化的对象文件。rdd.saveAsSequenceFile("hdfs://path/to/save")
将RDD保存为Hadoop序列文件。
11. foreach
- 功能: 对RDD中的每个元素应用一个函数。
(注意)RDD的方法和Scala集合对象的方法不一样集合对象的方法都是在同一个节点的内存中完成的 RDD的方法可以将计算逻辑发送到Executor(分布式节点)执行为了区分不同的处理效果,所以将RDD的方法称之为算子RDD的方法外部的操作在driver端执行,方法内的逻辑代码在executor端执行。
RDD核心特点——分区
分区的概念:
分区是Spark中对数据进行物理划分的基本单位。在RDD或DataFrame中,数据被分为多个分区,每个分区存储着数据的一部分。分区的概念是Spark的分布式计算的基础,它决定了任务的并行度,影响了任务的执行效率。
分区和执行的关系:
-
任务的并行度: 每个分区内的数据可以并行处理,因此分区的数量决定了任务的并行度。在Spark应用程序中,任务是在各个节点上的Executor上并行执行的。一个任务处理一个或多个分区的数据,多个任务可以同时执行,每个任务独立处理自己负责的分区。
-
Shuffle操作: 当涉及到Shuffle操作时,分区的设计会直接影响Shuffle的性能。Shuffle操作会将数据重新分区,并在不同节点之间进行数据传输。良好的分区设计可以减少Shuffle的数据传输量,提高性能。例如,合理的使用
repartition
操作可以改变RDD的分区数量,优化Shuffle性能。 -
数据本地性: Spark尽量在计算节点上执行数据的计算,即在数据所在的节点上进行任务的执行,减少数据的传输。良好的分区设计可以增加数据本地性,提高计算效率。
如何控制分区:
-
默认分区策略: Spark默认会根据Hadoop数据源的分片规则或者并行度参数来确定分区数。例如,通过
textFile
读取HDFS上的文件,默认情况下每个HDFS块对应一个分区。 -
手动设置分区: 在一些转换操作中,可以手动设置分区数,例如
repartition
、coalesce
等。手动设置分区数可以根据数据量和计算资源进行优化,以提高任务的并行度。
只要不经过shuffle操作,所有分区的数据都是不变的,即原来处于哪个分区的数据,在一系列转换算子之后还是原来的分区,因此groupBy不会改变分区。
RDD序列化
闭包检查
从计算的角度, 算子以外的代码都是在 Driver 端执行, 算子里面的代码都是在 Executor 端执行。那么在 scala 的函数式编程中,就会导致算子内经常会用到算子外的数据,这样就 形成了闭包的效果,如果使用的算子外的数据无法序列化,就意味着无法传值给 Executor 端执行,就会发生错误,所以需要在执行任务计算前,检测闭包内的对象是否可以进行序列 化,这个操作我们称之为闭包检测。Scala2.12 版本后闭包编译方式发生了改变
kryo序列化框架
Java 的序列化能够序列化任何的类。但是比较重(字节多),序列化后,对象的提交也 比较大。Spark 出于性能的考虑,Spark2.0 开始支持另外一种 Kryo 序列化机制。Kryo 速度 是 Serializable 的 10 倍。当 RDD 在 Shuffle 数据的时候,简单数据类型、数组和字符串类型 已经在 Spark 内部使用 Kryo 来序列化。
RDD应用实例——计算每一位学生的平均分
import org.apache.spark.{SparkConf, SparkContext}
object Spark_AverageScore {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("averageApp")
val sc = new SparkContext(sparkConf)
sc.textFile("src/main/resources/data/student_score") //读取文件
.repartition(1).map(_.split(",")) //设置分区数为1并且切片
.filter(_(1).matches("^\\d+(\\.\\d+)?$")) //过滤数据
.groupBy(_(0)) // 通过名字分组
.mapValues(l=>l.flatten.filter(_.matches("^\\d+(\\.\\d+)?$")) //扁平化过滤
.map(_.toDouble).sum/l.size) //求个人平均分
.sortBy(_._2,ascending = false) //由大到小排序
.saveAsTextFile("src/main/resources/data/average_score") //保存结果
sc.stop()
}
}
各分数文件
Math.csv:
姓名,数学
小明,90
小红,85
小刚,92
小芳,78
小李,87
小王,89
小张,93
小陈,86
English.csv:
姓名,英语
小明,88
小红,90
小刚,86
小芳,92
小李,95
小王,91
小张,89
小陈,87
Physics.csv:
姓名,物理
小明,78
小红,82
小刚,90
小芳,85
小李,88
小王,80
小张,92
小陈,89
Chemistry.csv:
姓名,化学
小明,85
小红,88
小刚,94
小芳,90
小李,84
小王,87
小张,91
小陈,93
History.csv:
姓名,历史
小明,92
小红,91
小刚,89
小芳,87
小李,93
小王,95
小张,84
小陈,88