SparkCore 总结

一. 架构

Spark 框架的核心是一个计算引擎,整体来说,它采用了标准 master-slave 的结构。如下图所示,它展示了一个 Spark 执行时的基本结构。图形中的Driver 表示 master,负责管理整个集群中的作业任务调度。
图形中的Executor 则是 slave,负责实际执行任务。
在这里插入图片描述

Driver 进程

Spark 驱动器节点,用于执行Spark任务中的main方法,负责实际代码的执行工作

Driver在Spark作业中执行时主要负责:
1.当遇到action行动算子,将用户程序转化为job任务(即数据和逻辑的准备)(一个Job里面可以有多个Task)
2.在Executor之间调度任务(task)
3.通过心跳机制,跟踪Executor的执行情况
4.通过UI展示查询运行情况

实际上,我们无法准确地描述Driver 的定义,因为在整个的编程过程中没有看到任何有关Driver 的字眼。所以简单理解,所谓的 Driver 就是驱使整个应用运行起来的程序,也称之为Driver 类

Executor 进程

Executor节点是一个JVM 进程,每个executor持有一个线程池,每个线程可以执行一个task任务,executor执行完task以后将结果返回给driver ;

Executor 功能:(类似MapTask、ReduceTask)
1.线程池中的线程负责运行Task任务,并将结果返回给Driver进程;
2.通过自身块管理器为用户程序中要求缓存的RDD提供内存式存储RDD是直接缓存在Executor进程内的,因此任务可以在运行时充分利用缓存数据的加速运算。

Master & Worker (独立部署时)

Master和Worker是跟资源相关的组件!
Master类似Yarn中的ResourceManager (进程),
Worker类似nodeManager(进程),
Spark 集群的【Standalone 独立部署】环境中,不需要依赖其他的资源调度框架,自身就实现了资源调度的功能,所以环境中还有其他两个核心组件:Master 和 Worker;
这里的 Master 是一个进程,主要负责资源的调度和分配,并进行集群的监控等职责,类似于 Yarn 环境中的 ResourceManager;
Worker 也是进程,一个 Worker 运行在集群中的一台服务器上,由 Master 分配资源对数据进行并行的处理和计算,类似于 Yarn 环境中 NodeManager。

ApplicationMaster

如果Driver直接和ResourceManager交互,那么耦合性强,不推荐 !
所以在ResourceManager和Driver中加一层ApplicationMaster;当Driver要申请资源,就委托ApplicationMaster,ApplicationMaster再向Master 申请资源;

Hadoop 用户向 YARN 集群提交应用程序时,提交程序中应该包含ApplicationMaster,用于向资源调度器申请执行任务的资源容器 Container,运行用户自己的程序任务 job,监控整个任务的执行,跟踪整个任务的状态,处理任务失败等异常情况。
说的简单点就是,ResourceManager(资源)和Driver(计算)之间的解耦合靠的就是ApplicationMaster。

核心概念

并行度

默认情况下,Spark 可以将一个job作业切分多个task任务后,发送给 Executor 节点并行计算,而能够并行计算的任务数量我们称之为并行度。这个数量可以在构建RDD 时指定。

分区和并行度有关系:先有分区,分区对应task,task给Executor执行,有几个Executor的Core核 在执行则有多少并行度

每个Executor由若干core组成,每个Executor的每个core一次只能执行一个Task;

Task被执行的并发度 = Executor数目 * 每个Executor的Core核数

但是分区≠并行度: 如果Executor数量 < task数量,如单核只有一个Executor,有三个task则只能并发执行,一次只有一个task在执行,即并行度 < 分区数

一个 inputSplit 对应 RDD 的一个 partition
RDD 的一个 partition 对应一个 task,也就是说 一个 inputSplit 对应一个 task;
通常情况下 一个 block 对应一个 inputSplit;

如何去提高并行度?

  1. task数量
    new SparkConf().set(“spark.defalut.parallelism”,”“500)
    该参数用于设置每个stage的默认task数量。这个参数极为重要,如果不设置可能会直接影响你的Spark作业性能。

    官方推荐,task数量,设置成 num-executors * executor-cores 的2-3倍 ,比如150个cpu core ,基本设置 task数量为 300~ 500. 与理性情况不同的,有些task 会运行快一点,比如50s 就完了,有些task 可能会慢一点,要一分半才运行完,所以如果你的task数量,刚好设置的跟cpu core 数量相同,可能会导致资源的浪费,因为 比如150task ,10个先运行完了,剩余140个还在运行,但是这个时候,就有10个cpu core空闲出来了,导致浪费。如果设置2~3倍,那么一个task运行完以后,另外一个task马上补上来,尽量让cpu core不要空闲。同时尽量提升spark运行效率和速度。提升性能。

有向无环图

第三代计算引擎的特点主要是 Job 内部的 DAG 支持(不跨越 Job),以及实时计算。这里所谓的有向无环图,并不是真正意义的图形,而是由 Spark 程序直接映射成的数据流的高级抽象模型。简单理解就是将整个程序计算的执行过程用图形表示出来,这样更直观, 更便于理解,可以用于表示程序的拓扑结构。

DAG有向无环图是由点和线组成的拓扑图形,该图形具有方向,不会闭环。

二. 运行流程

2.1 task任务提交大致流程

在这里插入图片描述

要想执行计算任务:

  1. 启动Yarn集群;
  2. 申请资源:准备内存、cpu ,创建Driver、Executor
  3. 计算准备:需要将计算的任务task(数据和逻辑)准备好
  4. 然后将task任务发送给资源进行计算

2.2 Yarn模式 ※

  1. 在 YARN Cluster 模式下,Spark执行bin/spark-submit 提交任务会启动一个SparkSubmit 进程;

  2. SparkSubmit 进程反射调用YarnClusterApplication 的main方法,YarnClusterApplication 就会创建Yarn客户端,让ResourceManager启动ApplicationMaster

  3. 随后ResourceManager 分配 container,在【合适的 NodeManager 上】启动 ApplicationMaster

  4. ApplicationMaster开启Driver线程;

  5. ApplicationMasterResourceManager 申请Executor 内存,
    ResourceManager 会分配container,然后【在合适的NodeManager 上】启动Executor 进程

  6. Executor 进程启动后会向Driver 反向注册,【Executor 全部注册完成后】, Driver 开始执行main 函数,遇到action行动算子触发一个Job,将Job交给DAGScheduler;

  7. DAGScheduler:首先构建Job中RDD依赖对应DAG有向无环图,根据宽依赖划分为Stage阶段,并将每个 Stage 序列化打包成TaskSet 交给 TaskScheduler

  8. TaskScheduler会将TaskSet封装为 TaskSet Manager,放到rootPool调度池中,然后根据调度策略决定发送哪些Task到Executor去执行;

    TaskSetManager 负责监控管理同一个 Stage 中的 Task,TaskScheduler 就是以TaskSetManager 为单元来调度任务;

    shuffle阶段根据stage调度进行shuffle write和shuffleread。
    只有前面的stage的task任务完成,才能执行后面的stage中task任务;

shuffle为什么会落盘?

因为shuffle是将【不同分区的】数据都进行打散,而不同分区(对应task)是并行计算的,不可能同时执行完,而在数据聚合时,不可能在内存中等待数据,如果在内存中等待再进行聚合数据的话内存不够用;
所以应该在文件中等待,则需要分区数据都执行完毕才能往下执行,这样就把程序分成多个Stage阶段,前面的阶段没有执行完,后面的不能执行;
所以shuffle会直接影响Job执行的性能;

三. 核心编程

三大数据结构
RDD : 弹性 分布式 数据集
累加器:分布式共享只写变量,数值计算的过程中使用 (一般在行动算子使用)
广播变量:分布式共享只读变量,多个任务之间分享数据

3.1 RDD

定义:是 Spark 中最基本的数据处理模型,是封装了计算逻辑的计算模型,并不保存数据
是代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行计算的集合;

RDD是最小计算单元,即计算逻辑;

RDD切片分解为多个task,发送给不同的excecutor去执行;

3.1.1 特点

弹性
· 存储的弹性:内存与磁盘的自动切换
· 容错的弹性:数据丢失可以自动恢复
· 计算的弹性:计算出错重试机制
· 分片的弹性:可根据需要重新分片(可以根据需要来改变分区个数 )

分布式:数据存储在大数据集群不同节点上
· 数据集:RDD 封装了计算逻辑,并不保存数据
· 数据抽象:RDD 是一个抽象类,需要子类具体实现,子类的功能更丰富更完整
· 不可变:RDD 封装的计算逻辑是只读的,不可变的想要改变,只能产生新的RDD,在新的RDD 里面封装计算逻辑

· 可分区、并行计算;

在这里插入图片描述

实际中会有多个RDD,每一个RDD都是一个计算单元,多个RDD组合组合在一起就能形成复杂的逻辑,随着task传给excecutor去执行就可以完成需求;

RDD的数据只有在调用行动算子时 时才会真正执行业务逻辑,之前的封装都是功能的扩展;

RDD中也有partition分区(各个分区之间并行计算),类似于haddop的切片,目的是提高并行计算的能力;
分区对应task;

3.1.2 RDD执行流程

RDD执行过程

3.1.3 RDD编程

创建RDD

(1)从集合(内存)中创建 RDD

在这里插入图片描述

makeRDD底层就是调用了rdd对象的parallelize方法
makeRDD的第二个参数就是分区(分区数量!)

(2)从外部存储(文件)创建RDD textFile

在这里插入图片描述
path可以写绝对路径也可以写相对路径,相对路径以project的根为基准;
path也可以是HDFS路径;

(3)从其他 RDD 创建
主要是通过一个RDD 运算完后,再产生新的RDD;

RDD 并行度与分区

默认情况下,Spark 可以将一个job作业切分多个task任务后,发送给 Executor 节点并行计算,而能够并行计算的任务数量我们称之为并行度
这个数量可以在构建RDD 时指定。

分区和并行度的关系:先有分区,分区对应task,task给Executor执行,有几个Executor在执行则有多少并行度;

RDD在分区内有序执行! 不同分区并行执行!

(1)makeRDD方法的分区
在这里插入图片描述

makeRDD的第二个参数就是分区(分区数量!)
如果不写,则默认为最大CPU核数;

切分规则:
在这里插入图片描述
假如传入 1,2,3,4,5 ,长度length为5,切片numSlices选3
则 0 until numSlices 即将 0 到3但不包含3进行迭代,
当i=0,start=0*0/0=0; end=(0+1)*5/3=1 , 输出(0,1)
当i=1,start=1 *5/3=1; end=(1+1)*5/3=3,输出(1,3)
即用数据长度除以分区数,然后用一个元组存储头尾数据的索引起点和终点,以此来分区;

(2)textFile方法的分区

在这里插入图片描述
第二个参数是minPartitions 最小分区数;

同样使用hadoop中的1.1倍原则,即切完后剩下的小于1.1倍,则不切了;

RDD转换算子 ※
  • 转换算子:功能补充
  • 行动算子:触发task调度和作业的执行
(1)Value类型

1. map( )
将处理的数据逐条进行映射转换,这里的转换可以是类型的转换,也可以是值的转换。
val rdd1: RDD[Int] = rdd.map( _*2 )

2. mapPartitions( )
将待处理的数据以分区为单位发送到计算节点进行处理(分区内计算,而不能分区间计算), 这里的处理是指可以进行任意的处理,可以是过滤数据;
map类似于IO中的流,效率不高,而mapPartititon类似于缓冲流,效率更高;
以分区为单位进行数据转换操作;

需要传入一个迭代器
在这里插入图片描述
缺点
整个分区的数据加载到内存进行引用,处理完的数据不会被释放掉,因为存在对象的引用,只有全部处理完才会释放数据

map 和 mapPartitions 的区别 ?
功能的角度:Map 算子是【分区内】一个数据一个数据的执行,类似于串行操作;mapPartitions 算子是以分区为单位进行批处理操作;
性能的角度:Map 算子因为类似于串行操作,所以性能比较低,而是 mapPartitions 算子类似于批处理,所以性能较高
但是mapPartitions 算子会长时间占用内存,那么这样会导致内存可能不够用,出现内存溢出的错误。

3. mapPartitionsWithIndex 分区索引
在处理时同时可以获取当前分区索引;

4. flatMap 扁平映射
将处理的数据进行扁平化后再进行映射处理,所以算子也称之为扁平映射;
返回一个可迭代集合 !

:将List(1,2) ,List(3,4)拆违单独的数字;
在这里插入图片描述 在这里插入图片描述

拆成多个后,需要LIst再将拆开的值做一个封装 ※
前面的List是数据的元素,后面的List是做封装的;

:wordcount中
在这里插入图片描述在这里插入图片描述
使用split( ) 方法返回的是一个数组,符合flatRDD返回的可迭代的集合的类型,所以只写split就可以;

5. glom
将【同一个分区】的数据直接转换为相同类型的内存数组进行处理,分区不变
和flatmap相反,将个体合并为整体
将【同一个分区】的数据当作了一个数组 !

:计算所有分区最大值求和(分区内取最大值,分区间最大值求和)
在这里插入图片描述
2+4=6

6. groupBy 分组
def groupBy[K](f: T => K)(implicit kt: ClassTag[K]): RDD[(K, Iterable[T])]
groupby会将数据源的每一个数据进行分组判断,根据Key进行分组,相同Key值的数据会被放置在一个组中;

会涉及到shuffle;
将数据根据指定的规则进行分组, 分区默认不变,但是【不同分区的】数据会被打乱重新组合------shuffle
大量数据shuffle到同一分区的情况,这就是spark的数据倾斜

:将 List(“Hello”, “Spark”, “Scala”, “Hadoop”)根据单词首写字母进行分组;
在这里插入图片描述
在这里插入图片描述
HS即分组的Key !

7. filter 过滤器
def filter(f: T => Boolean): RDD[T] 参数为Boolean, 满足为true的则保留,否则筛除;
将数据根据指定的规则进行筛选过滤,符合规则的数据保留,不符合规则的数据丢弃。
当数据进行筛选过滤后,分区不变(不会像groupby那样打散shuffle),但是分区内的数据可能不均衡,生产环境下,可能会出现数据倾斜;

:保留奇数,过滤掉偶数
在这里插入图片描述在这里插入图片描述

8. sample 随机抽取
根据指定的规则从数据集中随机抽取数据;
作用:数据倾斜的时候用sample;
分区时的数据是均衡的,但是shuffle时会打乱重新组合,极限情况下所有数在一个分区中,就产生数据倾斜
使用sample抽取查看,如1000条抽取100条,如果抽出来大部分都是某个数据,则针对这个数据进行处理,改善数据倾斜;

9.distinct 去重
将数据集中重复的数据去重;
底层用到了HashSet , HashSet数据结构①无序②不可重复 ;

10.coalesce 缩减 / 扩大分区
当大数据量过滤后只剩下少量数据,若还使用之前设置的分区数则有些浪费资源,如果将分区缩小再运行,能节约资源;

扩大则一定要shuffle ! 不shuffle的话原本同一个分区的数据无法被拆开,就无法扩大分区 !

11.repartition 缩减 / 扩大分区
def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]
repartiton的底层就是coalesce !参数 shuffle 的默认值为 true

该操作内部其实执行的是 coalesce 操作,参数 shuffle 的默认值为 true。无论是将分区数多的RDD 转换为分区数少的RDD,还是将分区数少的 RDD 转换为分区数多的RDD,repartition 操作都可以完成,因为无论如何都会经 shuffle 过程;

12.sortBy 分区内排序
默认是 true即升序asc, false为降序desc;
该操作用于排序数据。在排序之前,可以将数据通过 f 函数进行处理,之后按照 f 函数处理的结果进行排序,默认为升序排列。排序后新产生的 RDD 的分区数与原RDD 的分区数一致
由于要将不同分区的数据进行打散,所以中间存在 shuffle 的过程;

:排序
在这里插入图片描述在这里插入图片描述在这里插入图片描述

两个分区,分别都有序了!

(2)双Value类型

即两个数据源之间的关联操作!

1. intersection 交集
def intersection(other: RDD[T]): RDD[T]
对源RDD 和参数RDD 求交集后返回一个新的RDD;


在这里插入图片描述
输出: 3,4

2. union 并集
def union(other: RDD[T]): RDD[T]
对源RDD 和参数RDD 求并集后返回一个新的RDD,合并了两个数据源 !

3. subtract 差集
以一个 RDD 元素为主,去除两个 RDD 中重复元素,将其他元素保留下来。求差集;
如果以黄色为主,去掉的就是3、4,剩余的就是1、2
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
输出: 1,2

4. zip 拉链
将两个 RDD 中的元素,以键值对的形式进行合并。其中,键值对中的Key 为第 1 个 RDD中的元素,Value 为第 2 个 RDD 中的相同位置的元素。

注意:

  1. 使用zip要求数据源的分区个数必须一致 !
  2. 使用zip要求数据源头每个分区内的数量要相同 !


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

(3)Key - Value 类型

数据源必须是键值对类型, 即两个元素的tuple类型 (key,value)

1. partitionBy 重分区
def partitionBy(partitioner: Partitioner): RDD[(K, V)]
将数据1按照指定Partitioner 重新进行分区。Spark 默认的分区器HashPartitioner
区别repartiton ,repartition只是分区数量变化,而partitionBy是改变数据的位置 !
不同分区间的数据要放一起比较-----一定会shuffle

:使用HashPartitioner
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
此时1和3一个分区, 2和4一个分区 ! tuple后面的1只是为了让List转换成tuple格式 ;

repartition和partitionBy的区别?
Spark中,repartition和partitionBy都是重新分区的算子,其中partitionBy只能作用于PairRDD. 但是,当作用于PairRDD时,repartition和partitionBy的行为是不同的。
repartition是把数据随机打散均匀分布于各个Partition
而partitionBy则在参数中指定了Partitioner分区器(默认HashPartitioner),将每个(K,V)对按照K根据Partitioner计算得到对应的Partition;

2. reduceByKey 聚合 ※
def red1uceByKey(func: (V, V) => V): RDD[(K, V)]

将数据相同的Key 分为一组,对Value 进行聚合
当key有多个时,两两聚合,
当key只有一个时,不会进行计算!

:集合value
在这里插入图片描述
在这里插入图片描述

3. groupByKey 分组 ※
将数据根据 key 对 value 进行分组,即将key相同的数据放在一组形成一个对偶元素
元组元素中第一个元素是key, 第二个是相同的key的value的集合;


在这里插入图片描述
在这里插入图片描述

groupByKey 和 groupBy的区别?
groupByKey 的key固定是tuple的第一个元素! 而groupby的key需要自己指定;

reduceByKey 和 groupByKey 的区别 ※ ?

groupKey
在这里插入图片描述
reduceByKey
在这里插入图片描述
reduceByKey 【在同一个分区内】就有相同的key,其value就可以先预聚合 ! -------combine预聚合 !预聚合可以有效减少shuffle了落盘的数据量,提升shuffle的性能 !(shuffle是需要放到磁盘,会比较慢)

从性能的角度:reduceByKey 和 groupByKey 都存在 shuffle 的操作,但是reduceByKey 可以在 shuffle 前对分区内相同 key 的数据进行预聚合(combine)功能,这样会减少落盘的数据量,而groupByKey 只是进行分组,不存在数据量减少的问题,reduceByKey 性能比较高。

从功能的角度:reduceByKey 其实包含分组聚合的功能。
GroupByKey 只能分组,不能聚合,所以在分组聚合的场合下,推荐使用 reduceByKey,如果仅仅是分组而不需要聚合。那么还是只能使用groupByKey

reduceByKey分区内分区间计算规则是相同的,而aggregateByKey的分区内、分区间的计算规则可以不同;

4. aggregateByKey 合计
相同 key 的第一个数据和初始值进行分区内计算;
reduceByKey【分区内和分区间】计算规则是相同的,而aggregateByKey的分区内、分区键的计算规则可以不同
将数据根据不同的规则进行分区内计算和分区间计算;

函数签名def aggregateByKey[U: ClassTag](zeroValue: U) //第一个参数要传一个初始值,用于碰见第一个key的时候,和value进行分区内计算,如传0时,0和List中的value值1取max=1
(seqOp: (U, V) => U, combOp: (U, U) => U): RDD[(K, U)] //第二个参数传分区内规则 和 分区间计算规则

注:aggregateByKey 最终返回的结果应该和初始值的类型保持一致

当分区内计算规则和分区间计算规则相同时,aggregateByKey 就可以简化为foldByKey;

5. foldByKey
当分区内计算规则和分区间计算规则相同时,aggregateByKey 就可以简化为foldByKey;
def foldByKey(zeroValue: V)(func: (V, V) => V): RDD[(K, V)]

6.combineByKey
函数签名
def combineByKey[C](
createCombiner: V => C, // 将相同key的第一个数据进行结构的转换
mergeValue: (C, V) => C, // 分区内计算规则
mergeCombiners: (C, C) => C): RDD[(K, C)] // 分区间计算规则
函数说明:
最通用的对key-value 型 rdd 进行聚集操作的聚集函数(aggregation function)。类似于aggregate(),combineByKey()允许用户返回值的类型与输入不一致。

reduceByKey、foldByKey、aggregateByKey、combineByKey 的区别?
reduceByKey: 相同 key 的第一个数据不进行任何计算,分区内和分区间计算规则相

FoldByKey: 相同 key 的第一个数据和初始值进行分区内计算,分区内和分区间计算规则相同
AggregateByKey:相同 key 的第一个数据和初始值进行分区内计算,分区内和分区间计算规则可以不相同
CombineByKey:当计算时,发现数据结构不满足要求时,可以让第一个数据转换结构。分区内和分区间计算规则不相同

7.sortByKey 排序
在一个(K,V)的 RDD 上调用,K 必须实现 Ordered 接口(特质),返回一个按照 key 进行排序的结果;

8.join
join会导致数据量几何增长(笛卡尔积),并且影响shuffle性能,数据量越多,性能越差,不推荐使用;

函数说明:
在类型为(K,V)和(K,W)的RDD 上调用,返回一个相同 key 对应的value会连接在一起放在tuple的第二个元素
(K,(V,W))的RDD


在这里插入图片描述在这里插入图片描述
两个数据源,相同的key的value会连接在一起,放在tuple的第二个元素

思考一个问题:如果 key 存在不相等呢?
在这里插入图片描述在这里插入图片描述

key不想等的元素不会出现在结果中!

9.leftOuterJoin 左外连接
类似于 SQL 语句的左外连接
以左边数据为主要rdd !


在这里插入图片描述在这里插入图片描述

以rrd1为主,即rdd1中的c未匹配上,但是也会在结果中显示,即sql中外连接,匹配不上会显示null !
同理有rightOuterJoin

10. cogroup 分组+连接(group+connect) (full join)
在自己的rdd数据源建立一个分组,和另外一个数据源,即使没有数据也会放到一个组中,类似 full join
而join是保证两个数据源都有相同的key才能聚合;


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

:和Join不同的是 相同的key在一个rdd中会先进行合并,避免笛卡尔积
在这里插入图片描述
在这里插入图片描述

RDD行动算子

行动算子会直接触发Job作业的执行 !

1. reduce 聚合
def reduce(f: (T, T) => T): T
聚集RDD 中的所有元素,先聚合【分区内】数据,再聚合【分区间】数据
Scala的聚合都是两两聚合;


在这里插入图片描述
在这里插入图片描述

2. collect 采集
def collect(): Array[T]
将不同分区的数据按照分区的顺序 采集到Driver 端内存中形成数组


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
3. count 记数
函数签名
def count(): Long

函数说明
返回RDD 中元素的个数;


在这里插入图片描述
在这里插入图片描述
输出4;

4. first 取数据源第一个
函数签名
def first(): T

函数说明
返回RDD 中的第一个元;


在这里插入图片描述
在这里插入图片描述
输出1;

5.take 取多少个数据 ※

类似limit 0, _

函数签名
def take(num: Int): Array[T]

函数说明
返回一个由RDD 的前 n 个元素组成的数组;

在这里插入图片描述
在这里插入图片描述
输出1,2,3

6.takeOrdered 数据排序后,取多少个数据

函数说明:
返回该 RDD 排序后的前 n 个元素组成的数组
默认asc升序;
要降序传第二个参数,传Ordering.Int.reverse ;


在这里插入图片描述
输出1,2,3

7.aggregate
函数说明
分区的数据通过初始值和分区内的数据进行聚合,然后再和初始值进行分区间的数据聚合
而aggregateByKey只参与分区内的计算,分区间不参与 !


在这里插入图片描述
在这里插入图片描述
输出10;
初始值0,分区内相加,1+2=3 ,3+4=7; 分区间也相加 3+7=10;

aggregate和aggregateByKey的区别 :
aggregateByKey的初始值只参与【 分区内】的运算,
而aggregate中的初始值会参与【分区内】计算+【分区间】计算!


在这里插入图片描述
在这里插入图片描述

在aggregateByKe中,分区内10+1+2=13 ,10+3+4=17 ,分区间13+17=30
而在aggregate中,分区内10+1+2=13 ,10+3+4=17,分区间计算是10+13+17=40 !

8. fold 分区内和分区间计算规则相同时

函数签名
def fold(zeroValue: T)(op: (T, T) => T): T

函数说明
折叠操作,aggregate 的简化版操作

9.countByKey

函数签名
def countByKey(): Map[K, Long]

函数说明
统计每种 key 的个数


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

10.countByValue
这里的value不是键值对的value而是单值类型的value;


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
11. foreach
分布式遍历RDD 中的每一个元素,调用指定函数;

在这里插入图片描述
在这里插入图片描述
报错 :任务没有序列化,其实就是User类没有序列化 (driver端和executor通信时候进行io了)

原因
new User是在算子的外部(RDD算子以外的代码都是在Driver 端执行, RDD算子以内的代码都是在 Executor 端执行),即在Driver端的内存中;
而println中使用了user.age,是在算子的内部用到了算子外部的数据即闭包! 所以User对象要从Driver传到Executor !此时是Driver和Executor进程间通信,会在网络中传递,需要序列化 !

对象必须序列化才能io传输 !

collect.foreach和foreach的区别? ※
在这里插入图片描述在这里插入图片描述
collect.foreach先采集再循环collect会将数据以分区为单位采集到Driver端最后在Driver端的内存打印, 1、2是第一个分区,3、4在第二个分区;
foreach 是在Executor的内存中打印,这个问题涉及到计算逻辑在哪里执行的;

wordcount的几种方式 ※

方法一:
在这里插入图片描述
groupByKey需要传入key和value类型,所以将words用map转换类型为 (key,1),再使用groupByKey
groupByKey有shuffle ,当数据多时性能不高;

方法二:
:
简写rdd .flatMap(_.split(" ")).map((_,1)).reduceByKey(_ + _).collect().foreach(println(_))

reduceByKey 可以在 shuffle 前对分区内相同 key 的数据进行预聚合(combine)功能,这样会减少落盘的数据量;

方法三: 行动算子 countByKey
在这里插入图片描述

方法四: 行动算子 countByValue
不需要key ! 有相同的单词时会自动统计数量!
在这里插入图片描述

3.1.4 RDD序列化

1) 闭包检查
一个函数在实现逻辑时,将外部的变量引入函数的内部,改变了这个变量的声明周期,称之为闭包

从计算的角度, rdd算子以外的代码都是在【Driver 端】执行, rdd算子里面的代码都是在【 Executor 端】执行。

什么是序列化 ?
序列化的过程是将对象转换为二进制字节流;本质上可以理解为将非连续空间的链式存储转化为连续空间或块存储;

为什么需要序列化?
那么在 scala 的函数式编程中,就会导致算子内(Executor端)经常会用到算子外(Driver)的数据,这样就形成了闭包的效果,如果算子内使用的算子外的数据没有被序列化,就意味着无法传值给Executor 端执行,就会发生错误,
所以需要在执行任务计算前,检测闭包内的对象是否可以进行序列化,这个操作我们称之为闭包检测。Scala2.12 版本后闭包编译方式发生了改变;

闭包数据,都是以Task为单位进行发送,每个Task任务都包含闭包数据,可能会导致一个Executor中含有大量的数据,占用大量的内存;

2)序列化的方法
① java自带方法: extends Serializable

Kryo 轻量级序列化框架
Java 的序列化优点是能够序列化任何的类,但是比较重量级(字节多),序列化后,对象的提交也比较大,所以在大数据场景下性能差。

Spark 出于性能的考虑,Spark2.0 开始支持另外一种Kryo 序列化机制。Kryo 速度是 Serializable 的 10 倍,序列化后的文件大小约为Java序列化的1/10;
优点:速度快,体积小
缺点:不能对任意的类进行序列号;

注意

  1. 当 RDD 在【 Shuffle 数据的时候】,基本数据类型、数组和字符串类型已经在 Spark 内部使用 Kryo 来序列化。
  2. 即使使用Kryo 序列化,也要继承Serializable 接口;
  3. kryo可以序列化transient修饰的属性。而java中transient修饰的属性是不能被序列化的 ;
    Java中设计transient修饰主要是为了安全。

3.1.5 RDD依赖关系

为什么有依赖/血缘关系 ?
因为分布式计算中很容易出现错误,一旦出现错误,那么重新计算时从哪里开始执行,就需要血缘关系找到最开始的位置;

RDD是不保存数据的
为了出错时可以重新计算,就必须将RDD之间的转换关系记录下来;
一旦出现错误,可以根据血缘关系将数据源重新读取进行计算;

1)血缘关系

RDD 只支持粗粒度转换,即在大量记录上执行的单个操作。将创建 RDD 的一系列Lineage(血统)记录下来,以便恢复丢失的分区。
rdd.toDebugString 可以打印血缘关系:

RDD 的Lineage 会记录RDD 的元数据信息和转换行为,当该RDD 的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区。

有shuffle !会导致血缘关系中断 !

2)依赖关系

这里所谓的依赖关系,其实就是两个相邻 RDD 之间的关系,新的RDD依赖于旧的RDD;
间接的RDD称为血缘关系;

rdd.dependencies 可以打印依赖关系:

依赖关系分为 窄依赖宽依赖

3) RDD 窄依赖(一对一)

宽依赖窄依赖指的是【分区数据之间】的关系;

窄依赖表示每一个父(上游)RDD 的 Partition 分区只能被子(下游)RDD 的一个 分区Partition 使用, 窄依赖我们形象的比喻为“独生子女”。
新的RDD的分区依赖于旧的RDD 同一个分区 的数据;------OneToOne依赖(窄依赖);

在这里插入图片描述在这里插入图片描述
不同的分区之间不需要相互等待;

4) RDD 宽依赖(一对多)(shuffle依赖)

宽依赖表示同一个父(上游)RDD 的 Partition分区 被多个子(下游)RDD 的 Partition分区 依赖,会引起 Shuffle

新的RDD的多个分区依赖于旧的RDD中一个分区的数据;------Shuffle依赖(宽依赖);
在这里插入图片描述
有shuffle则需要分Stage阶段! Task也比窄依赖的多;
阶段2必须等待阶段1中所有分区都结束了才能开始! 有shuffle就要等!

上一个Stage阶段结束,才能开始下一个Stage;

5) RDD 阶段划分

默认有一个ResultStage
在这里插入图片描述

所以只要有一个Shuffle就会创造一个新的阶段 !
Stage 阶段的数量 = shuffle依赖的数量 + 1 (没有shuffle也会有一个ResultStage,Resultstage只会出现一次)
ResultStage只有一个,是最后需要执行的阶段

6) RDD Task任务划分

RDD 任务切分中间分为:ApplicationJobStageTask

  • Application:初始化一个 SparkContext 即生成一个Application;
  • Job:一个行动算子就会生成一个Job(action行动算子底层就是runJob);
  • Stage:Stage 个数 = 宽依赖(ShuffleDependency)的个数加 1 (自动有一个ResultStage);
  • Task:【一个 Stage 阶段中】,最后一个RDD 的分区个数就是【当前Stage阶段】的Task 的个数,只是一个阶段中 !而Job总共的Task并不是这个

注意
Application->Job->Stage->Task 每一层都是 1 对 n 的关系。
即一个应用中有多个行动算子即job;
一个Job中又多个shuffle依赖 , +1 即Stage阶段;
一个阶段会有最后的RDD,最后的RDD有多个partition分区即Task;

3.1.6 RDD持久化

引入:重用 RDD 对象并不能重用计算的数据,因为 RDD 是不存储数据的,要从头再算一遍!
在这里插入图片描述
在这里插入图片描述
当一个RDD需要重复被使用,则会从头再执行来获取数据 !
所以看上去对象重用了,但是数据并没有重用,而是又计算了一次;

解决
从map到reduceByKey时先把数据放入缓存!
在这里插入图片描述

1)RDD Cache缓存
RDD 通过Cache 或者 Persist 方法将前面的计算结果缓存,默认情况下会把数据以缓存在 JVM 的堆内存中,Cache缓存是保存为临时文件,后面会删除;

但是并不是这两个方法被调用时立即缓存,而是【触发后面的行动算子后】(触发了action行动算子才开始计算),该RDD 将会被缓存在计算节点的内存中,并供后面重用。

rdd.cache()

将rdd的计算结果数据存到内存中;

rdd. persist( 存储级别)

在这里插入图片描述
如果选Memory_only ,当内存不够用时 不会溢写到磁盘,而是直接丢弃;

场景
当数据执行时间较长,或者数据比较重要的场合,也可以采用持久化操作!

2)RDD CheckPoint检查点
rdd. CheckPoint

所谓的检查点其实就是通过将RDD 中间结果写入磁盘(一般是HDFS),所以可以重复使用而不用再次计算!

检查点要求必须设定检查点的保存路径(一般在分布式存储路径如HDFS),即会落盘而Cache缓存是保存为临时文件,后面会删除,而检查点在作业执行完后也不会删除;

由于血缘依赖过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果检查点之后有节点出现问题,可以从检查点开始重做血缘,减少了开销;

3) 缓存和CheckPoint检查点区别
相同点:都需要执行 Action算子 操作才能触发;

不同点
1.是否切断依赖
Cache 缓存只是将数据保存起来,不切断血缘依赖。Checkpoint 检查点会切断血缘依赖

checkpoint执行过程中会切断血缘关系:重新建立新的血缘,因为checkpoint将数据保存到分布式存储中,数据比较安全,而数据存放后完全作为新的数据源,所以可以切断血缘;(即checkpoint改变了数据源

2.安全性
Cache 缓存的数据默认只能临时存储在内存安全低,性能高。
persist:可以临时存在内存or磁盘,但是当作业执行完毕,临时文件就会丢失,安全性低;
checkpoint 的数据会落盘,通常存储在HDFS 等容错、高可用的文件系统,安全性高,落盘导致性能低。

注意:checkpoint为了数据的安全,检查点会再走一次作业!多独立执行了一次!

为了提高效率,一般将checkpoint和cache联合使用
check在缓存中记录数据,checkpoint()的RDD 会使用Cache 缓存,这样 checkpoint 的 job 只需从 Cache 缓存中读取数据即可,避免从头再算一次!
在这里插入图片描述

3.1.7 分区器

Spark 目前支持Hash 分区Range 分区,和用户自定义分区。
Hash 分区为当前的默认分区
分区器直接决定了RDD 中分区的个数、RDD 中每条数据经过Shuffle 后进入哪个分区,进而决定了Reduce 的个数。

  • 只有Key-Value 类型的RDD 才有分区器,非 Key-Value 类型的RDD 分区的值是 None
  • 每个RDD 的分区 ID 范围:0 ~ (numPartitions - 1),决定这个值是属于那个分区的。

1)Hash 分区:对于给定的 key,计算其hashCode,然后对分区个数取模;
2)Range 分区:将一定范围内的数据映射到一个分区中,尽量保证每个分区数据均匀,而且分区间有序;

3.1.8 RDD 文件读取与保存

Spark 的数据读取及数据保存可以从两个维度来作区分:文件格式以及文件系统
文件格式分为:text 文件、csv 文件、sequence 文件以及Object 文件;
文件系统分为:本地文件系统、HDFS、HBASE 以及数据库。

  • csv
    其文件以纯文本形式存储表格数据(数字和文本)。每条记录由字段组成,字段间的分隔符是其它字符或字符串,最常见的是逗号或制表符

  • sequence文件
    SequenceFile 文件是Hadoop 用来存储二进制形式的key-value 键值对而设计的一种平面文件(Flat File)。
    在 SparkContext 中,可以调用sequenceFilekeyClass, valueClass

  • object 对象文件
    对象文件是将对象【序列化后】保存的文件,采用 Java 的序列化机制。可以通过objectFileT: ClassTag 函数接收一个路径,读取对象文件,返回对应的 RDD,也可以通过调用saveAsObjectFile()实现对对象文件的输出。因为是序列化所以要指定类型。

3.2 累加器 Accumulator

3.2.1 概述;

引入
rdd算子以外的代码都是在Driver 端执行, rdd算子里面的代码都是在 Executor 端执行


在这里插入图片描述

在这里插入图片描述
输出: 0

原因:因为行动算子foreach内是在【executor】计算,但是算完后数据还是在executor,不会被采集到Driver端 !(没有用 collect )
在这里插入图片描述
在Driver端,sum会初始化为0,foreach在【executor端】实现sum的相加,然后并没有返回结果给Driver;

回顾collect.foreachforeach 区别 ?
1.collect.foreach是先采集再循环, collect会将数据以分区为单位采集到【Driver端】最后在Driver端的内存打印,
2.而foreach是在【Executor的内存】中打印;

背景
Driver将数据传给Executor进行计算,但是默认情况下无法将数据从Executor返回到Driver,这样就无法聚合计算结果;所以使用累加器!

解决:使用累加器
作用:累加器的作用就是将executor的计算结果 ①返回到Driver端 ②再做合并
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

注意:累加器为只写变量,累加器的值互相之间无法访问 ,而Driver都可以访问;

3.2.2 原理

累加器用来把【Executor 端】变量信息聚合到【Driver 端】。
在Driver 程序中定义的变量,以Task为单位传到Executor,在Executor 端的每个Task 都会得到这个变量的一份新的副本,每个 task 更新这些副本的值后, 传回Driver 端进行 merge

注意
在【转换算子中】使用累加器,如果没有行动算子的话则不会执行


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
输出0;


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
输出20;

3.3 广播变量 Broadcast

3.3.1 概述

定义:分布式、共享的只读变量;

引入
默认情况下,Driver给Executor传的闭包数据,都是 【以Task为单位】 进行发送,即一个Task就会发送一份数据 !且传的是相同的数据,这会导致内存中有大量的数据冗余, 占用大量的内存

解决广播变量
所以将数据(只读!!!)放在 【Executor进程的堆内存中】共享给Task线程 ,节省了内存空间,提升了效率 !

Executor就是一个JVM进程,所以在启动时,会自动分配内存;
可以将任务中的闭包数据放在【Executor的堆内存中】达到共享目的!

使用广播变量去前:
在这里插入图片描述

使用广播变量去后(Task线程共享Executor堆内存中的广播变量):
在这里插入图片描述

这样就避免出现大量冗余,
Spark的广播变量就可以将闭包数据保存在Executor进程的堆内存中(广播变量不能被更改),让Task线程去共享广播变量!
注意广播变量是只读变量 ! 不能更改!所以说不存在线程安全问题;

3.3.2 原理和使用

广播变量用来高效分发较大的对象。向所有【Executor工作节点】发送一个较大的只读值 !!! 以供多个Task共享。
比如,如果你的应用需要向所有节点发送一个较大的只读查询表, 广播变量用起来都很顺手。在多个并行操作中使用同一个变量,但是 Spark 会为每个任务分别发送。

用法:把rdd放入 sc.broadcast 的参数中即可;


在这里插入图片描述
// 封装map为广播变量
在这里插入图片描述
// rdd算子中访问广播变量
在这里插入图片描述

四. 运行流程

4.1 整体运行流程 ※

在这里插入图片描述

  1. 在 【YARN Cluster 】模式下,Spark客户端执行bin/spark-submit 提交任务,会产生sparksubmit进程, sparksubmit 进程会反射调用 YarnClusterApplicationmain方法,YarnClusterApplication会创建Yarn客户端,并让ResourceManager启动ApplicationMaster

  2. ResourceManager找到空闲的NodeManager节点,分配container,启动ApplicationMaster

  3. ApplicationMaster开启Driver线程;

  4. ApplicationMasterResourceManager申请开启Executor
    ResourceManager找到空闲的NodeManager节点,分配container,启动Executor进程;

  5. Executor 进程调用 ExecutorBackend向Driver里面的ScheduleBackend 反向注册;【Executor 全部注册完成后】,Driver 开始执行main 函数,遇到action行动算子触发一个JobSparkContext 将 Job 交给 DAGScheduler

  6. DagScheduler根据依赖关系构建有向无环图,然后根据宽依赖进行Stage划分,并将每个 Stage 序列化打包TaskSet 交给 TaskScheduler 调度;(DagScheduler做的事情比TaskScheduler简单)

  7. TaskScheduler会将TaskSet封装为 TaskSet Manager,放到rootPool调度池中,然后根据调度策略决定调度Task去Executor中执行;

    TaskSetManager 负责监控管理同一个 Stage 中的 Task,TaskScheduler 就是以TaskSetManager 为单元来调度任务;
    在这里插入图片描述

注意
9. 【执行过程】中,Executor会不断发送心跳给Driver的HeartbeatReceiver,从而实时监控Executor的状态;
10. Executor会将Task的运行状态通过ExecutorBackend发送给ScheduleBacked,ScheduleBacked再将其发给TaskSchedulerTaskScheduler 找到该Task 对应的 TaskSetManager,并通知到该 TaskSetManager,这样 TaskSetManager 就知道 Task的失败与成功状态,;

失败重试与黑名单机制:
当TaskSetManager 知道 Task的失败与成功状态后,对于失败的 Task,TaskSetManager记录失败的次数,如果失败次数还没有超过最大重试次数,那么就把它放回待调度的 Task 池子中,尝试重新调度,有黑名单

在记录 Task 失败次数过程中,会记录它上一次失败所在的 Executor Id 和Host,这样下次再调度这个 Task 时,会使用黑名单机制,避免它被调度到上一次失败的节点上,起到一定的容错作用

黑名单记录 Task 上一次失败所在的Executor Id 和 Host,以及其对应的“拉黑”时间,“拉黑” 时间是指这段时间内不要再往这个节点上调度这个 Task 了;

4.2 调度策略

TaskScheduler 支持两种调度策略,一种是 FIFO(默认) 的调度策略,另一种是 FAIR

1)FIFO 调度策略
如果是采用 FIFO 调度策略,则直接简单地将 TaskSetManager 按照先来先到的方式入队,出队时直接拿出最先进队的 TaskSetManager,即先进先出,类似队列;

2)FAIR 公平调度策略(略)

4.3 从wordcount的角度分析

wordcountrdd .flatMap(_.split(" ")).map((_,1)).reduceByKey(_ + _).collect().foreach(println(_))

在这里插入图片描述
主要针对Stage划分:
首先DAGScheduler 它会根据 RDD 的血缘关系构成的 DAG 进行切分,将一个 Job 划分为若干 Stages,具体划分策略是:从后往前,由最终的 RDD 不断通过依赖回溯判断父依赖是否是宽依赖,即以 Shuffle 为界,划分 Stage,窄依赖的 RDD 之间被划分到同一个 Stage 中
划分的 Stages 分两类,一类叫做 ResultStage 另一类叫做 ShuffleMapStage;

wordcount
由行动算子触发job,回溯遍历依赖关系,从RDD-3 开始回溯搜索,直到没有依赖的 RDD-0;
在回溯搜索过程中,RDD-3 依赖 RDD-2,并且是宽依赖,所以在RDD-2 和 RDD-3 之间划分 Stage,RDD-3 被划到最后一个 Stage,即 ResultStage
RDD-2 依赖RDD-1,RDD-1 依赖RDD-0,这些依赖都是窄依赖,所以将 RDD-0、RDD-1 和 RDD-2 划分到同一个 Stage即 ShuffleMapStage ,形成 pipeline 操作;
所以,wordcount有一个shuffle,共有一个ShuffleMapStage 和一个ResultStage ;

【在ShuffleMapStage 中】,实际执行的时候,数据记录会一气呵成地执行 RDD-0 到 RDD-2 的转化。不难看出,其本质上是一个深度优先搜索(Depth First Search)算法。

一个 Stage 是否被提交,需要判断它的父 Stage 是否执行,只有在父 Stage 执行完毕才能提交当前 Stage;
Stage 提交时会将 Task 信息(分区信息以及方法等)序列化打包TaskSet 交给 TaskScheduler

ShuffleMapStage 与 ResultStage
【在划分 stage 时】,最后一个 stage 是 ResultStage ,ResultStage前面的所有 stage 被称为 ShuffleMapStage
ShuffleMapStage 的结束伴随着 shuffle 文件的写磁盘。

ResultStage 基本上对应代码中的action 算子,即将一个函数应用在 RDD 的各个partition的数据集上,意味着一个 job 的运行结束

五. shuffle

Shuffle,中文的意思就是洗牌。
将所有分区的数据重新打散,然后根据某种特征汇聚到不同节点的过程就是Shuffle;

如reduceByKey时,需要根据key对不同分区的数据重新聚合;
在这里插入图片描述
Spark Shuffle 分为两种:一种是基于 Hash 的 Shuffle;另一种是基于 Sort 的 Shuffle。

5.1 Hash Shuffle

(1)未优化的Hash Shuffle
假设前提:每个 Executor 只有 1 个 CPU core,也就是说,无论这个 Executor 上分配多少个 task 线程,同一时间都只能执行一个 task 线程(并发);

如下图中有 3 个 Reducer,从 Task 开始那边各自把自己进行 Hash 计算(分区器:hash/numreduce 取模),分类出 3 个不同的类别,
每个 Task 都分对应reduce个数的数据,对应reduce个数的本地文件,然后 Reducer 会在每个 Task 中把属于自己类别的数据收集过来,汇聚成一个同类别的大集合;
即每 1 个 Task 输出 3 份本地文件对应3个reduce!

这里有 4 个Mapper Tasks,所以总共输出了 4 个 Tasks x 3 个分类文件 = 12 个本地小文件。
在这里插入图片描述
缺点:每个Task对都有每种类型的文件,小文件太多,效率低;

(2)优化的Hash Shuffle
优化的 HashShuffle 过程就是启用合并机制,合并机制就是复用 buffer;开启合并机制的配置是 spark.shuffle.consolidateFiles。该参数默认值为 false,将其设置为 true 即可开启优化机制;

在同一个Executor进程中,无论是有多少个 Task,都会把同样的Key的数据 放在同一个 Buffer 里,然后把 Buffer 中的数据写入【以 Core 数量为单位】的本地文件中,(一个 Core 只有一种类型的Key 的数据),

每 1 个Task 所在的进程中,分别写入共同进程中的 3 份本地文件,这里有 4 个 Mapper Tasks,
所以总共输出是 2 个进程 x 3 个分类文件 = 6 个本地小文件。
在这里插入图片描述
缺点:但任务过多,核数过多,依然有很多小文件!

总结Hash Shuffle
优点:
可以省略不必要的排序开销;避免了排序所需的内存开销。

缺点:
生产的文件过多,会对文件系统造成压力;

5.2 Sort Shuffle

(1)普通 Sort Shuffle

  1. 会先将数据写入一个内存中的数据结构Map,一边通过 Map 局部聚合,一遍写入内存,当达到阀值就将数据溢写到磁盘,清空内存中数据结构;(类似hadoop)
  2. 【在溢写磁盘前】,先根据 key 进行排序,排序过后的数据,会分批写入到磁盘文件中,一批默认是10000条数据;每一批溢写都会产生一个磁盘文件,也就是说一个Task 过程会产生多个临时文件。
  3. 最后【在每个 Task 中】,将所有的临时文件merge合并,此过程将所有临时文件读取出来,一次写入到最终文件;同时写一份索引文件,标识下游各个Task 的数据在文件中的位置,start offsetend offset
    在这里插入图片描述
    对应Task 输出 3个文件;

(二)bypass SortShuffle

bypass 运行机制的触发条件如下:
1)shuffle reduce task 数量小于等于 spark.shuffle.sort.bypassMergeThreshold 参数的值,默认为 200。
2)不是聚合类的 shuffle 算子(比如 reduceByKey)。

  1. 此时每个task 都会为每种reduce 都创建一个临时磁盘文件,并将数据按 key 进行hash 然后根据 key 的 hash 值,将数据 写入对应的磁盘文件之中;(类似未优化的Hash Shuffle,小文件非常多)
  2. 最后,同样会将所有临时磁盘文件都合并成一个磁盘文件,并创建一个单独的索引文件

bypass SortShuffle 类似未优化的Hash Shuffle,只是最后将临时文件合成了大文件并创建索引文件;
bypass SortShuffle 与普通 SortShuffleManager 运行机制的不同在于不会进行排序,所以节省了排序的性能开销;

Sort 的 Shuffle 机制的优缺点
优点:
1.小文件的数量大量减少,Mapper 端的内存占用变少;

缺点:
1.普通的Sort Shuffle需要排序,有排序的开销;

六. 内存管理

6.1 堆内和堆外内存规划

Executor 作为一个 JVM 进程,Executor 的内存管理建立在 JVM 的内存管理之上,
Spark 对 JVM的堆内(On-heap)空间进行了更为详细的分配,以充分利用内存。

同时,Spark 引入了堆外(Off-heap)内存,使之可以直接在工作节点的系统内存中开辟空间,进一步优化了内存的使用。
堆内内存受到 JVM 统一管理,堆外内存是直接向操作系统进行内存的申请和释放

在这里插入图片描述

(1)堆内内存

堆内内存的大小 , 由 Spark 应用程序启动时的– executor-memory 或spark.executor.memory 参数配置。
Executor 内运行的并发任务共享 JVM 堆内内存,这些任务在缓存 RDD 数据和广播(Broadcast)数据时占用的内存被规划为存储(Storage)内存, 而这些任务在执行 Shuffle 时占用的内存被规划为执行(Execution)内存,剩余的部分不做特殊规划,那些 Spark 内部的对象实例,或者用户定义的 Spark 应用程序中的对象实例,均占用剩余的空间。

Spark 对堆内内存的管理是一种逻辑上的”规划式”的管理,因为对象实例占用内存的申请和释放都由 JVM 完成,Spark 只能【在申请后和释放前】记录这些内存,我们来看其具体流程:
申请内存流程如下:

Spark 在代码中 new 一个对象实例;

JVM 从堆内内存分配空间,创建对象并返回对象引用;
Spark 保存该对象的引用,记录该对象占用的内存。释放内存流程如下:
1.Spark 记录该对象释放的内存,删除该对象的引用;
2.等待 JVM 的垃圾回收机制释放该对象占用的堆内内存。

我们知道,JVM 的对象可以 以序列化的方式存储,序列化的过程是将对象转换为二进制字节流,本质上可以理解为将非连续空间的链式存储转化为连续空间或块存储,在访问时则需要进行序列化的逆过程——反序列化,将字节流转化为对象,序列化的方式可以节省存储空间,但增加了存储和读取时候的计算开销。
对于 Spark 中序列化的对象,由于是字节流的形式,其占用的内存大小可直接计算,而对于非序列化的对象,其占用的内存是通过周期性地采样近似估算而得,即并不是每次新增的数据项都会计算一次占用的内存大小,这种方法降低了时间开销但是有可能误差较大,导致某一时刻的实际内存有可能远远超出预期。此外,在被 Spark 标记为释放的对象实例,很有可能在实际上并没有被 JVM 回收,导致实际可用的内存小于 Spark 记录的可用内存。所以 Spark 并不能准确记录实际可用的堆内内存,从而也就无法完全避免内存溢出(OOM, Out of Memory)的异常。
虽然不能精准控制堆内内存的申请和释放,但 Spark 通过对存储内存和执行内存各自独立的规划管理,可以决定是否要在存储内存里缓存新的 RDD,以及是否为新的任务分配执行内存,在一定程度上可以提升内存的利用率,减少异常的出现。

(2)堆外内存

为了进一步优化内存的使用以及提高 Shuffle 时排序的效率,Spark 引入了堆外(Off-heap)内存,使之可以直接在工作节点的系统内存中开辟空间,存储经过序列化的二进制数据。

堆外内存意味着把内存对象分配在 Java 虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机)。这样做的结果就是能保持一个较小的堆,以减少垃圾收集对应用的影响。

利用 JDK Unsafe API(从 Spark 2.0 开始,在管理堆外的存储内存时不再基于Tachyon, 而是与堆外的执行内存一样,基于 JDK Unsafe API 实现),Spark 可以直接操作系统堆外内存,减少了不必要的内存开销,以及频繁的 GC 扫描和回收,提升了处理性能。

堆外内存可以被精确地申请和释放;
(堆外内存之所以能够被精确的申请和释放,是由于内存的申请和释放不再通过JVM 机制,而是直接向操作系统申请,JVM 对于内存的清理是无法准确指定时间点的,因此无法实现精确的释放);
而且序列化的数据占用的空间可以被精确计算,所以相比堆内内存来说降低了管理的难度,也降低了误差。

在默认情况下堆外内存并不启用,可通过配置spark.memory.offHeap.enabled 参数启用, 并由spark.memory.offHeap.size 参数设定堆外空间的大小。除了没有 other 空间,堆外内存与堆内内存的划分方式相同,所有运行中的并发任务共享存储内存和执行内存。

6.2 内存空间分配

(1)静态内存管理(早期)

在 Spark 最初采用的静态内存管理机制下,存储内存执行内存其他内存的大小在Spark 应用程序运行期间均为固定的,但用户可以应用程序启动前进行配置,堆内内存的分配如图所示:
在这里插入图片描述
Storage 内存和 Execution 内存都有预留空间,目的是防止 OOM,因为 Spark 堆内内存大小的记录是不准确的,需要留出保险区域。

堆外内存分配较为简单,只有存储内存执行内存
由于堆外内存占用的空间可以被精确计算,所以无需再设定保险区域;
在这里插入图片描述

(2)统一内存管理

【Spark1.6 】之后引入的统一内存管理机制,与静态内存管理的区别在于存储内存和执行内存共享同一块空间,可以动态占用对方的空闲区域,统一内存管理的堆内内存结构如图所示:
在这里插入图片描述
统一内存管理的堆外内存结构如下图所示:
在这里插入图片描述
其中最重要的优化在于动态占用机制,其规则如下:

1)设定基本的存储内存和执行内存区域(spark.storage.storageFraction 参数),该设定确定了双方各自拥有的空间的范围;
2)双方的空间都不足时,则存储到硬盘若己方空间不足而对方空余时,可借用对方的空间;(存储空间不足是指不足以放下一个完整的 Block)
3)执行内存的空间被对方占用后,可让对方将占用的部分转存到硬盘,然后”归还”借用的空间
4)存储内存的空间被对方占用后,无法让对方”归还”,因为需要考虑 Shuffle 过程中的很多因素,实现起来较为复杂。

统一内存管理的动态占用机制如图所示:
在这里插入图片描述

  1. 都满了就溢写磁盘;
    一方不够,可以向另一方借内存;
  2. 【当存储内存向执行内存借内存】,当计算内存需要内存时,借的部分会被还回去,这块之前借的内存会被淘汰或者溢写,如果存储级别是MemoryOnly,就只能淘汰,不能溢写到磁盘 -------即没有出错的情况下,即正常情况下数据都有可能会丢失,所以cahe和persist的血缘是不能切断的!但是checkpoint可以切断,因为会落盘到HDFS中;
  3. 【当执行内存向存储内存借了内存】,当存储内存需要内存时,执行内存则不会归还!因为存储内存丢失则程序还可以根据血缘关系再走一次,而一旦执行内存的数据丢失了,结果统计的结果会出问题

七. 常见问题

1.说下对RDD的理解?RDD特点、算子?

RDD是弹性不可变分布式 数据集,是对计算逻辑的封装,本身不存储数据,是 Spark 中最基本的数据处理模型;多个RDD通过转换算子依赖在一起就能实现复杂的计算逻辑,完成需求;

代码中是一个抽象类;

特点

  • 弹性
    · 存储的弹性:内存与磁盘的自动切换
    · 计算的弹性:计算出错重试机制
    · 分区的弹性:可根据需要重新分片(可以根据需要来改变分区个数 )

  • 不可变
    RDD 封装的计算逻辑是只读的,不可变的,想要改变,只能产生新的RDD,在新的RDD 里面封装计算逻辑;

  • 分布式
    数据存储在大数据集群不同节点上;

  • 依赖关系
    依赖指的是两个相邻 RDD 之间的关系,新的RDD依赖旧的RDD;
    RDD之间通过转换算子相连,形成复杂的计算逻辑,以满足需求,
    依赖包括两种,一种是窄依赖(一对一),另一种是宽依赖(一对多);

  • 缓存
    cache、persist、checkpoint

  • 算子

参考:https://blog.csdn.net/xuehuagongzi000/article/details/103081319

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值