Spark Core

一.SparkCore

Spark 是一种基于==内存==的快速、通用、可扩展的大数据分析计算引擎。
区别:

  • 1.spark处理数据是基于内存的,而MapReduce是基于磁盘处理数据的。
  • 2.Spark在处理数据时构建了DAG有向无环图,减少了shuffle和数据落地磁盘的次数

Spark 核心模块

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:7077Yarn
    --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 内部通信服务端口号:7077Standalone 模式下,Spark Master Web 端口号:8080(资源)
    ➢ Spark 历史服务器端口号:18080Hadoop 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 速度
    是 Serializable10 倍。当 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 任务切分中间分为:ApplicationJobStageTaskApplication:初始化一个 SparkContext 即生成一个 Application;
⚫ Job:一个 Action 算子就会生成一个 Job;
⚫ StageStage 等于宽依赖(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端执行,那么该副本就会有多份,如果副本是大对象,占用内存太多

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值