一.SparkCore
Spark 是一种基于==内存==的快速、通用、可扩展的大数据分析计算引擎。
区别:
- 1.spark处理数据是基于内存的,而MapReduce是基于磁盘处理数据的。
- 2.Spark在处理数据时构建了DAG有向无环图,减少了shuffle和数据落地磁盘的次数
Spark 核心模块
➢ Spark Core
Spark Core 中提供了 Spark 最基础与最核心的功能,Spark 其他的功能如:Spark SQL,Spark Streaming,GraphX, MLlib 都是在 Spark Core 的基础上进行扩展的
➢ Spark SQL
Spark SQL 是 Spark 用来操作结构化数据的组件。通过 Spark SQL,用户可以使用 SQL或者 Apache Hive 版本的 SQL 方言(HQL)来查询数据。
➢ Spark Streaming
Spark Streaming 是 Spark 平台上针对实时数据进行流式计算的组件,提供了丰富的处理数据流的 API。
➢ Spark MLlib
MLlib 是 Spark 提供的一个机器学习算法库。MLlib 不仅提供了模型评估、数据导入等额外的功能,还提供了一些更底层的机器学习原语。
➢ Spark GraphX
GraphX 是 Spark 面向图计算提供的框架与算法库。
二.Spark 快速上手
1.增加 Scala 插件
2.创建Maven项目添加依赖
<dependencies>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.12</artifactId>
<version>3.0.0</version>
</dependency>
</dependencies>
3.添加Scala框架支持
4.代码实现
object SparkCoreTest01 {
def main(args: Array[String]): Unit = {
//todo: 创建spark上下文
val ct = new
SparkContext(new SparkConf().setAppName("SparkWordCount").setMaster("local[*]"))
//todo: 执行spark业务
//1. 读文件
val lines: RDD[String] = ct.textFile("datas");
println(lines.collect.mkString("-"))
//2. 拆分单词
val words: RDD[String] = lines.flatMap(_.split(" "));
println(words.collect().mkString("-"))
//3. 分组
//val wordsGroup: RDD[(String, Iterable[String])] = words.groupBy(word => word)
//println(wordsGroup.collect().mkString(","))
val wordCountOne: RDD[(String, Int)] = words.map((_,1))
println(wordCountOne.collect().mkString(","))
//4. 统计
// val groups: RDD[(String, Int)] = wordsGroup.map {
// case (key, it) => {
// (key, it.size)
// }
// }
val groups: RDD[(String, Int)] = wordCountOne.reduceByKey(_+_)
println(groups.collect().mkString("-"))
//5.输出
val tuples: Array[(String, Int)] = groups.collect()
tuples.foreach(println)
//todo: 关闭spark上下文
ct.stop()
}
}
输出结果:
hello spark,hello scala,hello spark,hello scala
hello,spark,hello,scala,hello,spark,hello,scala
(hello,1),(spark,1),(hello,1),(scala,1),(hello,1),(spark,1),(hello,1),(scala,1)
(scala,2),(hello,4),(spark,2)
(scala,2)
(hello,4)
(spark,2)
三.Spark 运行环境
1.本地模式
[zhyp@node0 opt]$ tar -zxf spark-3.0.0-bin-hadoop3.2.tgz
[zhyp@node0 opt]$ mv spark-3.0.0-bin-hadoop3.2 spark-local
执行scala代码
scala> sc.textFile("data/words.txt").flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_).collect
res1: Array[(String, Int)] = Array((scala,2), (hello,4), (world,1), (spark,1))
其中data是指当前Project根目录下的data文件夹,自己创建一个words.txt文件
打开 http://node0:4040 spart的web端监控页面
提交scala代码
[zhyp@node0 spark-local]$ bin/spark-submit \ // 提交命令
--class org.apache.spark.examples.SparkPi \ //提交jar包的主类名字
--master local[2] \ //本地模式 [2] 代表两个vCore
./examples/jars/spark-examples_2.12-3.0.0.jar \ //提交的jar包位置
10 // 定义的本次job的任务数
2.Standalone独立部署模式
[zhyp@node0 spark-standalone]$ vim conf/slaves
#localhost // 注释掉 原本的localhost 这是本地模式
node0 //添加独立部署模式的 多台主机域名或者ip
node1
node2
[zhyp@node0 spark-standalone]$ vim conf/spark-env.sh
export JAVA_HOME=/opt/jdk1.8 //配置JDK
SPARK_MASTER_HOST=node0 //master主机地址
SPARK_MASTER_PORT=7077 //spark内部通信端口
[zhyp@node0 spark-standalone]$ xsync /opt/spark-standalone //同步 spark-standalone到其他机器
[zhyp@node0 spark-standalone]$ sbin/start-all.sh
[zhyp@node0 spark-standalone]$ jpsall
=============== node0 ===============
8180 Worker
8539 Jps
8093 Master
=============== node1 ===============
10355 Worker
10683 Jps
=============== node2 ===============
10835 Worker
11162 Jps
查看 Master 资源监控 Web UI 界面: http://node0:8080
测试提交job
[zhyp@node0 spark-standalone]$ bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master spark://node0:7077 \
./examples/jars/spark-examples_2.12-3.0.0.jar \
10
参数说明:
--class Spark 程序中包含主函数的类
--master Spark 程序运行的模式(环境) 模式:local[*]、spark://linux1:7077、Yarn
--executor-memory 1G 指定每个 executor 可用内存为 1G 符合集群内存配置即可,具体情况具体分析。
--total-executor-cores 2 指定所有executor使用的cpu核数为 2 个
--executor-cores 指定每个executor使用的cpu核数
application-jar 打包好的应用 jar,包含依赖。这个 URL 在集群中全局可见。
比如 hdfs:// 共享存储系统,如果是master上的文件,那么所有的节点的path 都包含同样的 jar
application-arguments 传给 main()方法的参数
配置历史服务
无论是本地模式的spark-shell还是独立部署的集群,停止之后就看不到历史任务运行情况,所以要配置历史服务器
1) 修改 spark-defaults.conf.template 文件名为 spark-defaults.conf
mv spark-defaults.conf.template spark-defaults.conf
2) 修改 spark-default.conf 文件,配置日志存储路径
spark.eventLog.enabled true
spark.eventLog.dir hdfs://node0:8020/spark-history
注意:需要启动 hadoop 集群,HDFS 上的 spark-history 目录需要提前存在。
sbin/start-dfs.sh
hadoop fs -mkdir /spark-history
3) 修改 spark-env.sh 文件, 添加日志配置
export SPARK_HISTORY_OPTS="
-Dspark.history.ui.port=18080 //历史服务器访问端口18080,集群的访问端口还是8080
-Dspark.history.fs.logDirectory=hdfs://node0:8020/spark-history
-Dspark.history.retainedApplications=30" //页面保留内存中的应用数,而不是页面上显示的应用总数
4) 分发配置文件
xsync conf
5) 重新启动集群和历史服务
sbin/start-all.sh
sbin/start-history-server.sh
6) 重新执行任务
[zhyp@node0 spark-standalone]$ bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master spark://node0:7077 \
./examples/jars/spark-examples_2.12-3.0.0.jar \
7) 查看历史服务:http://linux1:18080
配置高可用(HA)
需要Zookeeper后期下载完毕后填写
3.Yarn模式
1 解压缩文件
将 spark-3.0.0-bin-hadoop3.2.tgz 文件上传到 linux 并解压缩,放置在指定位置。
[zhyp@node0 opt]$ tar -zxvf spark-3.0.0-bin-hadoop3.2.tgz -C /opt/
[zhyp@node0 opt]$ mv spark-3.0.0-bin-hadoop3.2 spark-yarn
2 修改配置文件
1) 修改 spark-defaults.conf.template 文件名为 spark-defaults.conf
mv spark-defaults.conf.template spark-defaults.conf
2) 修改 spark-default.conf 文件,配置日志存储路径
spark.eventLog.enabled true
spark.eventLog.dir hdfs://node0:8020/spark-history //hadoop的hdfs地址
注意:需要启动 hadoop 集群,HDFS 上的目录需要提前存在。
[root@linux1 hadoop]# sbin/start-dfs.sh
[root@linux1 hadoop]# hadoop fs -mkdir /directory
3) 修改 spark-env.sh 文件, 添加日志配置
export SPARK_HISTORY_OPTS="
-Dspark.history.ui.port=18080
-Dspark.history.fs.logDirectory=hdfs://node0:8020/directory
-Dspark.history.retainedApplications=30"
4) 修改 spark-defaults.conf
spark.yarn.historyServer.address=node0:18080 //spark的历史服务器地址
spark.history.ui.port=18080
5) 启动历史服务
sbin/start-history-server.sh
6) 重新提交应用
bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master yarn \
--deploy-mode cluster \
./examples/jars/spark-examples_2.12-3.0.0.jar \
10
4.Windows 的本地模式[测试学习可以用]
1 解压缩文件
将文件 spark-3.0.0-bin-hadoop3.2.tgz 解压缩到无中文无空格的路径中
2 启动本地环境
1) 执行解压缩文件路径下 bin 目录中的 spark-shell.cmd 文件,启动 Spark 本地环境
2) 在 bin 目录中创建 input 目录,并添加 word.txt 文件, 在命令行中输入脚本代码
3 命令行提交应用
在 DOS 命令行窗口中执行提交指令
spark-submit
--class org.apache.spark.examples.SparkPi
--master local[2]
../examples/jars/spark-examples_2.12-3.0.0.jar
10
端口号
➢ Spark 查看当前 Spark-shell 运行任务情况端口号:4040(计算)
➢ Spark Master 内部通信服务端口号:7077
➢ Standalone 模式下,Spark Master Web 端口号:8080(资源)
➢ Spark 历史服务器端口号:18080
➢ Hadoop YARN 任务运行情况查看端口号:8088
➢ HDFS页面端口号:9876
四.Spark运行架构
五.Spark核心编程
Spark 计算框架为了能够进行高并发和高吞吐的数据处理,封装了三大数据结构,
用于处理不同的应用场景。三大数据结构分别是:
➢ RDD : 弹性分布式数据集
➢ 累加器:分布式共享只写变量
➢ 广播变量:分布式共享只读变量
5.1 RDD
什么是RDD
RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,
是 Spark 中最基本的数据处理模型
RDD代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行计算的集合。
➢ 弹性
⚫ 存储的弹性:内存与磁盘的自动切换;
⚫ 容错的弹性:数据丢失可以自动恢复;
⚫ 计算的弹性:计算出错重试机制;
⚫ 分片的弹性:可根据需要重新分片;
➢ 分布式:数据存储在大数据集群不同节点上
➢ 数据集:RDD 封装了计算逻辑,并不保存数据
➢ 数据抽象:RDD 是一个抽象类,需要子类具体实现
➢ 不可变:RDD 封装了计算逻辑,是不可以改变的,想要改变,
只能产生新的 RDD,在新的 RDD 里面封装计算逻辑
➢ 可分区、并行计算
核心属性
RDD的五大属性,Internally, each RDD is characterized by five
- A list of partitions
分区列表,每个RDD内部有一个分区列表,数据存在分区中,以便于并行计算
- A function for computing each split
分区计算函数,每个RDD内部封装的计算逻辑,用于计算分区内的数据一般来说同一个RDD数据不同,但是计算逻辑相同
- A list of dependencies on other RDDs
RDD是计算模型的封装, 该属性代表当前RDD所依赖的其他RDDs
- Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
//可选属性
分区器, 如果RDD是KV数据类型时,可设置分区器自定义数据的分区
- Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file)
//可选属性
首选位置, 根据当前DRR的状态 与 结算节点的状态, 选择不同的节点进行逻辑计算
### RDD的创建
- 从集合(内存)中创建RDD
//1) 从集合(内存)中创建
RDDval rdd1: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4, 5))
//2) 从外部存储(文件)创建
RDDval rdd2: RDD[String] = sc.textFile("datas")
//3) 从其他 RDD 创建从其他 RDD
创建(以后讲解)
//4.直接创建 RDD(new)
使用 new 的方式直接构造 RDD,一般由 Spark 框架自身使用
RDD的并行度和分区
- 并行度: 同时执行的Task数量
- 分区: RDD内部将数据分成几部分
在创建RDD是可以指定RDD的分区数量
//makeRdd不指定分区数.默认值为scheduler.conf.getInt("spark.default.parallelism", totalCores)
//spark.default.parallelism指定的个数 如果没有默认是CPU的核数
val rdd1: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4, 5),2) //2个分区
可以通过 rdd1.saveAsTextFile("output1") 观察output1内的文件个数
集合创建RDD的分区数据划分:
def positions(length: Long, numSlices: Int): Iterator[(Int, Int)] = {
(0 until numSlices).iterator.map { i =>
val start = ((i * length) / numSlices).toInt
val end = (((i + 1) * length) / numSlices).toInt
(start, end)
}}
划分规则:
0 - 元素个数*1/分区数
元素个数*1/分区数 - 元素个数*2/分区数
比如:
1 2 3 4 5 6 7 8分区数为3,那么
第一个分区: 0 - 8/3 ==> 0 - 2
第二个分区: 2 - 16/3 ==> 2 - 5
第三个分区: 5 - 24/3 ==> 5 - 8
索引范围: [0,2) [2,5) [5,8) 这三个索引范围数据(前闭后开)
分区数据: 12 345 567
//文件创建RDD,如果没有指定分区数,默认最多2个,
//defaultParallelism就是scheduler.conf.getInt("spark.default.parallelism", totalCores)
//def defaultMinPartitions: Int = math.min(defaultParallelism, 2)
val rdd2: RDD[String] = sc.textFile("datas", 2) //2个分区
rdd2.saveAsTextFile("output2");
文件创建RDD的分区数据划分:
1.先按照Hadoop的TextFileInputFormat规则分区
目标大小 = 文件总大小/指定的分区数 ... 剩余字节数
判断 剩余字节数 <= 目标大小*1.1 则 分区数就是指定的分区数
剩余字节数 > 目标大小*1.1 则继续除以目标大小分区
比如: 文件总大小 10 指定分区 3 则最后分区数 10/3=3...1 则余数单独1个分区 ===> 4
文件总大小 17 指定分区 6 则最后分区数 17/6=2...5 则余数单独3个分区 ===> 9
2.按照偏移量(索引)读取数据
分区0 ==> [0,目标大小]
分区1 ==> [目标大小,目标大小*2]
分区2 ==> [目标大小*2,目标大小*3] ...
3.读取数据时采用的是hadoop的方式读取,所以一行一行读取,只要有一个字节在分区索引范围内,那么一行数据都会被读取,并且读取后下一个索引范围不会再读取该行数据(即数据不会重复)
第一行 1\r\n
第二行 2\r\n
第三行 3
7个字节指定2分区,结果3分区, 那么索引范围
第一个区: [0,3] 1\r\n2 但是需要读取整行==> 1\r\n2\r\n
第二个区: [3,6] 2\r\n3 由于2\r\n已经取出过 那么只剩==>3
第三个区: [6,9] 空
4.如果读取的是文件夹,即有多个文件
那么 目标大小 = 文件总大小/指定的分区数
然后每个文件分别计算该文件所需的分区数
(即Hadoop的文件切片规则,只不过hadoop默认是128M,这里是模板大小)
1.txt 1字节
2.txt 1字节
3.txt 10字节
指定分区数为5 那么目标大小 = 12 / 5 = 2字节/分区
文件1 单独一个分区
文件2 单独一个分区
文件3 5个分区
一共7个区分 而不是一起算的12/2=6个分区
注意: 每个文件根据自己的分区数读取数据,同样也按整行读取
5.思考与检测
1.txt ===> 12345
2.txt ===> 7\r\n1
3.txt ===> hello
字节数 5 + 4 + 5 = 14字节
执行代码:
val rdd2: RDD[String] = sc.textFile("datas",5)
rdd2.saveAsTextFile("output2")
结果为8个分区,
RDD的转换算子
- RDD 根据数据处理方式的不同将算子整体上分为
Value 类型、双 Value 类型和 Key-Value类型
Value 类型
val sc = new SparkContext(new SparkConf().setMaster("local[3]").setAppName("Operator"))
val rdd1: RDD[Int] = sc.makeRDD(List(10,20,30,40,50,60))
1. def map [ U ] ( f : T => U ) : RDD [ U ] //可以映射成不同类型的元素
val rdd2: RDD[Int] = rdd1.map(_ * 2)
2.def mapPartitions [ U ] ( //一次传入一个分区的所有数据 映射成另外一个集合,元素可以减少,也可以分区求和
f : Iterator [ T ] => Iterator [ U ] ,
preservesPartitioning: Boolean = false ) : RDD [ U ]
val rdd3: RDD[Int] = rdd1.mapPartitions(it => List(it.max).iterator)
3. def mapPartitionsWithIndex [ U ] ( //带有分区索引的mapPartitions
f : ( Int , Iterator [ T ] ) => Iterator [ U ] ,
preservesPartitioning: Boolean = false ) : RDD [ U ]
val rdd4: RDD[(Int, Int)] = rdd1.mapPartitionsWithIndex((idx,it)=>List((idx,it.max)).iterator)
4.def flatMap [ U ] ( f : T => TraversableOnce [ U ] ) : RDD [ U ] //把元素映射成集合,最后扁平化
val rdd5: RDD[Int] = rdd1.flatMap(num => List(num,num+1))
5.def glom( ) : RDD [ Array [ T ] ] //与扁平化相反,不同的是每个分区变成一个数组,分区数不变
val rdd6: RDD[Array[Int]] = rdd1.glom()
6.def groupBy [ K ] ( f: T => K ) ( implicit kt : ClassTag [ K ] ) : RDD [ ( K , Iterator [ T ] ) ] //键映射分组
val rdd7: RDD[(Int, Iterable[Int])] = rdd1.groupBy(_ % 20)
7.def filter ( f : T => Boolean ) : RDD [ T ] //过滤,过滤之后可能会数据倾斜
val rdd8: RDD[Int] = rdd1.filter(_ % 20 == 0)
8.def sample [ ] ( replacement : Boolean , fraction : Double , 随机数种子 ) : RDD [ T ]
val rdd9: RDD[Int] = rdd1.sample(false,0.5) //不放回抽取, 0.5表示每个元素抽取的概率
val rdd10: RDD[Int] = rdd1.sample(true,3) //放回收取 3表示每个元素期望抽取的次数
9.def distinct ( ) ( implicit ord : Ordering [ T ] = null ) : RDD [ T ] //所有数据去重
def distinct ( numPartitions : Int ) ( implicit ord : Ordering [ T ] = null ) : RDD [ T ]
val rdd11: RDD[Int] = rdd1.distinct(3) //去重后重新分区,使用HashPartitioner(3)进行重新分区
10.def coalesce (partitions : Int ,
shuffle : Boolean = false ,
partitionCoalesce : Option [ partitionCoallesce ] = Option.empty ) : RDD [ T ]
val rdd12: RDD[Int] = rdd1.coalesce(2) //缩减分区, 第二个参数默认false不shuffle,所以只能缩减 不能扩大分区数
//如果第二个参数为true,代表shuffle,那么此时也可以扩大分区数
11.def repartition( newPartitions : Int ) : RDD [ T ] //调用coalesce,shuffler为true,扩大或者缩减分区
val rdd13: RDD[Int] = rdd1.repartition(6)
12.def sortBy [ K ] ( // 元素映射后的值进行排序,第二个参数true,默认升序,第三个参数为重新分区
f : T => K ,
asc : Boolean = true ,
numPartitions : Int = this.partitions.length ) : RDD [ T ]
val rdd14: RDD[Int] = rdd1.sortBy(num => num % 3)
sc.stop();
双 Value 类型
1.def intersection ( other : RDD [ T ] ) : RDD [ T ]
2.def union ( other : RDD [ T ] ) : RDD [ T ]
3.def subtract ( other : RDD [ T ] ) : RDD [ T ]
4.def zip [ U ] ( other : RDD [ U ] ) : RDD [ ( T , U ) ] //拉链,必须保证分区个数一致,且每个分区元素个数一致
(Key,Value)类型
def main(args: Array[String]): Unit = {
val sc = new SparkContext(new SparkConf().setMaster("local[2]").setAppName("Operator"))
val rdd1: RDD[(Int, String)] = sc.makeRDD(List((2, "b"), (3, "d"), (1, "a"), (1, "c")))
//1.def partitionBy ( partitioner : Partitioner ) : RDD [ ( K , V ) ] //根据分区器以key分区
rdd1.partitionBy(new HashPartitioner(2)).glom().foreach(arr => println(arr.mkString(",")))
println("-----------------")
rdd1.partitionBy(new Partitioner() {
override def numPartitions = 2
override def getPartition(key: Any) = {
key.hashCode() % numPartitions
}
}).glom().foreach(arr => println(arr.mkString(",")))
//2.def reduceByKey ( f : ( V , V ) => V ) : RDD [ ( K , V ) ] //无初值的区内区间相同聚合
//def reduceByKey ( f : ( V , V ) => V , numPartitons : Int ) : RDD [ ( K , V ) ]
rdd1.reduceByKey(_ + _).collect().foreach(println)
//3.def groupbyKey( ) : RDD [ ( K , Iterable[ V ] ) ]
//def groupbyKey( numPartitions : Int ) : RDD [ ( K , Iterable[ V ] ) ]
//def groupbyKey( partitioner : Partitioner ) : RDD [ ( K , Iterable[ V ] ) ]
rdd1.groupByKey().collect().foreach(println)
//4.def aggregateByKey ( zeroValue : U ) ( seqOp : ( U ,V ) => U ) , //有初值的区内区间不同聚合
//combOp : ( U , U) => U ) : RDD [ ( K , U ) ]
rdd1.aggregateByKey("~")(_ + _, _ + _).collect().foreach(println)
//5.def foldByKey ( zeroValue : V ) ( f : ( V , V ) => V ) : RDD [ ( K , V ) ] //有初值的区内区间相同聚合
rdd1.foldByKey("start:")(_ + _).foreach(println)
//6.def combineByKey [ C ] ( create : V => C , //无初值有映射的区内区间不同聚合
// seqOP : ( C , V ) => C ,
// combOp : ( C , C ) => C ) : RDD [ ( K , C ) ]
val rdd2: RDD[(Int, String)] = rdd1.combineByKey(s => s + 1, _ + _, _ + _)
rdd2.collect().foreach(println)
//7.def sortByKey ( asc : Boolean = true , numPartitions : Int = self.partitons.length ) : RDD [ T ]
rdd1.sortByKey().collect().foreach(println) //对所有数据按照key排序
//8.def join [ W ] ( orther : RDD [ ( K , W ) ] ) : RDD [ ( K, ( V , W ) ) ] // 内连接 加key相同的过滤条件
val rdd3: RDD[(Int, String)] = sc.makeRDD(List((2, "b"), (3, "d"), (1, "a"), (1, "c")))
val rdd4: RDD[(Int, Char)] = sc.makeRDD(List((2, 'A'), (3, 'B'), (1, 'C'), (1, 'D')))
rdd3.join(rdd4).collect().foreach(println)
//9.def leftOutJoin [ W ] ( other : RDD [ ( K , W ) ] ) : RDD [ ( K , ( V , Option( W ) ) ) ]
//10.def cogroup [ W ] ( other : RDD [ ( K , W ) ] ) : RDD [ ( K , ( Iterable[ T ] , Iterable [ W ] ) ) ]
rdd3.cogroup(rdd4).collect().foreach(println) //先执行每个RDD的groupByKey 然后在以key分组
sc.stop();
}
RDD的行动算子
1.def reduce ( f : ( T ,T ) => T ) : T
2.def collect ( ) : Array [ T ]
3.def count ( ) : Long
4.def first ( ) : T
5.def take ( nums : Int ) : Array [ T ]
6.def takeOrdered ( nums : Int ) (implicit ord : Ordering [ T ] ) : Array [ T ]
//这里的aggregate和aggregateByKey不同, 不再在于
//初始值不仅在每个分区内参与计算一次,而且分区间计算也会参与一次
7.def aggregate ( zeroValue : U ) ( seqOp : ( U , T ) => U , conbOp : ( U , U ) => U ) : U
8.def fold ( zeroValue : T ) ( f : ( T , T ) => T ) : T
//countByKey 该算子是KV类型的才有, 统计RDD中该键出现的次数, 注意: (a,1)(a,2)(a,3) 最后a出现的次数是3次
9.def countByKey ( ) : Map [ T , Long ]
//countByValue 该算子是所有的RDD都有, 统计元素出现的次数,这里的元素是一个整体
//比如: sc.makeRDD(List((a,1),(b,1),(a,2))) ===> (a,1) 出现1次 (b,1) 出现1次 (a,2)出现一次
def countByValue ( ) : Map [ T , Long ]
10.def savaAsTextFile ( path : String ) : Unit
def saveAsObjectFile( path : String ) : Unit
def saveAsSequenceFile( path : String , codec : Option [ Class [_ <: CompressionCodec ] ] ) : Unit
11.def foreach ( f : T => Unit ) : Unit
//遍历算子,注意这个是算子,而不是collect后的集合遍历
//所以 foreach打印数据时 不同分区数据是并行的
RDD的序列化
RDD的算子都是在Executor端执行的, 算子以外的代码是在Driver端执行的
在Scala函数式编程中,经常会使用匿名函数,Lambda表达式之类的, 会用到算子以外的属性和对象
如果这些属性和对象无法序列化,那么就无法发送给Executor,就会发生错误
闭包检测: 当算子内的方法中 使用到了算子外的数据时,会检查所用的数据是否可以被序列化
Kryo 序列化框架
Java 的序列化能够序列化任何的类。但是比较重(字节多),序列化后,对象的提交也
比较大。Spark 出于性能的考虑,Spark2.0 开始支持另外一种 Kryo 序列化机制。Kryo 速度
是 Serializable 的 10 倍。当 RDD 在 Shuffle 数据的时候,简单数据类型、数组和字符串类型
已经在 Spark 内部使用 Kryo 来序列化。
val conf: SparkConf = new SparkConf()
.setAppName("SerDemo")
.setMaster("local[*]")
// 替换默认的序列化机制
.set("spark.serializer",
"org.apache.spark.serializer.KryoSerializer")
// 注册需要使用 kryo 序列化的自定义类
.registerKryoClasses(Array(classOf[Searcher]))
RDD 依赖关系
1.RDD的血缘关系
Spark的RDD的转换是连续的, 比如:
file ---读取---> rdd1 ---map---> rdd2 ---filter---> rdd3 ---reduce---> rdd4 ---map---> rdd5
每个RDD会记录在它之前的一系列的RDD以及转换算子, 这就是RDD的血统(血缘关系)
2.RDD的依赖关系
两个相邻的RDD之间的关系 我们就称为依赖关系
RDD3 ---经过map算子---> RDD4 那么我们就称为RDD4依赖于RDD3
窄依赖(NarrowDependency):
上一个RDD中每个Partition的数据, 在下一个RDD中最多被一个Parttion使用
比如: map算子 , filter算子 , flatMap算子 , coalesce算子 (缩减分区,但是不会shuffle)
宽依赖(ShuffleDependency) :
上一个RDD中每个Partition的数据, 在下一个RDD中可能被多个Parttion使用,会引起shuffle
比如: coalesce的第二个参数为true , repartition , reduceByKey , reduce等
3.RDD 阶段划分
- 一个Job中可能包含一系列的RDD算子, 我们把它们分为多个阶段
- 阶段的划分标记是 两个RDD为宽依赖, 因为宽依赖会有shuffle操作,需要等待前面的RDD数据出来完毕才能继续
- 如果tupleRDD到reduceRDD是宽依赖, 那么此处就是阶段的一个划分位置
4.RDD 任务划分
RDD 任务切分中间分为:Application、Job、Stage 和 Task
⚫ Application:初始化一个 SparkContext 即生成一个 Application;
⚫ Job:一个 Action 算子就会生成一个 Job;
⚫ Stage:Stage 等于宽依赖(ShuffleDependency)的个数加 1;
⚫ Task:一个 Stage 阶段中,最后一个 RDD 的分区个数就是 Task 的个数。
为什么:
1. 为什么Stage = ShuffleDependency+1?
如上图,最后一个strRDD 后肯定需要一个Action算子(只是图中没有画出来) , 那么一个宽依赖把整个流程分为两部分,前一个我们称为 ShuffleStage 后面没有shuffle也必须是一个阶段,我们称为ResultStage
2. 为什么划分Stage? 主要是设计合理的并行度
一个复杂的业务逻辑如果有shuffle,那么就意味着前面阶段产生结果后,才能执行下一个阶段,
即下一个阶段的计算要依赖上一个阶段的数据。那么我们按照shuffle进行划分(也就是按照宽依赖就行划分),
就可以将一个DAG划分成多个Stage/阶段,对于不同的Stage有不同的分区,进而有不同的Task数量,即并行度
3. Application = (1~n) Job = (1~n)(1~n)Stage
最后的task数量(并行度) 取决于 当前stage的最后一个RDD的分区数量
4.关于Stage源码
当subsubmitJob时, 无论是否有ShuffleDependency都会创建一个ResultStage,
在创建ResultStage之前,获取所有的ShuffleDependency,有几个就会创建几个ShuffleMapStage
RDD 持久化
RDD 缓存
1.RDD Cache 缓存
RDD 通过 cache 或者 persist 方法将该RDD的计算结果缓存,
默认情况下会把数据以缓存在 JVM 的堆内存中
2.RDD的cache方法,底层调用的就是persist方法,而persist默认的存储等级为内存,所以都成为缓存方法
/**
* Persist this RDD with the default storage level (`MEMORY_ONLY`).
*/
def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)
/**
* Persist this RDD with the default storage level (`MEMORY_ONLY`).
*/
def cache(): this.type = persist()
3.RDD的persist方法存储级别我们也可以自己调用时传入
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)
RDD的checkpoint
所谓的检查点其实就是将 RDD 计算后的结果落盘
那么RDD的checkpoint方法和RDD的persist方法的异同:
1. 必须都要调用Action算子 才会执行
2. cache方法不需要重新执行所有RDD,而是仅仅缓存当前RDD的计算结果
checkpoint方法,会重跑一个job执行所有RDD,把结果落盘
所以我们一般会
rdd.cache() //在调用checkpoint之前,缓存一下,避免重新跑一个job去执行checkpoint
rdd.checkpoint()
3.Cache 缓存只是将数据保存起来,相当于中间加了一个CacheRDD,不切断血缘依赖。
Checkpoint 检查点切断血缘依赖,相当于替换掉数据源, 使用当前这个checkpoint作为新数据源
4.Cache 缓存的数据通常存储在磁盘、内存等地方,可靠性低。Checkpoint 的数据通常存
储在 HDFS 等容错、高可用的文件系统,可靠性高。
RDD 分区器
pairRdd.groupbykey(new 分区器())
1.Spark中有创建好的Partitioner
HashPartitioner 哈希分区器,根据元素的键的哈希值对分区数取模,确定元素的分区
RangePartitioner 范围分区器, 对不同分区中的数据平均取样,排序后确定范围,根据范围确定分区边界
2.自定义分区器
public class MyPartioner extends Partitioner {
@Override
public int numPartitions() {
return 1000;
}
@Override
public int getPartition(Object key) {
String k = (String) key;
int code = k.hashCode() % 1000;
System.out.println(k+":"+code);
return code < 0?code+1000:code;
}
@Override
public boolean equals(Object obj) {
if(obj instanceof MyPartioner){
if(this.numPartitions()==((MyPartioner) obj).numPartitions()){
return true;
}
return false;
}
return super.equals(obj);
}
}
RDD 文件读取与保存
➢ text 文件
val inputRDD: RDD[String] = sc.textFile("input/1.txt")
inputRDD.saveAsTextFile("output")
➢ sequence 文件
//SequenceFile 文件必须是key-value对的类型
dataRDD.saveAsSequenceFile("output")
sc.sequenceFile[Int,Int]("output").collect().foreach(println)
//注意泛型是[ Int , Int ] 而不是 [ ( Int , Int ) ]
➢ object 对象文件
对象文件是将对象序列化后保存的文件,采用 Java 的序列化机制。
dataRDD.saveAsObjectFile("output")
sc.objectFile[Int]("output").collect().foreach(println)
5.2 累加器
小案例
def main(args: Array[String]): Unit = {
val sc = new SparkContext(new SparkConf().setMaster("local[*]").setAppName("Operator"))
val rdd1: RDD[Int] = sc.makeRDD(List(1,2,3,4))
var sum= 0
rdd1.foreach(num=>{
sum +=num
})
println(sum)
sc.stop()
}
以上的运行结果是0 ,因为foreach是行动算子,会发送到不同的Executor执行,出现闭包并且sum会分别拷贝副本到Executor中,所以操作的是Executor中的副本变量,Driver本地的sum并没有改变
累加器
引入
- 思考
能不能有一种变量, 既可以发送到不同Executor执行,而执行后又可以返回给Driver进行结果合并???
这就是我们说的累加器
更准确的定义:
累加器用来把 Executor 端变量信息聚合到 Driver 端。
在 Driver 程序中定义的变量,在Executor 端的每个 Task 都会得到这个变量的一份新的副本,每个 task 更新这些副本的值后,传回 Driver 端进行 merge。
def main(args: Array[String]): Unit = {
val sc = new SparkContext(new SparkConf().setMaster("local[*]").setAppName("Operator"))
val rdd1: RDD[Int] = sc.makeRDD(List(1,2,3,4))
//1.创建一个累加器,通过sc就可以创建出来
val sum: LongAccumulator = sc.longAccumulator("sumAcc")
//val sum1: DoubleAccumulator = sc.doubleAccumulator 小数类型的累加器
//val sum2: CollectionAccumulator[Int] = sc.collectionAccumulator[Int](" ") List集合类型的累加器
rdd1.foreach(num=>{
println(num)
sum.add(num)
})
println(sum.value)
sc.stop()
}
自定义累加器
def main(args: Array[String]): Unit = {
val sc = new SparkContext(new SparkConf().setMaster("local[*]").setAppName("Operator"))
val rdd1: RDD[String] = sc.makeRDD(List("Hello", "Spark", "Hello", "Hive"))
val rdd2: RDD[(String, Int)] = rdd1.map((_, 1))
//自定义累加器:
//1.自定义类 继承 抽象类AccumulatorV2 并实现方法
//2.创建累加器对象,并注册到sc中
val wordAcc = new WordCountAccumulator
sc.register(wordAcc, "wordAcc")
rdd2.foreach({
case (word, count) => {
wordAcc.add(word)
}
})
println(wordAcc.value)
sc.stop()
}
class WordCountAccumulator extends AccumulatorV2[String, mutable.Map[String, Long]] {
var map: mutable.Map[String, Long] = mutable.Map[String, Long]() //保存数据的map
override def isZero: Boolean = map.isEmpty //判断是否为初始状态
//可以复制当前的WordCountAccumulate对象,可以是深复制,把map数据也填充进去,这里就不写了
override def copy(): AccumulatorV2[String, mutable.Map[String, Long]] = new WordCountAccumulator
override def reset(): Unit = map.clear //清空map,重置累加器
override def add(word: String): Unit = {
map.update(word, map.getOrElse(word, 0L) + 1) //添加元素进来的逻辑
}
override def merge(other: AccumulatorV2[String, mutable.Map[String, Long]]): Unit = { //Driver端合并
other.value.foreach({
case (word, cnt) => {
map.update(word, map.getOrElse(word, 0L) + cnt)
}
})
}
override def value: mutable.Map[String, Long] = map //获取累计器的数据
}
}
5.3 广播变量
广播变量主要用于Executor中多个Task共享大对象
我们知道 当算子的闭包中使用到外部数据时,外部数据会生产一个副本,随着Task发送到Executor端执行
如果多个Task发送到同一个Executor端执行,那么该副本就会有多份,如果副本是大对象,占用内存太多