2.5 行动算子
行动算子是触发了整个作业的执行。因为转换算子都是懒加载,并不会立即执行。
2.5.1 reduce() 聚合
1)函数签名:def reduce(f: (T, T) => T): T
2)功能说明:f函数聚集RDD中的所有元素,先聚合分区内数据,再聚合分区间数据。
2.5.2 collect() 以数组的形式返回数据集
1)函数签名:def collect(): Array[T]
2)功能说明:在驱动程序中,以数组Array的形式返回数据集的所有元素。
2.5.3 count() 返回RDD中元素个数
1)函数签名:def count(): Long
2)功能说明:返回RDD中元素的个数
2.5.4 first() 返回RDD中的第一个元素
1)函数签名: def first(): T
2)功能说明:返回RDD中的第一个元素
2.5.5 take() 返回由RDD前n个元素组成的数组
1)函数签名: def take(num: Int): Array[T]
2)功能说明:返回一个由RDD的前n个元素组成的数组
2.5.6 takeOrdered() 返回该RDD排序后前n个元素组成的数组
1)函数签名: def takeOrdered(num: Int)(implicit ord: Ordering[T]): Array[T]
2)功能说明:返回该RDD排序后的前n个元素组成的数组
2.5.7 aggregate()
aggregate算子:
2.5.8 fold()
fold算子:
2.5.9 countByKey()统计每种key的个数
1)函数签名:def countByKey(): Map[K, Long]
2)功能说明:统计每种key的个数
2.5.10 save
1)saveAsTextFile(path)保存成Text文件
(1)函数签名
(2)功能说明:将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark将会调用toString方法,将它装换为文件中的文本。
2)saveAsSequenceFile(path) 保存成Sequencefile文件
(1)函数签名
(2)功能说明:将数据集中的元素以Hadoop Sequencefile的格式保存到指定的目录下,可以使HDFS或者其他Hadoop支持的文件系统。
注意:只有kv类型RDD有该操作,单值的没有。
3)saveAsObjectFile(path) 序列化成对象保存到文件
(1)函数签名
(2)功能说明:用于将RDD中的元素序列化成对象,存储到文件中。
2.5.11 foreach(f)遍历RDD中每一个元素
foreach算子:
代码示例:
object Spark01_action {
def main(args: Array[String]): Unit = {
//创建Spark配置文件对象
val conf: SparkConf = new SparkConf().setAppName("Spark01_action").setMaster("local[*]")
//创建SparkContext
val sc: SparkContext = new SparkContext(conf)
//创建RDD
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
//reduce
println(rdd.reduce(_ + _))
//collect
rdd.collect().foreach(println)
//foreach
rdd.foreach(println)
//count
println(rdd.count())
//first
println(rdd.first())
//take
println(rdd.take(3))
//takeOrdered
println(rdd.takeOrdered(3))
//aggregate
val rdd1: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 8)
println(rdd1.aggregate(0)(_ + _, _ + _))
//fold
println(rdd1.fold(10)(_ + _))
//countByKey
val rdd2: RDD[(Int, String)] = sc.makeRDD(List((1, "a"), (1, "a"), (1, "a"), (2, "b"), (3, "c"), (3, "c")))
println(rdd2.countByKey())
//关闭连接
sc.stop()
}
}
2.6 序列化
在实际开发中我们往往需要自己定义一些对于RDD的操作,那么此时需要注意的是,初始化工作是在Driver端进行的,而实际运行程序是在Executor端进行的,这就涉及到了跨进程通信,是需要序列化的。下面我们看几个例子:
代码示例:
object Spark02_TestSerializable {
def main(args: Array[String]): Unit = {
//创建Spark配置文件对象
val conf: SparkConf = new SparkConf().setAppName("Spark02_TestSerializable ").setMaster("local[*]")
//创建SparkContext
val sc: SparkContext = new SparkContext(conf)
//创建User对象
val user1 = new User
user1.name = "zhangsan"
val user2 = new User
user2.name = "lisi"
//创建RDD
val rdd: RDD[User] = sc.makeRDD(List(user1, user2))
rdd.foreach(println)
//关闭连接
sc.stop()
}
}
class User extends Serializable {
var name: String = _
override def toString = s"User($name)"
}
输出:
User(lisi)
User(zhangsan)
2.6.1 闭包检查
2.6.2 序列化方法和属性
Driver:算子以外的代码都是在Driver端执行
Executor:算子里面的代码都是在Executor端执行
2.6.3 Kryo序列化框架
参考地址: https://github.com/EsotericSoftware/kryo
Java的序列化能够序列化任何的类。但是比较重,序列化后对象的提交也比较大.
Spark出于性能的考虑,Spark2.0开始支持另外一种Kryo序列化机制。Kryo速度是Serializable的10倍。当RDD在Shuffle数据的时候,简单数据类型、数组和字符串类型已经在Spark内部使用kryo来序列化。
注意:即使使用kryo序列化,也要继承Serializable接口。
2.7 RDD依赖关系
2.7.1 查看血缘关系
RDD只支持粗粒度转换,即在大量记录上执行的单个操作。将创建RDD的一系列Lineage(血统)记录下来,以便恢复丢失的分区。RDD的Lineage会记录RDD的元数据信息和转换行为,当该RDD的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区。
代码示例:
object Spark04_TestLineage {
def main(args: Array[String]): Unit = {
//创建Spark配置文件对象
val conf: SparkConf = new SparkConf().setAppName("Spark01_CreateRDD_mem").setMaster("local[*]")
//创建SparkContext
val sc: SparkContext = new SparkContext(conf)
//创建RDD
val rdd: RDD[String] = sc.makeRDD(List("hello spark", "hello world"), 2)
val flatMapRDD: RDD[String] = rdd.flatMap(_.split(" "))
val mapRDD: RDD[(String, Int)] = flatMapRDD.map((_, 1))
val resRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_ + _)
//查看RDD血缘关系
println(rdd.toDebugString)
println("------------------")
println(flatMapRDD.toDebugString)
println("------------------")
println(mapRDD.toDebugString)
println("------------------")
println(resRDD.toDebugString)
//关闭连接
sc.stop()
}
}
输出
(2) ParallelCollectionRDD[0] at makeRDD at Spark04_TestLineage.scala:14 []
------------------
(2) MapPartitionsRDD[1] at flatMap at Spark04_TestLineage.scala:15 []
| ParallelCollectionRDD[0] at makeRDD at Spark04_TestLineage.scala:14 []
------------------
(2) MapPartitionsRDD[2] at map at Spark04_TestLineage.scala:16 []
| MapPartitionsRDD[1] at flatMap at Spark04_TestLineage.scala:15 []
| ParallelCollectionRDD[0] at makeRDD at Spark04_TestLineage.scala:14 []
------------------
(2) ShuffledRDD[3] at reduceByKey at Spark04_TestLineage.scala:17 []
+-(2) MapPartitionsRDD[2] at map at Spark04_TestLineage.scala:16 []
| MapPartitionsRDD[1] at flatMap at Spark04_TestLineage.scala:15 []
| ParallelCollectionRDD[0] at makeRDD at Spark04_TestLineage.scala:14 []
2.7.2 查看依赖关系![在这里插入图片描述](https://img-blog.csdnimg.cn/20210719203257576.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDY5OTY4OQ==,size_16,color_FFFFFF,t_70)
注意:要想理解RDDS是如何工作的,最重要的就是理解Transformations。
RDD 之间的关系可以从两个维度来理解: 一个是 RDD 是从哪些 RDD 转换而来, 也就是 RDD 的 parent RDD(s)是什么; 另一个就是 RDD 依赖于 parent RDD(s)的哪些 Partition(s). 这种关系就是 RDD 之间的依赖.
RDD和它依赖的父RDD(s)的关系有两种不同的类型,即窄依赖(narrow dependency)和宽依赖(wide dependency)。
2.7.3 窄依赖
窄依赖表示每一个父RDD的Partition最多被子RDD的一个Partition使用,窄依赖我们形象的比喻为独生子女。
2.7.4 宽依赖
宽依赖表示同一个父RDD的Partition被多个子RDD的Partition依赖,会引起Shuffle,总结:宽依赖我们形象的比喻为超生。
具有宽依赖的 transformations 包括: sort, reduceByKey, groupByKey, join, 和调用rePartition函数的任何操作.
宽依赖对 Spark 去评估一个 transformations 有更加重要的影响, 比如对性能的影响.
2.7.5 Spark中的Job调度
-集群(Standalone|Yarn)
一个Spark集群可以同时运行多个Spark应用
-应用
用来完成某个功能的程序
一个应用可以运行多个并发的Job
-Job
行动算子,每次执行一个行动算子,都会提交一个Job
一个Job由多个Stage组成
-Stage
一个宽依赖做一次阶段的划分
阶段的个数=宽依赖的个数+1
一个Stage由多个Task组成
-Task
每一个阶段最后一个RDD的分区数,就是当前阶段的Task的个数
一个Spark应用包含一个驱动进程(driver process,在这个进程中写Spark的逻辑代码)和多个执行器进程(executor process,跨越集群中的多个节点)。Spark 程序自己是运行在驱动节点, 然后发送指令到执行器节点。
一个Spark集群可以同时运行多个Spark应用, 这些应用是由集群管理器(cluster manager)来调度。
Spark应用可以并发的运行多个job, job对应着给定的应用内的在RDD上的每个 action操作。
1)Spark应用
一个Spark应用可以包含多个Spark job, Spark job是在驱动程序中由SparkContext 来定义的。
当启动一个 SparkContext 的时候, 就开启了一个 Spark 应用。 一个驱动程序被启动了, 多个执行器在集群中的多个工作节点(worker nodes)也被启动了。 一个执行器就是一个 JVM, 一个执行器不能跨越多个节点, 但是一个节点可以包括多个执行器。
一个 RDD 会跨多个执行器被并行计算. 每个执行器可以有这个 RDD 的多个分区, 但是一个分区不能跨越多个执行器.
2)SparkJob
由于Spark的懒执行, 在驱动程序调用一个action之前, Spark 应用不会做任何事情,
针对每个action,Spark 调度器就创建一个执行图(execution graph)和启动一个 Spark job。
每个 job 由多个stages 组成, 这些 stages 就是实现最终的 RDD 所需的数据转换的步骤。一个宽依赖划分一个stage。每个 stage 由多个 tasks 来组成, 这些 tasks 就表示每个并行计算, 并且会在多个执行器上执行。
2.7.6 Stage任务划分
1)DAG有向无环图
DAG(Directed Acyclic Graph)有向无环图是由点和线组成的拓扑图形,该图形具有方向,不会闭环。原始的RDD通过一系列的转换就形成了DAG,根据RDD之间的依赖关系的不同将DAG划分成不同的Stage,对于窄依赖,partition的转换处理在Stage中完成计算。对于宽依赖,由于有Shuffle的存在,只能在parent RDD处理完成后,才能开始接下来的计算,因此宽依赖是划分Stage的依据。例如,DAG记录了RDD的转换过程和任务的阶段。
2)Stage任务划分
3)代码示例:
object Spark05_task {
def main(args: Array[String]): Unit = {
//创建Spark配置文件对象
val conf: SparkConf = new SparkConf().setAppName("Spark05_task").setMaster("local[*]")
//创建SparkContext
val sc: SparkContext = new SparkContext(conf)
//创建RDD
val dataRDD: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4, 1, 2), 2)
//3.1聚合
val resultRDD: RDD[(Int, Int)] = dataRDD.map((_, 1)).reduceByKey(_ + _)
//Job: 一个Action算子就会生成一个Job
//Job1打印输出到控制台
resultRDD.collect().foreach(println)
//Job2输出到磁盘
resultRDD.saveAsTextFile("E:\\spark-0701\\output")
Thread.sleep(1000000)
//关闭连接
sc.stop()
}
}
localhost:4040查看
由两个Job,4个Task
Job0 Stage:
Job1 Stage(有一个阶段跳过了,之前执行额时候有缓存)
注意:如果存在shuffle过程,系统会自动进行缓存,UI界面显示skipped的部分
Job0 Stage0 Task:
Job0 Stage1 Task:
Job1 Stage2 Task:
Job1 Stage3 Task:
2.8 RDD持久化
2.8.1 RDD Cache缓存
RDD通过Cache或者Persist方法将前面的计算结果缓存,默认情况下会把数据以序列化的形式缓存在JVM的堆内存中。但是并不是这两个方法被调用时立即缓存,而是触发后面的action时,该RDD将会被缓存在计算节点的内存中,并供后面重用。
1)代码示例:
object Spark01_cache {
def main(args: Array[String]): Unit = {
//创建Spark配置文件对象
val conf: SparkConf = new SparkConf().setAppName("Spark01_cache").setMaster("local[*]")
//创建SparkContext
val sc: SparkContext = new SparkContext(conf)
//创建RDD
val rdd: RDD[String] = sc.makeRDD(List("hello spark", "hello world"), 2)
//扁平映射
val flatMapRDD: RDD[String] = rdd.flatMap(_.split(" "))
//结构转换
val mapRDD: RDD[(String, Int)] = flatMapRDD.map {
word => {
println("执行map算子")
(word, 1)
}
}
//打印血缘关系
println(mapRDD.toDebugString)
//触发行动操作
//对RDD进行缓存
mapRDD.cache()
mapRDD.collect()
println("------------------")
//打印血缘关系
println(mapRDD.toDebugString)
//触发行动操作
mapRDD.collect()
println("------------------")
//关闭连接
sc.stop()
}
}
输出:
可以看到只调用一次map,第二次需要调用map的时候是从缓存中拿到的数据。
(2) MapPartitionsRDD[2] at map at Spark01_cache.scala:21 []
| MapPartitionsRDD[1] at flatMap at Spark01_cache.scala:18 []
| ParallelCollectionRDD[0] at makeRDD at Spark01_cache.scala:15 []
执行map算子
执行map算子
执行map算子
执行map算子
------------------
(2) MapPartitionsRDD[2] at map at Spark01_cache.scala:21 [Memory Deserialized 1x Replicated]
| CachedPartitions: 2; MemorySize: 400.0 B; ExternalBlockStoreSize: 0.0 B; DiskSize: 0.0 B
| MapPartitionsRDD[1] at flatMap at Spark01_cache.scala:18 [Memory Deserialized 1x Replicated]
| ParallelCollectionRDD[0] at makeRDD at Spark01_cache.scala:15 [Memory Deserialized 1x Replicated]
2)源码:
mapRdd.cache()
def cache(): this.type = persist()
def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)
object StorageLevel {
val NONE = new StorageLevel(false, false, false, false)
val DISK_ONLY = new StorageLevel(true, false, false, false)
val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2)
val MEMORY_ONLY = new StorageLevel(false, true, false, true)
val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2)
val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false)
val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)
val MEMORY_AND_DISK = new StorageLevel(true, true, false, true)
val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2)
val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)
val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)
val OFF_HEAP = new StorageLevel(true, true, true, false, 1)
默认的存储级别都是仅在内存存储一份。在存储级别的末尾加上“_2”表示持久化的数据存为两份。
缓存有可能丢失,或者存储于内存的数据由于内存不足而被删除,RDD的缓存容错机制保证了即使缓存丢失也能保证计算的正确执行。通过基于RDD的一系列转换,丢失的数据会被重算,由于RDD的各个Partition是相对独立的,因此只需要计算丢失的部分即可,并不需要重算全部Partition。
3)自带缓存算子
Spark会自动对一些Shuffle操作的中间数据做持久化操作(比如:reduceByKey)。这样做的目的是为了当一个节点Shuffle失败了避免重新计算整个输入。但是,在实际使用的时候,如果想重用数据,仍然建议调用persist或cache。
2.8.2 RDD CheckPoint检查点
1)检查点:是通过将RDD中间结果写入磁盘。
2)为什么要做检查点?
由于血缘依赖过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果检查点之后有节点出现问题,可以从检查点开始重做血缘,减少了开销。
3)检查点存储路径:Checkpoint的数据通常是存储在HDFS等容错、高可用的文件系统
4)检查点数据存储格式为:二进制的文件
5)检查点切断血缘:在Checkpoint的过程中,该RDD的所有依赖于父RDD中的信息将全部被移除。
6)检查点触发时间:对RDD进行checkpoint操作并不会马上被执行,必须执行Action操作才能触发。但是检查点为了数据安全,会从血缘关系的最开始执行一遍
7)设置检查点步骤
(1)设置检查点数据存储路径:sc.setCheckpointDir("./checkpoint1")
(2)调用检查点方法:wordToOneRdd.checkpoint()
代码示例:
object Spark02_checkpoint {
def main(args: Array[String]): Unit = {
//创建Spark配置文件对象
val conf: SparkConf = new SparkConf().setAppName("Spark02_checkpoint").setMaster("local[*]")
//创建SparkContext
val sc: SparkContext = new SparkContext(conf)
//设置检查点目录(开发环境应该将检查点目录设置在hdfs上)
sc.setCheckpointDir("E:\\spark-0701\\output\\cp")
//创建RDD
val rdd: RDD[String] = sc.makeRDD(List("hello spark", "hello world"), 2)
//扁平映射
val flatMapRDD: RDD[String] = rdd.flatMap(_.split(" "))
//结构转换
val mapRDD: RDD[(String, Long)] = flatMapRDD.map {
word => {
(word, System.currentTimeMillis())
}
}
//打印血缘关系
println(mapRDD.toDebugString)
//在开发环境,检查点与缓存配合使用
mapRDD.cache()
//设置检查点,检查点数据从缓存中读取
mapRDD.checkpoint()
//触发行动操作
mapRDD.collect().foreach(println)
println("------------------")
//打印血缘关系
println(mapRDD.toDebugString)
//触发行动操作
mapRDD.collect().foreach(println)
println("------------------")
//关闭连接
sc.stop()
}
}
输出:
(2) MapPartitionsRDD[2] at map at Spark02_checkpoint.scala:23 []
| MapPartitionsRDD[1] at flatMap at Spark02_checkpoint.scala:20 []
| ParallelCollectionRDD[0] at makeRDD at Spark02_checkpoint.scala:17 []
(hello,1626787423762)
(spark,1626787423762)
(hello,1626787423762)
(world,1626787423763)
------------------
(2) MapPartitionsRDD[2] at map at Spark02_checkpoint.scala:23 [Memory Deserialized 1x Replicated]
| CachedPartitions: 2; MemorySize: 464.0 B; ExternalBlockStoreSize: 0.0 B; DiskSize: 0.0 B
| ReliableCheckpointRDD[3] at collect at Spark02_checkpoint.scala:39 [Memory Deserialized 1x Replicated]
(hello,1626787423762)
(spark,1626787423762)
(hello,1626787423762)
(world,1626787423763)
------------------
可以看到第二次执行的时候是读取了检查点的数据(时间戳一样)
2.8.3 缓存和检查点区别
1)Cache缓存只是将数据保存起来,不切断血缘依赖。Checkpoint检查点切断血缘依赖。
2)Cache缓存的数据通常存储在磁盘、内存等地方,可靠性低。Checkpoint的数据通常存储在HDFS等容错、高可用的文件系统,可靠性高。
3)建议对checkpoint()的RDD使用Cache缓存,这样checkpoint的job只需从Cache缓存中读取数据即可,否则需要再从头计算一次RDD。
4)如果使用完了缓存,可以通过unpersist()方法释放缓存
2.8.4 检查点存储到HDFS集群上
// 设置访问HDFS集群的用户名
System.setProperty("HADOOP_USER_NAME","atguigu")
// 需要设置路径.需要提前在HDFS集群上创建/checkpoint路径
sc.setCheckpointDir("hdfs://hadoop102:9000/checkpoint")
2.9 键值对RDD数据分区
Spark目前支持Hash分区和Range分区,和用户自定义分区。Hash分区为当前的默认分区。分区器直接决定了RDD中分区的个数、RDD中每条数据经过Shuffle后进入哪个分区和Reduce的个数。
1)注意:
(1)只有Key-Value类型的RDD才有分区器,非Key-Value类型的RDD分区的值是None
(2)每个RDD的分区ID范围:0~numPartitions-1,决定这个值是属于那个分区的。
2.9.1 Hash分区
2.9.2 Ranger分区
2.9.3 自定义分区
见[大数据]Spark(2)RDD(1)2.4.3.1