Spark

Spark

一、RDD
1)定义:RDD 又称弹性分布式数据集,是Spark中最基本的数据抽象。它代表的是一个不可变的、可分区的、里面的元素可以并行计算的集合。

不可变:一旦创建就不可改变,想要改变RDD必须创建新的RDD;RDDA-->RDDB,RDDA经过转换操作变成RDDB,这两个RDD具有血缘关系,但是是两个不同的RDD。

可分区& 并行计算:一个RDD通常由很多partition构成,在spark中有多少partition就对应有多少个task来执行。    
2)RDD的弹性体现在哪些方面:
1.内存的弹性:自动进行存储方式的切换。RDD的数据默认情况下存放在内存中的,但是在内存资源不足时,Spark会自动将RDD数据写入磁盘。

2.容错的弹性:基于Linage的高效容错机制,可以自动从失败节点中恢复分区,各个分区之间的数据互不影响。 

3.计算弹性:Task如果失败会特定次数的重试,缺省情况下,会自动重试4次。Stage如果失败会自动进行特定次数的重试,而且只会计算失败的阶段。缺省情况下,会自动重试4次。

4.分片弹性:通过rePartition和coalesce 算子实现分片的高度弹性。 

5.checkpoint【每次对RDD操作都会产生新的RDD,如果链条比较长,计算比较笨重,就把 数据放在硬盘中】和persist 【内存或磁盘中对数据进行复用】(检查点、持久化) 


6.数据调度弹性:Spark将执行模型抽象为DAG,可以让多个Stage 任务串联或者并行执行,而无需将中间结果写入到HDFS中。这样当某个节点故障的时候,可以由其他节点来执行出错的任务。
3)RDD的分布式体现在哪些方面 
	数据的计算并非局限于单个节点,而是多节点之间的协同计算得到

(4)RDD的数据集体现在哪些方面 
	RDD 是只读的分区记录的集合,每个分区分布在集群中不同的节点上;
	RDD 并不存储真正的数据,只是对数据和操作的描述。

(5)五大特性:
	1、一组分区(Partition):就是RDD数据集的分区

	2、一个分区计算的函数:相当于对RDD的每个split或partition做计算

	3、RDD之间的依赖关系:一个RDD依赖于其他多个RDD,这个点很重要,RDD的容错机制就是依据这个特性而来的

	4、一个Partitioner,即RDD的分片函数。当前Spark中实现了两种类型的分片函数,一个是基于哈希的HashPartitioner,另外一个是基于范围的RangePartitioner。只有对于key-value的RDD,才会有Partitioner,非key-value的RDD的Parititioner的值是None"Partitioner函数不但决定了RDD本身的分片数量,也决定了parent RDD Shuffle输出时的分片数量"5、一个列表:存储每个Partition的优先位置,移动数据不如移动计算,即当前的计算发给哪个分区,哪个executor,发程序给数据在的节点(优先位置)
1.6、RDD缓存
	Spark 的一个重要概念能够跨操作把数据持久化到内存中。当你持久化一个RDD的时候,每一个节点都将参与计算的所有分区数据存储到内存中。当基于这些数据进行其他操作时进行复用,这使得将来进行的操作更快(通常是数十倍)。cache()persist() 方法常用与缓存 重复使用的但是又不能太大的RDD。

	我们可以使用persist() 或者cache() 方法来标记需要保留的RDD,在第一次 action 操作结束后,它将保留在该计算节点的内存中。

	Spark 的缓存具有容错性:如果RDD的任何一个分区丢失,Spark可以通过原有的转换操作自动的重复计算并且创建出这个分区。

1、为什么要进行持久化
一方面:使用分布式系统时风险很高所以容易出错,就要容错,rdd出错或者分片出错可以根据血统算出来,如果没有对父rdd进行persist 或者cache的话,就需要重头。

另一方面:就是节省资源的问题,对重复使用的rdd进行持久化,可以避免重复计算,大大节省资源。


2、持久化的使用场景
1)某个步骤计算非常耗时,需要进行persist持久化
2)计算链条非常长,重新恢复要算很多步骤,很好使,persist 
3)checkpoint 是需要把整个job重新从头计算一边,checkpoint之前一定会进行persist
4) shuffle之后的结果进行持久化,shuffle要进性网络传输,风险很大,数据丢失重来,恢复代价很大  




3、存储级别的选择    
Spark 的存储级别的选择,核心问题是在内存使用率和 CPU 效率之间进行权衡。
	(1)默认情况下,性能最高的当然是MEMORY_ONLY。但前提是你的内存必须足够大,可以绰绰有余的存放下整个RDD 的所有数据。对这个RDD 的后续算子操作都是基于纯内存的数据操作,不需要从磁盘中读取数据,性能高。但是在实际场景下,恐怕这种策略使用的场景是有限的。

	(2)如果使用MEMORY_ONLY级别发生溢出,那么建议使用MEMORY_ONLY_SER级别。该级别会将RDD数据序列化后再保存在内存中,每一个分区仅仅是一个字节数组而已,大大降低了内存的占用。这种级别比MEMORY_ONLY 多出来的性能开销,主要是序列化和反序列化的开销。但是后续算子是基于纯内存的操作,总体性能比较高。

	(3)如果计算数据集不是很消耗资源,或者这些函数过滤了数据集中大量的数据,不建议把数据存储在磁盘中。这样会导致计算速度很慢,性能就下降。

	后缀为2的级别,必须将所有数据都复制一份副本,并发送到其他节点上。数据的复制以及网络传输会导致较大的性能开销。不建议使用。

4、在不会使用cached RDD的时候,及时使用unpersist方法来释放它。 


1.7RDD的检查点
	1、checkpoint:检查点常被用于运算时间很长或者运算量太大才能得到的RDD。将这些业务非常长的计算逻辑的中间结果缓存在HDFS上面,忽略了RDD的血统关系。

在spark中主要有两块应用:
	(1)是在spark core中对RDD做checkpoint,可以切断做checkpoint RDD的依赖关系,将RDD数据保存到可靠存储(如HDFS)以便数据恢复;

	(2)是应用在spark streaming中,使用checkpoint用来保存DStreamGraph以及相关配置信息,以便在Driver崩溃重启的时候能够接着之前进度继续进行处理(如之前waiting batch的job会在重启后继续处理)。

2、cache 与 checkpoint的 区别
	cache 和 checkpoint 是具有显著区别的。cache 是把RDD 计算出来,然后放到内存中,但是RDD 的依赖链也不能丢。当某个节点发生宕机,其上面缓存的RDD就会丢失,需要重新通过依赖链计算出来。checkpoint 是将RDD 保存在HDFS 上,是多副本可靠存储的。所以就算斩断依赖链,还是可以通过副本实现高容错。

3、	checkpoint 注意点
	checkpoint 是需要把整个job重新从头计算一边,所以最好先cache 一下。这样checkpoint 就可以保存缓存中的RDD 了,无需重新计算一边,极大提升了性能。

	在应用启动时需要先设置checkpoint文件的存储目录,通过下面的命令:sparkcontext.setcheckpoint(""),并在需要切断lineage 的RDD 后面执行:rdd.checkpoint(),最后在action 算子执行的时候进行相关的checkpoint 操作。
1.8 RDD的共享变量
	Spark提供了两种有限的共享变量:广播变量和累加器。
(1).广播变量(Broadcast Variables)
		– 广播变量缓存到各个节点的内存中,而不是每个 Task
		– 广播变量被创建后,能被集群中运行的任何函数调用
		– 广播变量是只读的,不能在被广播后修改
		– 对于大数据集的广播, Spark 尝试使用高效的广播算法来降低通信成本
	val broadcastVar = sc.broadcast(Array(1, 2, 3))方法参数中是要广播的变量
	(2). 累加器
	累加器只支持加法操作,可以高效地并行,用于实现计数器和变量求和。只有驱动程序才能获取累加器的值
1.9 Spark中创建RDD的方式总结
(9) Spark中创建RDD的方式总结
1、从集合中创建RDD;2、从外部存储创建RDD;3、从其他RDD创建。

从集合中创建RDD,Spark主要提供了两种函数:parallelize和makeRDD
val rdd = sc.parallelize(Array(1,2,3,4,5,6,7,8))
val rdd = sc.makeRDD(Array(1,2,3,4,5,6,7,8))

MakeRDD 底层调用了Parallelize    

由外部存储系统的数据集创建RDD
val rdd= sc.textFile("hdfs://node01:8020/data/test")

从其他RDD创建
val rdd1 = sc.parallelize(Array(1,2,3,4))
val rdd2 =rdd.map(x=>x.map(_*2))
1.10 简述SparkSQL中RDD、DataFrame、DataSet三者的区别与联系?
共性:
(1)RDD、DataFrameDataset全都是spark平台下的分布式弹性数据集,为处理超大型数据提供便利。
(2)三者都有惰性机制,在进行创建、转换,如map方法时,不会立即执行,只有在遇到Action如foreach时才会触发job 执行
(3)三者都会根据spark的内存情况自动缓存运算,这样即使数据量很大,也不用担心会内存溢出
(4)三者都有partition的概念
(5)三者有许多共同的函数,如filter,排序等
(6)在对DataFrameDataset进行操作许多操作都需要这个包进行支持
import spark.implicits._
(7DataFrameDataset均可使用模式匹配获取各个字段的值和类型
区别

RDD:
(1)RDD一般和机器学习库sparkmlib同时使用
(2)RDD不支持sparksql的操作

DataFrame:
(1) 与RDD和DataSet不同,DataFrame的每一行为固定的Row,只有通过解析才能获取各个字段的值
(2) DataFrameDataSet一般不和spark mlib同时使用  
(3)DataFrameDataSet均支持spark SQL的操作
(4)DataFrameDataset支持一些特别方便的保存方式,比如保存成csv,可以带上表头,这样每一列的字段名一目了然。

//保存
val saveoptions = Map("header" -> "true", "delimiter" -> "\t", "path" -> "hdfs://172.xx.xx.xx:9000/test")
datawDF.write.format("com.databricks.spark.csv").mode(SaveMode.Overwrite).options(saveoptions).save()
//读取
val options = Map("header" -> "true", "delimiter" -> "\t", "path" -> "hdfs://172.xx.xx.xx:9000/test")
val datarDF= spark.read.options(options).format("com.databricks.spark.csv").load()  


DataSet 
DatasetDataFrame拥有完全相同的成员函数,区别只是每一行的数据类型不同。
DataFrame也可以叫Dataset[Row],每一行的类型是Row,不解析,每一行究竟有哪些字段,各个字段又是什么类型都无从得知。
而Dataset中,每一行是什么类型是不一定的,在自定义了case class之后可以很自由的获得每一行的信息。
同时Spark还引入了一个新的概念Encoder。
	当序列化数据时,Encoder产生字节码与堆外内存进行交互,能够达到按需访问数据的效果,而不用反序列化整个对象。
三者之间的转换

1DataFrame/DataSet->RDD
val rdd1=testDF.rdd
val rdd2=testDS.rdd

2、RDD -> DataFrame (toDF)
val df: DataFrame = rdd.toDF("id","name","age")

3、RDD -> DataSet(toDS)
case class Coltest(col1:String,col2:Int)extends Serializable //定义字段名和类型
val testDS = rdd.map {line=>
Coltest(line._1,line._2)
}.toDS

4DataFrame -> dataSet (as)
case class Coltest(col1:String,col2:Int)extends Serializable //定义字段名和类型
val testDS = testDF.as[Coltest]

5、dataSet -> dataFrame (toDF)
val testDF = testDS.toDF
1.11 Spark Core/RDD是不是就要被淘汰了?
Spark Core/RDD 作为一种底层的API有它较为底层的应用场景,虽然这种场景会越来越少, 
DataFrame/DataSet 会逐渐替代Spark Core 的一些应用场景。但是不可否认,这种场景依然是存在的。

同时,Spark 
Core/RDD 是spark 生态中不可代替的基础API和引擎,DataFrame/DataSet的底层都是基于Spark Core/RDD 构建而成的。未来不会被淘汰,只是使用场景越来越少
1.12 我们什么时候需要使用到RDD,即使用RDD而不使用DataFrame/DataSet的场景?
1)如果我们需要对数据集进行非常底层的操作和掌握,例如:手动管理RDD分区,或者根据RDD运行逻辑结合各种参数和编程进行较为底层的调优。 

(2)我们要处理的数据是非结构化的,比如多媒体的数据,普通的文本数据。 

(3)我们不关心数据的schema,即元数据。
(4)我们想要使用过程式编程风格来处理数据。
1.13 DataSet/DataFrame性能比RDD高?
一句话总结:优化的执行计划。Dataframe/Dataset是构建在Spark SQL引擎之上的,它会根据你执行的操作,使用Spark SQL引擎的Catalyst来生成优化后的逻辑执行计划和物理执行计划,可以大幅度节省内存或磁盘的空间占用的开销,同时也能提升计算的性能。

如何执行:举例
先对DataFrame进行过滤,再join过滤后的较小的结果集   

在这里插入图片描述

二、算子
1、spark中的常用算子区别(map,mapPartitions,foreach,foreachPatition)
map:用于遍历RDD,将函数应用于每一个元素,返回新的RDD(transformation算子)
foreach:用于遍历RDD,将函数应用于每一个元素,无返回值(action算子)
mapPatitions:类似于map,但独立地在RDD的每一个分片上运行,返回生成一个新的RDD(transformation算子)
foreachPatition:用于遍历操作RDD中的每一个分区,无返回值(action算子) 
2、RDD 的分类
	转换算子:通过现有的RDD通过转换后生成新的RDD,转换是懒加载的,不会触发作业的执行。如:map、filter、flatMap
	动作算子:在RDD的计算后面,将计算结果返回给应用程序或者写入文件系统。reduce、collect
3、请列举Spark的transformation算子
map(func)作用:返回一个新的RDD,该RDD由每一个输入元素经过func函数转换后组成。
flatMap(func) 作用:类似于map,返回一个新的RDD,但是每一个输入元素可以被映射为0或多个输出元素。
filter(func) 过滤。返回一个新的RDD,该RDD由经过func函数计算后返回值为true的输入元素组成。
coalesce(numPartitions) 作用:缩减分区数(合并分区,不打乱顺序),用于大数据集过滤后,提高小数据集的执行效率。
repartition(numPartitions) 作用:根据分区数,重新通过网络随机洗牌所有数据。
========================================================
1)关系:
两者都是用来改变RDD的partition数量的,repartition底层调用的就是coalesce方法:coalesce(numPartitions, shuffle =false)
	2)区别:
repartition一定会发生shuffle,coalesce根据传入的参数来判断是否发生shuffle
一般情况下增大rdd的partition数量使用repartition,减少partition数量时使用coalesce
========================================================  
reduceByKey(func, [numTasks]) 作用:在一个(K,V)的RDD上调用,返回一个(K,V)的RDD,使用指定的reduce函数,将相同key的值聚合到一起,reduce任务的个数可以通过第二个可选的参数来设置

groupByKey([numTasks]) 作用:在一个(K,V)的RDD上调用,返回一个(K,Seq[v])的RDD。默认情况下有8个并行任务。

两者区别*******************************
  1. reduceByKey:按照key进行聚合,在shuffle之前有combine(预聚合)操作,返回结果是RDD[k,v].
	2. groupByKey:按照key进行分组,直接进行shuffle。
	3. 开发指导:reduceByKey比groupByKey,建议使用。但是需要注意是否会影响业务逻辑。

sortByKey([ascending], [numTasks]) 作用:在一个(K,V)的RDD上调用,K必须实现Ordered接口,返回一个按照key进行排序的(K,V)的RDD
4、请列举Spark的action算子
reduce(func)作用:通过func函数聚集RDD中的所有元素,先聚合分区内数据,再聚合分区间数据。
val rdd2 = sc.makeRDD(Array(("a",1),("a",3),("c",3),("d",5)))
rdd2.reduce((x,y)=>(x._1 + y._1,x._2 + y._2))
=> (adca,12)collect() 作用:在驱动程序中,以数组的形式返回数据集的所有元素。
count() 作用:返回RDD中元素的个数。
take(n) 作用:返回一个由RDD的前n个元素组成的数组。
foreach(func)作用:在数据集的每一个元素上,运行函数func进行更新。rdd.foreach(println(_))
saveAsTextFile(path)作用:将数据集的元素以

textfile的形式保存到HDFS文件系统或者其他支持的文件系统,
5、spark中有哪些算子会进行数据shuffle

去重类的操作:    
 def distinct()
 def distinct(numPartitions: Int) 

聚合类的操作:比如reduceByKey、groupByKey、 aggregateByKey     

 重分区类的操作:
 def coalesce(numPartitions: Int, shuffle:     dfe repartition(numPartitions: Int)

排序类算子:
def sortByKey 

   集合或者表操作: join 内连接,intersection 交集 subtract 差集    
6.为什么尽量少用groupByKey算子?
	尽量用aggregateByKey算子或者reduceByKey算子代替groupByKey,因为aggregateByKey算子或者reduceByKey算子会首先在分区内进行局部聚合,再全局聚合,而groupByKey算子直接进行全局聚合,而全局聚合时会走网络传输进行shuffle,会极大消耗资源,尤其是在数据量非常大的情况下,这一改进可以极大地提高性能。
三、DAGScheduler
1、谈谈spark中的宽窄依赖

宽依赖:指的是多个子RDD的Partition会依赖同一个父RDD的Partition,关系是一对多,即父RDD的一个分区的数据去到子RDD的不同分区里面,会有shuffle的产生。

窄依赖:指的是每一个父RDD的Partition最多被子RDD的一个partition使用,是一对一的,也就是父RDD的一个分区去到了子RDD的一个分区中,这个过程没有shuffle产生

区分的标准就是看父RDD的一个分区的数据的流向,要是流向一个partition的话就是窄依赖,否则就是宽依赖。
2、spark中如何划分stage
	Spark任务会根据RDD之间的执行流程,形成一个DAG有向无环图,DAG会提交给DAGSchedulerDAGScheduler会根据RDD的依赖关系把DAG划分相互依赖的多个stage。
	stage 的划分标准就是宽依赖:何时产生宽依赖就会产生一个新的stage,例如reduceByKey,groupByKey,join的算子,会导致宽依赖的产生;

	Stage的划分规则:从后往前,遇到宽依赖就切割stage;

每个stage包含一个或多个task,最后将这些Stage以taskSet的形式提交给TaskScheduler运行
3、简要介绍一下DAGScheduler以及其主要功能
	(1DAGSchedulerSpark面向Stage的高层调度器,负责接受用户提交的Job。 
	(2Spark任务会根据RDD之间的执行流程,形成一个DAG有向无环图,DAG会提交给DAGSchedulerDAGScheduler会根据RDD的依赖关系把DAG划分相互依赖的多个stage,划分的标准就是宽依赖,一旦遇到宽依赖就划分;Stage的划分规则:从后往前,遇到宽依赖就切割stage;每一个stage内产生一系列的task,并封装成taskset。

划分的Stages分两类,一类叫做ResultStage,为DAG最下游的Stage,另一类叫做ShuffleMapStage,为下游Stage准备数据。
一个Stage是否被提交,需要判断它的父Stage是否执行,只有在父Stage执行完毕才能提交当前Stage,如果一个Stage没有父Stage,那么从该Stage开始提交。

	(3)决定每个task的最佳位置,根据RDD依赖关系,并结合当前的缓存情况,将taskSet提交给TaskScheduler。

	(4)重新提交shuffle输出丢失的stage给taskScheduler。
4、DAG 图
	DAG,有向无环图,简单的来说,就是一个由顶点和有方向性的边构成的图中,从任意一个顶点出发,没有任意一条路径会将其带回到出发点的顶点位置。
5Job 
	由一个或者多个调度阶段所组成的一次性计算,往往由Spark Action 触发生成,一个 Application 往往含有多个job。
一旦driver程序中出现action,就会生成一个job,并向DAGScheduler提交job。如果driver程序后面还有action,那么其他action也对应生成相应的job,所以,driver端有多少action就会提交多少job。
每一个job可能会包含一个或者多个stage,在提交job 的过程中,DAGScheduler会首先从后往前划分stage,划分的标准就是宽依赖,一旦遇到宽依赖就划分,然后先提交没有父阶段的stage们,并在提交过程中,计算该stage的task数目以及类型,并提交具体的task,在这些无父阶段的stage提交完之后,依赖该stage 的stage才会提交。
四、Spark作业提交
1、作业提交参数
  - num-executors —— 启动executors的数量,默认为2
  - executor-cores —— 每个executor使用的内核数,默认为1,官方建议2-5- executor-memory —— executor内存大小,默认1G
 - driver-cores —— driver使用内核数,默认为1
 - driver-memory —— driver内存大小,默认512M
2、spark-submit的时候如何引入外部jar包
	在通过spark-submit提交任务时,可以通过添加配置参数来指定
--driver-class-path 类路径
--jars 外部jar包
Spark 的执行流图

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

五、Spark部署模式
1Spark 支持3种集群部署模式
1.Standalone:独立模式,Spark原生的简单集群管理器,自带完整的服务,可单独部署到一个集群中,无需依赖任何其他资源管理系统。同时使用Standalone可以很方便地搭建一个集群;
2Apache Mesos:一个强大的分布式资源管理框架,它允许多种不同的框架部署在其上,包括yarn;
3Hadoop YARN:提供统一的资源管理机制,在上面可以运行多套计算框架,如mapreduce、 Spark等,根据driver在集群中的位置不同,分为yarn client和yarn cluster。 在实际工厂环境下使用的绝大多数的集群管理器是Hadoop YARN。  
2Spark 3种本地运行模型
local:在本地运行,只有一个工作进程,无并行计算能力。
local[K]:在本地运行,有K个工作进程,通常设置K为机器的CPU核心数量。
local[*]:在本地运行,工作进程数量等于机器的CPU核心数量。    
3Spark运行模式配置
	用户在提交任务给Spark处理时,以下两个参数共同决定了Spark的运行方式。
–master MASTER_URL :决定了Spark任务提交给哪种集群处理。
–deploy-mode DEPLOY_MODE:决定了Driver的运行方式,可选值为Client或者 Cluster
4、YARN模式运行机制
4.1 YARN Client模式
	在YARN Client模式下,Driver在任务提交的本地机器上运行,Driver启动后会和ResourceManager通讯申请启动ApplicationMaster,随后ResourceManager分配container,在合适的NodeManager上启动ApplicationMaster,此时的ApplicationMaster的功能相当于一个ExecutorLaucher,只负责向ResourceManager申请Executor内存。
	ResourceManager接到ApplicationMaster的资源申请后会分配container,然后ApplicationMaster在资源分配指定的NodeManager上启动Executor进程,Executor进程启动后会向Driver反向注册,Executor全部注册完成后Driver开始执行main函数,之后执行到Action算子时,触发一个job,并根据宽依赖开始划分stage,每个stage生成对应的taskSet,之后将task分发到各个Executor上执行。

img

	4.2 YARN Cluster模式
	在YARN Cluster模式下,任务提交后会和ResourceManager通讯申请启动ApplicationMaster,随后ResourceManager分配container,在合适的NodeManager上启动ApplicationMaster,此时的ApplicationMaster就是Driver。在ApplicationMaster中进行SparkContext 的初始化 。
	Driver启动后向ResourceManager申请Executor内存,ResourceManager接到ApplicationMaster的资源申请后会分配container,然后在合适的NodeManager上启动Executor进程,Executor进程启动后会向Driver反向注册,Executor全部注册完成后Driver开始执行main函数,之后执行到Action算子时,触发一个job,并根据宽依赖开始划分stage,每个stage生成对应的taskSet,之后将task分发到各个Executor上执行。

在这里插入图片描述

5Standalone 模式
5.1 Standalone Client模式
	在Standalone Client模式下,Driver运行在任务提交的本地机器上,Driver启动后向Master注册应用程序,Master根据submit脚本的资源需求找到内部资源至少可以启动一个Executor的所有Worker,然后在这些Worker之间分配ExecutorWorker上的Executor启动后会向Driver反向注册,所有的Executor注册完成后,Driver开始执行main函数,之后执行到Action算子时,开始划分stage,每个stage生成对应的taskSet,之后将task分发到各个Executor上执行。
5.2 Standalone Cluster模式
	在Standalone Cluster模式下,任务提交后,Master会找到一个Worker启动Driver进程, Driver启动后向Master注册应用程序,Master根据submit脚本的资源需求找到内部资源至少可以启动一个Executor的所有Worker,然后在这些Worker之间分配ExecutorWorker上的Executor启动后会向Driver反向注册,所有的Executor注册完成后,Driver开始执行main函数,之后执行到Action算子时,触发一个job,并根据宽依赖开始划分stage,每个stage生成对应的taskSet,之后将task分发到各个Executor上执行。
六、Spark 优化
6.1 spark 如何防止内存溢出
(1) driver端的内存溢出:
	可以增大driver的内存参数:spark.driver.memory (default 1g)这个参数用来设置Driver的内存。
在Spark程序中,SparkContextDAGScheduler以及对应rdd的Stage切分都是在Driver端运行,如果用户自己写的程序有过多的步骤,切分出过多的Stage,这部分信息消耗的是Driver的内存,这个时候就需要调大Driver的内存。    
(2) map过程产生大量对象导致内存溢出
	这种溢出的原因是在单个map中产生了大量的对象导致的,例如:rdd.map(x=>for(i <- 1 to 10000) yield i.toString),这个操作在rdd中,每个对象都产生了10000个对象,这肯定很容易产生内存溢出的问题。

针对这种问题,在不增加内存的情况下,可以通过减少每个Task的大小,以便达到每个Task即使产生大量的对象Executor的内存也能够装得下。

具体做法可以在会产生大量对象的map操作之前调用repartition方法,分区成更小的块传入map。例如:rdd.repartition(10000).map(x=>for(i <- 1 to 10000) yield i.toString)。
	面对这种问题注意,不能使用rdd.coalesce方法,这个方法只能减少分区,不能增加分区,不会有shuffle的过程。
(3)数据不平衡导致内存溢出:
	数据不平衡除了有可能导致内存溢出外,也有可能导致性能的问题,解决方法和上面说的类似,就是调用repartition重新分区。
(4) shuffle后内存溢出:
	shuffle内存溢出的情况可以说都是shuffle后单个文件过大导致的。在shuffle的使用,需要传入一个partitioner,大部分Spark中的shuffle操作,默认的partitioner都是HashPatitioner,默认值是父RDD中最大的分区数,这个参数通过spark.default.parallelism控制(在spark-sql中用spark.sql.shuffle.partitions) , spark.default.parallelism参数只对HashPartitioner有效,所以如果是别的Partitioner或者自己实现的Partitioner就不能使用spark.default.parallelism这个参数来控制shuffle的并发量了。如果是别的partitioner导致的shuffle内存溢出,就需要从partitioner的代码增加partitions的数量。

(5)、standalone模式下资源分配不均匀导致内存溢出:
	在standalone的模式下如果配置了–total-executor-cores 和 –executor-memory 这两个参数,但是没有配置–executor-cores这个参数的话,就有可能导致,每个Executor的memory是一样的,但是cores的数量不同,那么在cores数量多的Executor中,由于能够同时执行多个Task,就容易导致内存溢出的情况。这种情况的解决方法就是同时配置–executor-cores或者spark.executor.cores参数,确保Executor资源分配均匀。
6.2 spark中的数据倾斜的
1、数据倾斜的现象:
	多数task执行速度较快,少数task执行时间非常长,或者等待很长时间后提示你内存不足,执行失败。
(2)、数据倾斜的原因 
	数据问题 
		1、key本身分布不均衡(包括大量的key为空)
		2、key的设置不合理
	spark使用问题 
		1、shuffle时的并发度不够
		2、计算方式有误
(3)、数据倾斜的后果 
	1、spark中的stage的执行时间受限于最后那个执行完成的task,因此运行缓慢的任务会拖垮整个程序的运行速度(分布式程序运行的速度是由最慢的那个task决定的)。
	2、过多的数据在同一个task中运行,将会把executor撑爆。
(4)、如何解决数据倾斜
	4.1 数据问题造成的数据倾斜

 -->找出异常的Key
  如果任务长时间卡在最后最后1个或几个任务,首先要对key进行抽样分析,判断是哪些key造成的。选取key,对数据进行抽样,统计出现的次数,根据出现次数大小排序取出前几个。
  比如: df.select(“key”).sample(false,0.1).(k=>(k,1)).reduceBykey(+).map(k=>(k._2,k._1)).sortByKey(false).take(10)
	    如果发现多数数据分布都较为平均,而个别数据比其他数据大上若干个数量级,则说明发生了数据倾斜。

 -->经过分析,倾斜的数据主要有以下三种情况:
		1null(空值)或是一些无意义的信息之类的,大多是这个原因引起。
		2、无效数据,大量重复的测试数据或是对结果影响不大的有效数据。
		3、有效数据,业务导致的正常数据分布。

  -->解决办法
		第12种情况,直接对数据进行过滤即可(因为该数据对当前业务不会产生影响)。
		第3种情况则需要进行一些特殊操作,常见的有以下几种做法

		(1) 隔离执行,将异常的key过滤出来单独处理,最后与正常数据的处理结果进行union操作。
		(2) 对key先添加随机值,进行操作后,去掉随机值,再进行一次操作。
		(3) 使用reduceByKey 代替 groupByKey(reduceByKey用于对每个key对应的多个value进行merge操作,最重要的是它能够在本地先进行merge操作,并且merge操作可以通过函数自定义.)
		(4) 使用map join。 

  --> 案例:如果使用reduceByKey因为数据倾斜造成运行失败的问题。具体操作流程如下:
		(1) 将原始的 key 转化为 key + 随机值(例如Random.nextInt)
		(2) 对数据进行 reduceByKey(func)
		(3) 将 key + 随机值 转成 key
		(4) 再对数据进行 reduceByKey(func)  


4.2 提高shuffle并行度
	dataFrame和sparkSql可以设置spark.sql.shuffle.partitions参数控制shuffle的并发度,默认为200。
	rdd操作可以设置spark.default.parallelism控制并发度,默认参数由不同的Cluster Manager控制。
	局限性: 只是让每个task执行更少的不同的key。无法解决个别key特别大的情况造成的倾斜,如果某些key的大小非常大,即使一个task单独执行它,也会受到数据倾斜的困扰。

	使用map join 代替reduce join
	在小表不是特别大(取决于你的executor大小)的情况下使用,可以使程序避免shuffle的过程,自然也就没有数据倾斜的困扰了.(详细见局限性: 因为是先将小数据发送到每个executor上,所以数据量不能太大。 
http://blog.csdn.net/lsshlsw/article/details/50834858、http://blog.csdn.net/lsshlsw/article/details/48694893
	将多份数据进行关联是数据处理过程中非常普遍的用法,不过在分布式计算系统中,这个问题往往会变的非常麻烦,因为框架提供的 join 操作一般会将所有数据根据 key 发送到所有的 reduce 分区中去,也就是 shuffle 的过程。造成大量的网络以及磁盘IO消耗,运行效率极其低下,这个过程一般被称为 reduce-side-join。
	如果其中有张表较小的话,我们则可以自己实现在 map 端实现数据关联,跳过大量数据进行 shuffle 的过程,运行时间得到大量缩短,根据不同数据可能会有几倍到数十倍的性能提升。

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

6.3 Spark为什么比mapreduce快?
1、spark是基于内存进行数据处理的,MapReduce是基于磁盘进行数据处理的
MapReduce的设设计:中间结果保存在HDFS文件中,提高了可靠性,减少了内存占用。但是牺牲了性能。
Spark的设计:数据在内存中进行交换,要快一些,但是内存这个东西,可靠性不如磁盘。所以性能方面比MapReduce要好。
2、spark中具有DAG有向无环图,DAG有向无环图在此过程中减少了shuffle以及落地磁盘的次数。

	SparkDAGScheduler 相当于一个改进版的 MapReduce,如果计算不涉及与其他节点进行数据交换,Spark 可以在内存中一次性完成这些操作,也就是中间结果无须落盘,减少了磁盘 IO 的操作。但是,如果计算过程中涉及数据交换,Spark 也是会把 shuffle 的数据写磁盘的!

"有一个误区,Spark 是基于内存的计算,所以快,这不是主要原因",要对数据做计算,必然得加载到内存,Hadoop 也是如此,只不过 Spark 支持将需要反复用到的数据给 Cache 到内存中,减少数据加载耗时,所以 Spark 跑机器学习算法比较在行(需要对数据进行反复迭代)。Spark 基于磁盘的计算也是比 Hadoop 快。

3、spark是粗粒度资源申请,也就是当提交spark application的时候,application会将所有的资源申请完毕,如果申请不到资源就等待,如果申请到资源才执行application,task在执行的时候就不需要自己去申请资源,task执行快,当最后一个task执行完之后task才会被释放。

优点是执行速度快,缺点是不能使集群得到充分的利用

MapReduce是细粒度资源申请,当提交application的时候,task执行时,自己申请资源,自己释放资源,task执行完毕之后,资源立即会被释放,task执行的慢,application执行的相对比较慢。

优点是集群资源得到充分利用,缺点是application执行的相对比较慢。
6.4 Mapreduce和Spark的都是并行计算,那么他们有什么相同和区别?
相同:两者都是用mr模型来进行并行计算:
不同:见改进
6.5 Spark对MapReduce的改进
1MapReduce抽象层次低,需要手工编写代码完成;Spark基于RDD抽象,使数据处理逻辑的代码非常简短。

2MapReduce只提供了map和reduce两个操作,表达力欠缺;Spark提供了很多转换和动作算子。 

3MapReduce中,只有map和reduce两个阶段,复杂的计算需要大量的组合,并且由开发者自己定义组合方式;Spark中,RDD可以连续执行多个转换操作,在调度时可以生成多个阶段(Stage4.MapReduce处理逻辑隐藏在代码中,不直观;Spark代码不包含操作细节,通过算子逻辑更清晰。


5.MapReduce中间结果放在HDFS中;Spark中间结果放在内存中,内存放不下时才写入本地磁盘而不是HDFS,这显著提高了性能,特别是在迭代式数据处理的场合。    
6.6 Spark 参数调优
https://www.cnblogs.com/arachis/p/spark_parameters.html
1、num-executors参数说明:该参数用于设置Spark作业总共要用多少个Executor进程来执行。

参数调优建议:每个Spark作业的运行一般设置50~100个左右的Executor进程比较合适
2、executor-memory
参数说明:该参数用于设置每个Executor进程的内存。Executor内存的大小,很多时候直接决定了Spark作业的性能,而且跟常见的JVM OOM异常,也有直接的关联。
	参数调优建议:每个Executor进程的内存设置4G~8G较为合适。num-executors乘以executor-memory,是不能超过队列的最大内存量的。
3、executor-cores
参数说明:该参数用于设置每个Executor进程的CPU core数量。这个参数决定了每个Executor进程并行执行task线程的能力。
	参数调优建议:Executor的CPU core数量设置为2~4个较为合适。
4、driver-memory
参数说明:该参数用于设置Driver进程的内存。
参数调优建议:Driver的内存通常来说不设置,或者设置1G左右应该就够了。唯一需要注意的一点是,如果需要使用collect算子将RDD的数据全部拉取到Driver上进行处理,那么必须确保Driver的内存足够大,否则会出现OOM内存溢出的问题。
5、spark.default.parallelism
参数说明:该参数用于设置每个stage的默认task数量。
参数调优建议:Spark作业的默认task数量为500~1000个较为合适。很多同学常犯的一个错误就是不去设置这个参数,那么此时就会导致Spark自己根据底层HDFS的block数量来设置task的数量,默认是一个HDFS block对应一个task。通常来说,Spark默认设置的数量是偏少的(比如就几十个task),如果task数量偏少的话,就会导致你前面设置好的Executor的参数都前功尽弃。试想一下,无论你的Executor进程有多少个,内存和CPU有多大,但是task只有1个或者10个,那么90%Executor进程可能根本就没有task执行,也就是白白浪费了资源!因此Spark官网建议的设置原则是,设置该参数为num-executors * executor-cores的2~3倍较为合适,比如Executor的总CPU core数量为300个,那么设置1000个task是可以的。
6、spark.storage.memoryFraction  存储内存的比例
参数说明:该参数用于设置RDD持久化数据在Executor内存中能占的比例,默认是0.6。也就是说,默认Executor 60%的内存,可以用来保存持久化的RDD数据。

参数调优建议:如果Spark作业中,有较多的RDD持久化操作,该参数的值可以适当提高一些,保证持久化的数据能够容纳在内存中。避免内存不够缓存所有的数据,导致数据只能写入磁盘中,降低了性能。    
7、spark.shuffle.memoryFraction 执行内存的比例
参数说明:该参数用于设置shuffle过程中一个task拉取到上个stage的task的输出后,进行聚合操作时能够使用的Executor内存的比例,默认是0.2。

参数调优建议:如果Spark作业中的RDD持久化操作较少,shuffle操作较多时,建议降低持久化操作的内存占比,提高shuffle操作的内存占比比例,避免shuffle过程中数据过多时内存不够用,必须溢写到磁盘上,降低了性能。    
七、Spark任务调度
在详细阐述任务调度前,首先说明下Spark里的几个概念。
	一个Spark应用程序包括JobStage以及Task三个概念:
		Job是以Action方法为界,遇到一个Action方法则触发一个JobStageJob的子集,以RDD宽依赖(Shuffle)为界,遇到Shuffle做一次划分;
		TaskStage的子集,以并行度(分区数)来衡量,分区数是多少,则有多少个task。

在这里插入图片描述

7.1 Spark Stage级调度
	DAGScheduler负责Stage级的调度,主要是将job切分成若干Stages,并将每个Stage打包成TaskSet交给TaskScheduler调度。

	Spark的任务调度是从DAG切割开始,主要是由DAGScheduler来完成。当遇到一个Action操作后就会触发一个Job的计算,并交给DAGScheduler来提交,DAGScheduler会根据RDD的血缘关系构成的DAG进行切分,将一个Job划分为若干Stages,具体划分策略是,由最终的RDD不断通过依赖回溯判断父依赖是否是宽依赖,即以Shuffle为界,划分Stage,窄依赖的RDD之间被划分到同一个Stage中。划分的Stages分两类,一类叫做ResultStage,为DAG最下游的Stage,另一类叫做ShuffleMapStage,为下游Stage准备数据。
一个Stage是否被提交,需要判断它的父Stage是否执行,只有在父Stage执行完毕才能提交当前Stage,如果一个Stage没有父Stage,那么从该Stage开始提交。Stage提交时会将Task信息(分区信息以及方法等)序列化并被打包成TaskSet交给TaskScheduler,一个Partition对应一个Task,另一方面TaskScheduler会监控Stage的运行状态,只有Executor丢失或者Task由于Fetch失败才需要重新提交失败的Stage以调度运行失败的任务,其他类型的Task失败会在TaskScheduler的调度过程中重试。
	下图是涉及到Job提交的相关方法调用流程图。   

在这里插入图片描述

7.2 Spark Task级调度
	Spark Task的调度是由TaskScheduler来完成,由前文可知,DAGSchedulerStage打包到TaskSet交给TaskSchedulerTaskScheduler会将TaskSet封装为TaskSetManager加入到调度队列中,TaskSetManager负责监控管理同一个Stage中的TasksTaskScheduler就是以TaskSetManager为单元来调度任务。

之后就去请求计算资源,系统将application 所有可用的executor封装成一个workOffers 集合;workOffer集合代表了所有准备好计算资源。

资源分配后之后,就会启动task。首先反序列化task 信息,executor 会为每个 task 创建一个TaskRunner,并标记task处于运行态,最后将 taskRunner 放到一个线程池中执行。      
TaskSetManager结构如下图所示

在这里插入图片描述

	TaskSetManager负责监控管理同一个Stage中的TasksTaskScheduler就是以TaskSetManager为单元来调度任务。
流程如下:
(1TaskScheduler会为每一个taskset创建一个createTaskSetManager,用于管理同一Stage的task;

(2)将TaskSetManager加入rootPool调度池中之后,调用SchedulerBackendreviveOffers() 发送ReviveOffer消息;  
(3)driverEndpoint接收到分配置资源的消息后,调用makeOffers方法。将application 所有可用的executor封装成一个workOffers 集合;workOffer集合代表了所有准备好计算资源,taskscheduler.resourceOffers()为每一task分配executor。资源分配后之后,向executorSchedulerBackend发送启动task 的消息 
(4)向ExecutorSchedulerBackend 发送启动task 的消息后,触发task的执行。首先序列化task 信息,executor 会为每个 task 创建一个 TaskRunner,并标记task处于运行态,最后将 taskRunner 放到一个线程池中执行。

在这里插入图片描述

	TaskScheduler支持两种调度策略,一种是FIFO,也是默认的调度策略,另一种是FAIR。在TaskScheduler初始化过程中会实例化rootPool调度池,根调度池,是Pool类型。
	如果是采用FIFO调度策略,则直接简单地将TaskSetManager按照先来先到的方式入队,出队时直接拿出最先进队的TaskSetManager,其树结构如下图所示,TaskSetManager保存在一个FIFO队列中。

在这里插入图片描述

	FAIR调度策略有一个rootPool和多个子Pool,各个子Pool中存储着所有待分配的TaskSetMagager。

在FAIR模式中,需要先对子Pool进行排序,再对子Pool里面的TaskSetMagager进行排序,因为PoolTaskSetMagager都继承了Schedulable特质,因此使用相同的排序算法。

	排序过程的比较是基于Fair-share来比较的,每个要排序的对象包含三个属性: runningTasks值(正在运行的Task数)、minShare值(最小资源共享)、weight值,比较时会综合考量runningTasks值,minShare值以及weight值。

	minShare、weight的值均在公平调度配置文件fairscheduler.xml中被指定,调度池在构建阶段会读取此文件的相关配置。

	1)如果A对象的runningTasks大于它的minShare,B对象的runningTasks小于它的minShare,那么B排在A前面;(runningTasks比minShare小的先执行)

	2)如果AB对象的runningTasks都小于它们的minShare,那么就比较runningTasks与minShare的比值(minShare使用率),谁小谁排前面;(minShare使用率低的先执行)

	3)如果AB对象的runningTasks都大于它们的minShare,那么就比较runningTasks与weight的比值(权重使用率),谁小谁排前面(权重使用率低的先执行)

	4)如果上述比较均相等,则比较名字。
	整体上来说就是通过minShare和weight这两个参数控制比较过程,可以做到让minShare使用率和权重使用率少(实际运行task比例较少)的先运行
	FAIR模式排序完成后,所有的TaskSetManager被放入一个ArrayBuffer里,之后依次被取出并发送给Executor执行.

在这里插入图片描述

八、简述Spark的两种核心Shuffle
1Stage的分类:ShuffleMapStageResultStage
	在划分stage时,最后一个stage称为finalStage,它本质上是一个ResultStage对象,前面的所有stage被称为ShuffleMapStageShuffleMapStage主要为下游Stage准备数据,伴随着shuffle文件的写磁盘。 
	ResultStage基本上对应代码中的action算子,意味着一个job的运行结束。

2Shuffle中的任务个数

	map阶段:如果Spark任务从HDFS中读取数据,那么初始RDD分区个数由该文件的split个数决定,也就是一个split对应生成的RDD的一个partition,我们假设初始partition个数为N。初始RDD经过一系列算子计算后(假设没有执行repartition和coalesce算子进行重分区)当执行到Shuffle操作时,map端的task个数和partition个数一致,即map task为N个。

reduce阶段:stage默认取 spark.default.parallelism 这个配置项的值作为分区数,如果没有配置,则以map端的最后一个RDD的分区数作为其分区数(也就是N),那么分区数就决定了reduce端的task的个数。

3、reduce端的数据拉取过程如下:

		1.map task 执行完毕后会将计算状态以及磁盘小文件位置等信息封装到MapStatus对象中,然后由本进程中的MapOutPutTrackerWorker对象将mapStatus对象发送给Driver进程的MapOutPutTrackerMaster对象;

		2.在reduce task开始执行之前会先让本进程中的MapOutputTrackerWorkerDriver进程中的MapoutPutTrakcerMaster发动请求,请求磁盘小文件位置信息;

		3.当所有的Map task执行完毕后,Driver进程中的MapOutPutTrackerMaster就掌握了所有的磁盘小文件的位置信息。此时MapOutPutTrackerMaster会告诉MapOutPutTrackerWorker磁盘小文件的位置信息;

		4.完成之前的操作之后,由BlockTransforServiceExecutor所在的节点拉数据,默认会启动五个子线程。每次拉取的数据量不能超过48M(reduce task每次最多拉取48M数据,将拉来的数据存储到Executor内存的20%内存中)。
8.1 简述一下 Spark 的shuffle
	Spark的shuffle总体而言就包括两个基本的过程:Shuffle write 和Shuffle read。
 	在划分Stage时,遇到shuffle类型的算子,即宽依赖时,会直接划分为一个stage,shuffle操作之前的一个stage称之为map task。shuffle操作后的一个stage称之为reduce task。

     其中map task负责数据的划分,对相同的key执行hash算法,从而将相同key都写入同一个磁盘文件中,而每一个磁盘文件都只属于下游stage的一个task。

     其中reduce task负责数据的聚合,当前stage的每一个task就需要将上一个stage的计算结果中属于自己的那一个磁盘文件,从各个节点上通过网络都拉取到自己所在的节点上,然后进行key的聚合或连接等操作。
8.1 HashShuffle解析
未经优化的HashShuffleManager
	shuffle write阶段,将每个task处理的数据按key进行“划分”。对相同的key执行hash算法,从而将相同key都写入同一个磁盘文件中,而每一个磁盘文件都只属于下游stage的一个task。在将数据写入磁盘之前,会先将数据写入内存缓冲中,当内存缓冲填满之后,才会溢写到磁盘文件中去。

	"下一个stage的task有多少个,当前stage的每个task就要创建多少份磁盘文件"。比如下一个stage总共有100个task,那么当前stage的每个task都要创建100份磁盘文件。如果当前stage有50个task,所有Executor上会创建5000个磁盘文件。"未经优化的shuffle write操作所产生的磁盘文件的数量是极其惊人的"。

	shuffle read阶段,此时"当前stage的每一个task就需要将上一个stage的计算结果中属于自己的那一个磁盘文件,从各个节点上通过网络都拉取到自己所在的节点上,然后进行key的聚合或连接等操作"。

	shuffle read的"拉取过程是一边拉取一边进行聚合的"。每个shuffle read task都会有一个自己的buffer缓冲,"每次都只能拉取与buffer缓冲相同大小的数据,然后通过“内存中的一个Map进行聚合”等操作"。聚合完一批数据后,再拉取下一批数据,并放到buffer缓冲中进行聚合操作。以此类推,直到最后将所有数据到拉取完,并得到最终的结果。

在这里插入图片描述

优化后的HashShuffleManager

	为了优化HashShuffleManager我们可以设置一个参数,spark.shuffle.consolidateFiles,该参数默认值为false,将其设置为true即可开启优化机制。如果我们使用HashShuffleManager,那么都建议开启这个选项。
	"开启consolidate机制"之后,此时会出现shuffleFileGroup的概念。每个"shuffleFileGroup会对应一批磁盘文件,磁盘文件的数量与下游stage的task数量是相同的"。第一批并行执行的每个task都会创建一个shuffleFileGroup,并将数据写入对应的磁盘文件内。下一批task就会复用之前已有的shuffleFileGroup,包括其中的磁盘文件,将数据写入已有的磁盘文件中,而不会写入新的磁盘文件中。
	"consolidate机制允许不同批次task复用同一批磁盘文件,一定程度上将多个task的磁盘文件进行合并,从而大幅度减少磁盘文件的数量,进而提升shuffle write的性能"。
	假设每个Executor1个CPU core,并且一个Executor上有多少个CPU core,就可以并行执行多少个task。假设第二个stage有100个task,总共有10Executor,则第一批总共并行10个task,每个Executor此时只会创建100个磁盘文件,所有Executor只会创建1000个磁盘文件。
8.2 SortShuffle解析(spark 默认使用SortShuffle)
	SortShuffleManager的运行机制主要分成两种,一种是普通运行机制,另一种是bypass运行机制。
	当shuffle read task的数量小于等于spark.shuffle.sort.bypassMergeThreshold参数的值时(默认为200),且不是聚合类的shuffle算子(比如reduceByKey),就会启用bypass机制。
1、普通运行机制
	在该模式下,数据会先写入一个内存数据结构中。聚合算子写入Map,一边通过Map进行聚合,一边写入内存;如果是join这种普通的shuffle算子,那么会选用Array数据结构,直接写入内存。
	每写一条数据进入内存数据结构之后,就会判断一下,是否达到了某个临界阈值。如果达到临界阈值的话,那么就会尝试将内存数据结构中的数据溢写到磁盘,然后清空内存数据结构。

	在溢写到磁盘文件之前,会先根据key对内存数据结构中已有的数据进行排序。排序过后,会分批将数据写入磁盘文件。默认的batch数量是10000条,也就是说,排序好的数据,会以每批1万条数据的形式分批写入磁盘文件。写入磁盘文件是通过JavaBufferedOutputStream实现的。BufferedOutputStreamJava的缓冲输出流,首先会将数据缓冲在内存中,当内存缓冲满溢之后再一次写入磁盘文件中,这样可以减少磁盘IO次数,提升性能。

	一个task将所有数据写出内存数据结构的过程中,会发生多次磁盘溢写操作,也就会产生多个临时文件。最后会将之前所有的临时磁盘文件都进行合并,"最后一个task就只对应一个磁盘文件,该task为下游stage的task准备的数据都在这一个文件中",因此还会单独写一份索引文件,其中标识了下游各个task的数据在文件中的start offset与end offset。
	比如第一个stage有50个task,最终只会产生50个磁盘文件。

在这里插入图片描述

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

	每个task会为每个下游task都创建一个临时磁盘文件,对相同的key执行hash算法,从而将相同key都写入同一个磁盘文件中。当然,写入磁盘文件时也是先写入内存缓冲,缓冲写满之后再溢写到磁盘文件的。"最后,同样会将所有临时磁盘文件都合并成一个磁盘文件,并创建一个单独的索引文件。

	该过程的磁盘写机制其实跟未经优化的HashShuffleManager是一模一样的,因为都要创建数量惊人的磁盘文件,只是在最后会做一个磁盘文件的合并而已。

	而该机制与普通SortShuffleManager运行机制的不同在于:第一,磁盘写机制不同;第二,不会进行排序。也就是说,启用该机制的最大好处在于,shuffle write过程中,不需要进行数据的排序操作,也就节省掉了这部分的性能开销。
九、Spark 内存管理
在执行spark应用程序时,spark集群会启动 DriverExecutor 两种 jvm 进程。
1Driver: 为运行Spark 应用程序的main() 函数和创建SparkContext 的进程。
由SparkContext 负责与Cluster Manager 进行通信,进行资源的申请,任务的分配和监控;当Executor 部分运行完毕后,Drive 负责将SparkContext 关闭。  

	后者负责在工作节点上执行具体的计算任务,并将结果返回给Driver; 同时为需要的持久化的RDD提供存储功能。
堆内和堆外内存的规划
Spark 对于JVM 堆内(on-heap)空间进行了更为详细的分配。同时Spark 引入了堆外(off-heap)内存,使之可以直接在工作节点的系统内存中开辟空间。
	堆内(on-heap)内存直接受到JVM的统一管理,堆外(off-heap)是直接向操作系统进行内存的申请和释放。
9.1 堆内内存
	堆内内存的大小,由Spark 应用程序启动时的 executor-memory 或者 spark.executor.memory 参数配置。Executor 内运行的并发任务(task)共享JVM堆内内存。堆内内存分为三部分:存储内存,执行内存,剩余内存,三部分在不同的管理模型下所占空间大小各不相同。

当缓存RDD数据和广播数据时占用的内存被规划为“存储内存”,
	当执行Shuffle时占用的内存被规划为“执行内存”,
	剩余的部分内存不做特殊处理,一般是Spark 内部的对象实例占用这一部分剩余内存。
Spark “规划式”的管理。因为对象所占用内存的申请和释放都是由JVM 来管理的,Spark只能在申请后和释放前记录这些内存。

内存申请流程:
		1.Spark 在代码中new 一个对象实例;
		2.JVM从堆内内存中分配空间,创建对象并返回对象的引用;
		3.Spark 保留该对象的引用,记录该对象占用的内存。
	内存的释放流程:
		1.Spark 记录该对象释放的内存,删除该对象的引用;
		2.等待JVM的垃圾回收机制释放该对象的堆内内存。

	我们知道,JVM 的对象可以以序列化的方式存储,序列化的过程是将对象转换为二进制字节流,本质上可以理解为将非连续空间的链式存储转化为连续空间或块存储,在访问时则需要进行序列化的逆过程——反序列化,将字节流转化为对象,序列化的方式可以节省存储空间,但增加了存储和读取时候的计算开销。


	对于 Spark 中序列化的对象,由于是字节流的形式,其占用的内存大小可直接计算,而对于非序列化的对象,其占用的内存是"通过周期性地采样近似估算而得",即并不是每次新增的数据项都会计算一次占用的内存大小,这种方法降低了时间开销但是有可能误差较大,在被Spark标记为释放对象的实例,很有可能在实际上并没有被JVM 回收,导致某一时刻的实际可用的内存有可能远远低于预期。

	此外,在被Spark标记为释放对象的实例,很有可能在实际上并没有被JVM 回收,导致实际可有的内存要小于Spark 中记录的可用内存。所以,Spark 并不能准确的记录实际可用的堆内内存,因此也就无法完全避免OOM的异常。        
9.2 堆外内存
	"为了进一步优化内存的使用以及提高Shuffle 时排序的效率,Spark 引入了堆外内存"。堆外内存意味着把内存对象分配在Java虚拟机的堆以外的内存,"这些内存直接受操作系统管理"(而不是虚拟机)。这样做的结果就是能保持一个较小的堆,以减少垃圾收集对应用的影响。

	利用JDK Unsafe API Spark 可以直接操作系统的堆外内存,减少了不必要的内存开销,以及频繁的GC 扫描和回收。
"堆外内存可以被精准申请和释放",原因是由于内存的申请和释放不在通过JVM机制,而是直接面向操作系统申请。而且序列化的数据占用的空间可以被精确的计算,所以相比于堆内内存而言,降低了管理的难度和误差。

	默认情况下堆外内存并不启用,可以通过配置spark.memory.offheap.enabled 参数启动,并通过spark.memory.offheap.size 参数设置堆外空间的大小。

相比于堆内内存,堆外内存没有剩余内存。除此之外堆外内存和堆内内存的划分方式相同。所有运行中的并发任务共享存储内存和执行内存。
9.3 静态内存管理机制
	Spark 最初使用的是静态内存管理机制,存储内存、执行内存、其他内存在Spark 应用程序运行期间的大小是固定的。但是用户可以在应用程序启动时进行配置。
1、堆内内存:
	可用的存储内存=systemMaxMemory * system.storage.memoryFaction * spark.storage.safety Fraction
	可用的执行内存=systemMaxMemory * spark.shuffle.memoryFraction * spark.shuffle.safety Fraction 

参数解释:systemMaxMemory 取决于当前 JVM 堆内内存的大小,可用的执行内存和存储内存需要在此基础上乘以memoryFaction 和 safetyFraction。
	之所以需要乘以safetyFraction参数,"因为Spark 堆内内存大小的记录是不准确的,需要预留一个逻辑上的保险区,以防止OOM风险"。注意这个预留的保险区域仅仅是一种逻辑上的规划,在具体使用时 Spark 并没有区别对待,和”其它内存”一样交给了 JVM 去管理。

  2、堆外内存:
	堆外内存只包含存储内存和执行内存,其可用的存储内存和执行内存可以通过spark.memory.storageFraction 参数直接指定。同时由于堆外内存占用的空间大小可以被精准计算,所以无需设定保险区域。  

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wWqjzcc8-1637937842689)(file:///C:\Users\86156\AppData\Roaming\Typora\ksohtml4704\wps9.jpg)]

在这里插入图片描述

9.4 统一内存管理
	Spark 1.6 之后引入了统一内存管理的机制,其与静态内存管理的最大的不同在于:"存储内存和执行内存共享同一块空间,可以动态的占用对方的空闲空间"。

统一内存管理中最重要的优化是动态占用机制:
	(1"设定基本的存储内存和执行内存区域"(由spark.storage.storageFraction参数控制,默认为0.5),该设置确定了双方各自拥有的空间范围
	(2)当双方的空间都不足时,则存储到硬盘。若己方的空间不足而对方空余时可以借用对方的空间。
	(3)当执行内存的空间被对方占用后,可以让对方将占用的部分数据转存到硬盘中,然后归还借用的空间
	(4)存储内存的空间被对方占用后,无法让对方“归还”,因为需要考虑 shuffle 过程中因素很多所以实现起来比较复杂。

	凭借统一内存管理机制,Spark 在一定程度上提高了堆内和堆外内存资源的利用率。但并不意味着开发者可以高枕无忧。如果存储内存的空间太大或者说缓存的数据过多,反而会导致频繁的全量垃圾回收,降低任务执行时的性能,因为缓存的 RDD 数据通常都是长期驻留内存的。
十、spark 的工作组件
10.1 driver的功能是什么?
	Spark 中的driver为运行Spark应用程序的main()函数和创建SparkContext的进程。
SparkContext作为Spark功能的主入口点,其目的是为Spark应用程序创建运行环境。同时 SparkContex负责与Cluster Manager进行通信,进行资源的申请,任务的分配和监控。
  当 Executor部分运行完毕后,Driver 负责将SparkContext 关闭。每个JVM 中只能存在一个处于活 跃状态的SparkContext,在创建新的SparkContext 之前必须调用Stop() 方法关闭之前 SparkContextExecutor   
它们负责运行组成 Spark 应用的任务,并将结果返回给驱动器进程;
它们通过自身的块管理器(Block Manager)为用户程序中要求缓存的 RDD 提供内存式存储   
10.2 spark中worker 的主要工作是什么?
	主要功能:管理当前节点内存、CPU的使用状况,接收master分配过来的资源指令,通过ExecutorRunner启动程序分配任务.
注意点:
	worker会不会汇报当前信息给master?worker心跳给master主要只有workid,不会以心跳的方式发送资源信息给master,这样master就知道worker是否存活,只有故障的时候才会发送资源信息;
	2.worker不会运行代码,具体运行的是executor    
``
十一、SparkStreaming
十二、Spark 本地化

在这里插入图片描述

spark 每个Executor单独运行在一个JVM进程中,每个Task则是运行在Executor中的一个线程。

HadoopMapReduceMap TaskReduce Task都是进程级别的。 
十三、Spark Join 的实现形式
1、Broadcast Join
Broadcast Hash Join:适合一张很小的表和一张大表进行Join。

broadcast hash join可以分为两步:
1.broadcast阶段:将小表广播到所有的executor上,广播的算法有很多,最简单的是先发给driver,driver再统一分发给所有的executor。
2.hash join阶段:在每个executor上执行 hash join,小表构建为hash table,大表的分区数据匹配hash table中的数据;


缺陷:
1、这个方案只能用于广播较小的表,否则数据的冗余传输就远大于shuffle的开销。
2、另外,广播时需要将被广播的表先collect到driver端,当频繁有广播出现时,对driver的内存也是一个考验。
2、shuffle hash join
Shuffle Hash Join:适合一张小表(比上一个大一点)和一张大表进行Join;

shuffle hash join分为两步:
	1、对两张表分别按照join keys进行分区,即shuffle,目的是为了让有相同join keys值的记录分到对应的分区中。

	2、对对应分区中的数据进行join,此处先将小表分区构造一张hash表,然后根据大表分区中记录的join keys值拿出来进行匹配。    
3、sort merge join
Sort Merge Join:适合两张大表进行Join
Sort Merge Join分为三步:
	1、shuffle阶段:对两张表分别按照join keys进行分区,即shuffle,目的是为了让有相同join keys值的记录分到对应的分区中。
	2、sort阶段:对单个分区节点的两表数据,分别进行排序;   
   3、merge阶段:对排好序的两张分区表数据执行join操作。从头遍历,碰到key相同的就输出;如果不同,左边小就继续取左边,反之取右边。   
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值