这里要总结Spark的相关基础知识咯
Spark是什么:
Spark是一种基于内存的,适用性好的,效率高的分布式计算引擎。
Spark的特点就是对任意类型的数据进行自定义计算,可以计算结构化,半结构化,非结构化等各种类型的数据结构,同时也支持使用Python,Java,Scala,R以及SQL语言去开发应用计算程序。
Hadoop和Spark的历史以及比较:
spark应该是和Hadoop中的MapReduce去比较,不能和Hadoop整体去比较,Hadoop整体是一个大数据生态圈,不只有MapReduce,还有Yarn以及HDFS等。
Spark五大特点:
1.运行速度快:
相比较Hadoop中的MapReduce来说,Spark在内存中的运算速度比它快100倍,在硬盘中要快10倍。
原因是有以下两点:
1.Spark处理数据时,可以将中间处理结果数据存储到内存中,也就是内存迭代计算。但是map和reduce的数据交互是通过HDFS的磁盘来进行交互的,map将数据写到HDFS的磁盘上,然后reduce再将数据从HDFS的磁盘中读出来,所以这个速度就会比spark慢很多。
2.Spark提供了非常丰富的算子(API),可以做到复杂任务在一个Spark程序中完成。但是hadoop的mapreduce中的算子只有两个,一个是map,一个是reduce,受限于这样的编程限制,很难在一个mapreduce程序中完成任务的计算,所以会串联好多的mapreduce程序,但是串联的mapreduce程序也是通过硬盘来进行传输的。
2.简单易用:
编程比较简单。
3.通用性强:
支持R语言,SQL,Python,Java,Scala等语言。
Spark还提供了SparkSQL+DataFrames,Streaming,MLlib,GraphX
SparkSQL:可以通过sql语言完成结构化数据的处理。
SparkStreaming:基于这个模块完成流式的数据处理(流式计算)。
StructureStreaming:以SparkSQL为基础,进行数据的流式计算。
MLlib Machine Learning:MLlib模块可以完成机器学习的数据计算。
GraphX Graph Computation:可以完成图计算。
4.可以在非常多地方运行:
Spark支持多种运行方式,包括在Hadoop的yarn上和Mesos资源管理调度框架之上,也支持Standalone的独立运行模式,Local模式,同时也可以运行在云Kubernetes(Spark2.3开始支持)上。
Local模式的本质就是启动一个JVM Process进程(一个进程里面有多个线程),执行任务Task。Local模式可以限制模拟Spark集群环境的线程数量,即Local[N]或者Local[*],这里面N就代表可以使用的线程数,每个线程拥有一个cpu core。如果不指定N,则默认就是1个线程。通常cpu有几个Core,就指定几个线程,最大化利用计算能力。如果是Local[*],则代表按照cpu最多的cores设置线程数。
Local下的角色分布:
资源层面:
Master:Local进程本身(总管家)
Worker:Local进程本身(单个服务器的管家)
但是在Local模式下都是一样的,单个服务器的管家就是总管家,因为只有一个进程。
任务执行:
Driver:Local进程本身
Executor:不存在,没有独立的Executor角色,由Local进程(也就是Driver)内的线程提供计算能力。
如果是Local[10]就是等于是启动10个线程来进行工作,也可以理解成模拟10台服务器来进行工作。
StandAlone模式是Spark自带的一种集群模式,不同于前面本地模式启动多个进程来模拟集群的环境,Standalone模式是真实地在多个机器之间搭建Spark集群的环境,完全可以利用该模式搭建多机器集群,用于实际的大数据处理。
StandAlone是完整的Spark运行环境,其中:Master角色以Master进程存在,Worker角色以Worker进程存在,Driver角色在运行的时候是存在于Master进程内的,Executor运行于Worker进程内。
Standalone集群在进程上主要有3类进程:
1.主节点Master进程:
Master角色,管理整个集群资源,并且托管运行各个任务的Driver。
2.从节点Workers进程:
Worker角色,管理每个机器的资源,并且分配对应的资源来运行Executor(Task)。每个从节点分配资源信息给Worker管理,资源信息包含内存Memory和CPU Cores核数。
3.历史服务器HistoryServer:
Spark Application运行完成以后,保存事件日志数据至HDFS,启动HistoryServer可以查看应用运行历史。
这个图里面是只有一个Spark程序的情况,如果再开启一个Spark程序的话,那么需要在Master进程中又开启了一个Driver。Local模式下,只要开启一个Spark程序,就会有一个独立的local进程。但是在standalone的模式下,一个Master进程,三个Worker进程的集群(或者说是资源)是固定的了,那么如果再开启一个Spark程序,那就只是再开启一个Driver而已,然后这个新的Driver也会对应很多的Executor。
Standalone集群下的端口:
4040:是一个运行的Application在运行的过程中临时绑定的端口,用以查看当前任务的状态,4040被占用会顺延到4041,4042等。4040是一个临时端口,当前程序运行完成后,4040就会被注销。一个程序所绑定的特定接口,也就是driver所对应的接口。
8080:默认是StandAlone下,Master角色(进程)的WEB端口,用以查看当前Master(集群)的状态。
18080:默认是历史服务器的端口,由于每个程序运行完成后,4040端口就被注销了,以后的时候如果想回看某个程序的运行状态就可以通过历史服务器查看,历史服务器长期稳定运行,可供随时查看被记录的程序的运行过程。
5.支持很多的数据源:
ES,Kafka,Redis,MySQL以及自定义数据源。
Spark框架模块:
Spark Core:Spark的核心,Spark核心功能均由Spark Core模块提供,是Spark运行的基础。Spark Core以RDD(分布式弹性数据集)为数据抽象,数据都是以RDD为载体来提供的。
Spark的架构角色:
资源层面:
Master:整个集群资源的管家。
Worker:单个服务器的管家。
任务运行层面:
Driver:单个任务的管理。
Executor:单个任务的计算执行。
正常情况下Executor是干活的角色,不过在特殊场景下(Local模式)Driver可以即管理又干活。
Job\State\Task的关系?
一个Spark程序会被分为多个子任务(Job)运行,每一个Job会分成多个State(阶段)来运行,每一个State内会分出来多个Task(线程)来执行具体任务。
如何解决Master单点故障问题?(StandAlone HA运行)
Master单点故障问题是所有Master—Slave架构集群,也就是主从架构集群都会存在的问题。
1.基于文件系统的单点恢复(Single-Node Recovery with Local File System)--只能用于开发或者测试环境。
2.基于zookeeper的Standby Masters(Standby Masters with ZooKeeper)--可以用于生产环境。
ZooKeeper提供了一个Leader Election机制,利用这个机制可以保证虽然集群存在多个Master,但是只有一个是Active的,其他的都是Standby。当Active的Master出现故障时,另外的一个Standby Master会被选举出来。由于集群的信息,包括Worker,Driver和Application的信息都已经持久化道文件系统,因此在切换的过程中只会影响新Job的提交,对于正在进行的Job没有任何的影响。加入Zookeeper的集群整体架构如下图所示。
如果主节点(已经注册的节点)挂掉了,那么就会从其他的standBy的master节点里面选择出一个来,在Zookeeper中去注册他,然后让他成为新的主节点。
StandAlone HA的原理:
基于Zookeeper做状态的维护,开启多个Master进程,一个作为活跃,其他的作为备份,当活跃的进程宕机的时候,备份Master进行接管。
SparkOnYarn模式:
Spark On Yarn的本质?
Master角色由Yarn的ResourceManager担任。
Worker角色由Yarn的NodeManager担任。
Driver角色运行在Yarn容器内部,或者 提交任务的客户端进程中。
真正干活的Executor运行在Yarn提供的容器内。
ApplicationMaster作为任务运行的老大并没有去代替Driver,而是和Driver共同存在于Yarn容器的内部。在StandAlone模式下,之所以需要Master和Worker是因为没有Yarn给他提供ResourceManager和NodeManager。
Spark On Yarn需要啥?
1.需要Yarn集群,已经安装好了。
2.需要Spark客户端工具,比如spark-submit,可以将Spark程序提交到YARN中。(不需要Spark集群)
3.需要有代码,我们自己开发的spark程序,spark任务。
部署模式DeployMode?
Spark On Yarn是有两种运行模式的,一种是Cluster模式一种是Client模式。
这两种模式的区别就是体现在Driver运行的位置。
Cluster模式:Driver运行在YARN容器内部和ApplicationMaster在同一个容器内。
Client模式:Driver运行在客户端进程中,比如Driver运行在spark-submit程序的进程中。
Cluster模式:
Client模式:
在这里Yarn集群需要和外部集群(Spark客户端工具)进行通信,会降低整个集群的效率,因为至少需要经过一个网关。相比较Cluster模式来说,通信效率比较低。但是有一个点是Cluster模式无法避免的,那就是集群日志的查看。因为日志在容器内部,所以如果要查询日志的话,还需要进入到容器内,但是Client模式中,Driver运行在客户端当中,日志输出在客户端的标准输出流中,方便查看。如果有什么信息需要查询的话,我们可以直接在终端中查询,也就是客户端spark-submit中。
两种模式详细流程:
Executor进程最后都会和Driver进行通信的,也就是Executor开始干活了。
SparkCore:
分布式计算需要:
分区控制,Shuffle控制,数据存储\序列化\发送,数据计算API等。 这些需要RDD去帮助完成。
什么是RDD:
RDD就是一个数据抽象对象——(Resilient Distributed Datasets)弹性分布式数据集,是Spark中最基本的数据抽象,代表一个不可变,可分区,里面的元素可并行计算的集合。
Dataset:一个数据集合,用来存放数据的。
Distributed:RDD中的数据是分布式存储的(RDD数据是跨机器存储的,跨进程存储的),可用于分布式计算。
Resilient:RDD中的数据可以存储在内存中或者磁盘中。
RDD的五大特性:
1. RDD是有分区的。(RDD的分区是RDD数据存储的最小单位,一份RDD的数据本质上是分隔成了多个分区)分区是一个真实存在的物理概念。
2.计算方法都会作用到每一个分片(分区)之上。
3.RDD之间是有相互依赖的关系的。
rdd1会产生rdd2,但是rdd2依赖rdd1。
rdd2会产生rdd3,但是rdd3依赖rdd2。
rdd3会产生rdd4,但是rdd4依赖rdd3。
所以说rdd之间是有依赖关系的,也可以说是有血缘关系的。
4.KV型RDD可以有分区器。(KV型RDD指的就是RDD内存储的数据是二元元祖)
5.RDD的分区规划,会尽量靠近数据所在的服务器。(因为这样可以走本地读取,避免网络读取)
Spark读取数据依靠的是rdd,也就是说初始rdd的规划是在读取数据的时候。那么rdd的分区规划,最好是按照数据的实际情况来安排,比如将分区安排到靠近数据所在的服务器,或者说就安排到数据所在的服务器最好。
以RDD的角度去看WordCount的过程如下:
这里面说的Spark的并行计算能力为3,代表RDD的分区数设置为3。
RDD编程:
程序执行入口SparkContext对象:
Spark RDD编程的程序入口对象是SparkContext对象(不论何种编程语言)
只有构建出SparkContext,基于它才能执行后续的API调用和计算。
SparkContext对编程来说,作用就是创建第一个RDD出来。
RDD两种创建方式:
1.通过并行化集合创建(本地集合 转 分布式RDD)
rdd = sparkcontext.parallelize(参数1,参数2)
参数1:集合对象即可,比如list
参数2:分区数量(默认是电脑的CPU核心数)
2.读取外部数据源(读取文件创建)
textFile API
这个API可以读取本地数据,也可以读取hdfs数据。
sparkcontext.textFile(参数1,参数2)
#参数1,必填,文件路径 支持本地文件 支持HDFS 也支持一些比如S3协议。
#参数2,可选,表示最小分区数量。
#注意:参数2 话语权不足,spark有自己的判断,在它允许的范围内,参数2有效果,超出spark允许的范围,参数2失效。
读取文件创建还有一种方式:
wholeTextFile,读取小文件专用的,可以读取一堆小文件。
用法:其实是和textFile是一样的。
读取出来的结果就是元组形式的,其中key代表文件路径,value代表文件内容,以list集合的形式返回,list集合里面每个对象都是一个元组。
使用map方法取出所有的value。
RDD算子:
什么是算子?
算子就是分布式集合上的API,本地对象的API,叫做方法or函数,分布式对象的API叫做算子。
RDD的算子可以分成两类:分别是Transformation算子和Action算子。
Transformation转换算子:RDD的算子,返回值仍旧是一个RDD的,称之为转换算子。
这类算子是Lazy 懒加载的,如果没有action算子,Transformation算子是不工作的。这里起始可以理解为,Transaction算子其实是规定好了一个个的流程,一个个的步骤,但是他是不工作的,只是计划好了工作的过程,然后当action算子调用的时候,才会开启这个流程,触发整个工作的过程,也就是说action算子可以理解成开关,遥控器。
Action动作算子:返回值不是rdd的就是action算子,比如rdd调用的collect方法。
常用Transformation算子:
这里介绍map算子,flatMap算子和reduceByKey算子。
map算子:将RDD的数据一条条处理(按照我们规定的方式),返回新的RDD。
用map方法举个例子:所以其实单纯说map方法的作用,就只是将RDD里面的数据一条一条都拿出来,然后一条一条的处理,但是这个处理方法是按照我们规定的逻辑去执行的,让里面的每一条数据都执行我们规定的逻辑。
flatMap算子:也是将RDD里面的数据一条条的拿出来,然后也是进行我们提前设定好逻辑的操作,到这里完成这步骤的时候,其实就是已经达到了map的功能,但是flatMap还比map多一个功能,那就是解除嵌套。
reduceByKey算子:针对kv型RDD,自动按照key分组,然后根据我们自己设定提供的聚合逻辑,完成组内数据(value)的聚合操作。(一共是两个步骤,一是分组,二是聚合)
代码示例如下:分组自动按照key分组,聚合的操作逻辑自己设计,然后返回聚合操作逻辑之后的结果。
mapValues算子:针对二元元组RDD,对其内部的二元元组的Value执行map操作。
重新总结WordCount的实现方法:
hello spark
hello hadoop
hello flink
对以上这个小文件的单词进行计数。
1.先通过flatMap,将其变成[hello spark hello hadoop hello flink]的形式。
2.再通过map,将其变成[(hello,1),(spark,1),(hello,1),(hadoop,1),(hello,1),(flink,1)]。
3.再通过reduceByKey,将其变成(hello,3),(spark,1),(hadoop,1),(flink,1)。
4.再通过collect()算子输出。
groupBy算子:将rdd的数据进行分组
代码示例如下:
groupBy的执行结果是根据传入参数来进行分组,确定按照传入的参数进行分组,然后返回的结果就是一个二元元祖 (传入的参数也就是分组标准,然后二元元祖v的部分就是一个迭代器对象)。
Filter算子:过滤想要的数据进行保留。
distinct算子:对RDD数据进行去重,返回新的RDD。
参数1: 去重分区数量,一般不用传。
union算子:2个rdd合并成1个rdd并且返回。(union算子是不会去重的,rdd的类型不同也是可以合并的)
join算子:对两个rdd执行join操作(可实现sql的内外连接,但是join算子只能用于二元元组)
rdd.join(other_rdd) #内连接
rdd.leftOuterJoin(other_rdd) #左外
rdd.rightOuterJoin(other_rdd)#右外
join算子关联的时候,on的条件其实默认是key。
intersection算子:
功能:求2个rdd的交集,返回一个新rdd。
glom算子:将RDD的数据,加上嵌套,这个嵌套按照分区来进行。
groupByKey算子:针对kv型RDD,自动按照key分组。和groupBy不太一样。groupBy是整个把kv都放在v的位置体现出来,但是groupByKey只是将Value的值放在集合里面在V的位置。(可参考对比)
sortBy算子:对RDD数据进行排序,基于我们指定的排序依据。
sortByKey算子:其实和sortBy类似,但是sortByKey自动按照key去排序,sortBy可以设置排序方法。
常用Action算子:
action算子的返回值不是rdd
countByKey:根据key进行计数。
collect算子:将RDD各个分区内的数据,统一收集到Driver中,形成一个List对象。
这个算子可以将所有分区的数据都拉取到Driver中,RDD是分布式对象,所有的分区加起来可能会导致数据量特别大,所以用这个算子之前要了解数据的大小,否则会导致Driver内存爆炸。
reduce算子:对RDD数据集按照传入的逻辑进行聚合。
rdd = sc.parallelize(range(1,10))
#将rdd的数据进行累加求和
print(rdd.reduce(lambda a,b: a+b))
fold算子:和reduce一样,接受设定的逻辑进行聚合,聚合是带有初始值的。
先是分区内聚合,然后是分区间聚合。
first算子:取出RDD的第一个元素。
sc.parallelize([3,2,1]).first() //3
take算子:取出RDD的前n个元素。
sc.parallelize([3,2,1,4,5,6]).take(5) //[3,2,1,4,5]
top算子:对RDD数据集进行降序排序,取前n个。
sc.parallelize([3,2,1,4,5,6]).top(3) //[6,5,4]
count算子:计算RDD有多少条数据,返回值是一个数字。
sc.parallelize([3,2,1,4,5,6]).count() //6
takeSample算子:随机抽样RDD的数据。
takeSample(参数1:True or False,参数2:采样数,参数3:随机数种子)
参数1:True表示运行取同一个数据,False表示不允许取同一个数据。
参数2:抽样要几个。
参数3:随机数种子,这个参数随机传入一个数字即可。(一般参数3不需要传入,spark会自动生成一个数字的)
不会取出相同的数字,也就是意味着第二个参数设置为false,但是这里指的是不会重复取出同一个位置的数字两次,不代表数值一定不重复。
takeOrdered算子:对RDD进行排序取出前n个(和top进行区分,top只能是进行降序排序,然后取出前n个靠前的,但是takeOrdered可以选择正序或者降序)
rdd.takeOrdered(参数1,参数2)
-参数1 要几个数据
-参数2 对排序的数据进行更改(不会更改数据本身,只是在排序的时候换个样子)
这个方法使用安装元素自然顺序升序排序,如果你想玩倒序,需要采用参数2,来对排序的数据进行反转处理。
foreach算子:对RDD的每一个元素,执行我们提供的逻辑的操作(和map是一个意思),但是这个方法没有返回值。
foreach的执行是经由executor直接输出的,不需要有Driver汇总再执行。
saveAsTextFile算子:将RDD的数据写入文本文件中。
action中:foreach 和 saveAsTextFile这两个算子是分区(Executor)直接执行的。跳过Driver,由分区所在的Executor直接执行。其他的Action算子都是将结果发送至Driver。
mapPartitions算子:和map相似,但是操作过程不同。
map一次操作一条数据。
mapPartitions一次操作一个分区的数据。
foreachPartition算子:和普通的foreach一致,一次处理的是一整个分区数据。
partitionBy算子:对RDD进行自定义分区操作。
rdd.partitionBy(参数1,参数2)
参数1:重新分区后有几个分区。
参数2:自定义分区规则,函数传入。
repartition算子:对RDD的分区执行重新分区(仅数量变化,不对分区的规则进行改变)
coalesce算子:修改分区(但是有一个保护机制,减少分区可以,但是增加分区必须设定shuffle=true才可以)
算子名称 | 算子的功能 | 算子类型 |
map | 对RDD中的每一条数据都进行规定方式的处理 | transformation |
flatMap | 和map功能差不多,但是会多一个解除嵌套的功能 | transformation |
reduceByKey | 针对于kv型RDD,根据key进行分组,然后让value执行我们所设定好的逻辑 | transformation |
mapValues | 针对于kv型RDD,对RDD中的每一条数据的Value都进行规定方式的处理。 | transformation |
groupby | 将RDD数据按照传入的参数分组,返回结果也是二元元组,key部分是分组的标准,value部分是一个迭代器对象装着分组的对象。 | transformation |
filter | 过滤想要的数据进行保留 | transformation |
distinct | 对RDD数据进行去重,返回新的RDD | transformation |
union | 2个RDD合并成一个RDD并且返回 | transformation |
join | 对两个RDD执行join操作,包括join内连接,leftOuterJoin左外连接,rightOuterJoin右外连接,join算子在进行关联的时候,on的条件其实默认是key。 | transformation |
intersection | 求2个rdd的交集,返回一个新的rdd。 | transformation |
glom | 将RDD的数据加上嵌套,这个嵌套按照分区来进行。 | transformation |
groupByKey | 针对kv型RDD,自动按照key进行分组,然后将分组后同一组的Value都放在一个迭代器对象中(这里是和groupBy算子不一样的地方) | transformation |
sortBy | 对RDD数据进行排序,基于我们指定的排序依据。一共是有三个参数(1.排序方式的指定,2.ascending 设置为true还是false,决定是正序还是倒序,3.numPartitions这里是设定分区数量,如果设定为1,那么就是全局排序,如果设定为n,那么就是按照n个分区来进行排序) | transformation |
sortByKey | 其实和sortBy类似,但是sortByKey自动按照key去排序,也就是只需要两个参数就够了。 | transformation |
countByKey | 根据key进行计数 | action |
collect | 将RDD各个分区内的数据,统一收集到Driver中,形成一个List对象。(将所有数据都拉到Driver中,前提是了解这些分区加起来有多少的数据量,如果特别大会导致内存爆炸的。) | action |
reduce | 对RDD数据集按照传入的逻辑进行聚合。 | action |
fold | 和reduce一样,接受设定的逻辑进行聚合,聚合是带有初始值的,最后会把初始值也一起算进去的。(先是分区内聚合,然后是分区间聚合) | action |
first | 取出RDD中的第一个元素 | action |
take | 取出RDD中的前n个元素 | action |
top | 对RDD数据集进行降序排序,然后取出前n个。 | action |
count | 计算RDD有多少条数据,返回值是一个数字。 | action |
takeSample | 随机抽样RDD的数据,一共有3个参数: (1.True or False 表示是否同意运行取同一个数据。2.抽样要几个数据。3.随机数种子(一般不需要传入)) | action |
takeOrdered | 默认是升序排序,可以通过设置参数2实现降序排序。 参数1:要几个数据 参数2:对排序的数据进行更改(不会更改数据本身,只是在排序的时候换个样子) | action |
foreach | 对RDD算子中的每一个元素,执行我们提供的逻辑操作。(foreach时经由executor直接输出的,不需要有Driver汇总再执行) | action |
saveAsTextFile | 将RDD的数据写入文本文件中。(直接由Executor执行后输出,不会经过Driver) | action |
mapPartitions | 和map的功能完全一样,只不过是一次操作一个分区的数据,map是一次操作一条数据。(有返回值) | action |
foreachPartition | 和普通的foreach一样,不过一次处理一个分区的数据。(没有返回值) | action |
partitionBy | 对RDD进行自定义分区操作,两个参数: 1.重新分区后有几个分区 2.自定义分区规则,函数传入 | action |
repartition | 对RDD的分区执行重新分区(仅数量变化,不对分区的规则进行改变) | action |
coalesce | 修改分区(但是有一个保护机制,减少分区可以,增加分区必须设定shuffle=true才可以),这个函数有两个参数。 | action |
面试题 groupByKey和reduceByKey的区别是什么:
groupByKey是先分组,再聚合,但是这样就会导致在shuffle阶段,需要处理很多数据。
reduceByKey是在shuffle之前,先把聚合做了一部分了(预聚合),然后因为先预聚合了,所以被shuffle的数据可以极大的减少。因此提升了性能。
面试题 哪两个Action算子的结果不经过Driver,直接输出:
foreach 和 saveAsTextFile直接由Executor执行后输出
不会将结果发送到Driver上去。
面试题 对于分区操作有什么要注意的地方:
尽量不要增加分区,可能会破坏内存迭代的计算管道。(Spark内存模型会涉及到)一般用全局的并行度去设计就可以了。
RDD的持久化:
RDD的数据是过程数据:
RDD之间进行相互迭代计算(Transformation的转换),当执行开始后,新的RDD生成,代表老的RDD的消失。所以RDD是过程数据,因为当新的RDD生成之后,老的RDD也不需要存在了。那么Spark是内存迭代的框架,所以原则就是对于内存,能省则省,老的RDD及时在内存中淘汰。
问题的产生:这里面rdd1到rdd3其实是执行了两次。所以需要RDD持久化。
RDD缓存:
对于上面的场景,肯定是要优化的,因为执行两次也是比较浪费资源的事情。
RDD是有对应的缓存技术的,比如Spark提供了缓存API,通过调用API就可以将指定的RDD数据保留在内存或者硬盘上面。
这里回去查看视频,看DAG图即可。将rdd3缓存之后,在DAG图里面那里会显示出一个小绿点,代表只执行小绿点之后的部分,前面的部分不需要重复执行了。
当执行RDD3.cache()的时候,代表将RDD3的实体数据存储到了多个机器的内存或者硬盘(看是选择memory还是disk)当中,这个叫做分散存储(因为RDD数据是有不同分区的)。但是这样的存储并不被认为是安全的,因为存在内存当中,就有可能出现宕机or断电or内存不足被清理掉等情况;在硬盘中,就有可能出现硬盘损坏的情况(虽然很少)。总之都是被认为不安全的。那么如果一旦丢失了,解决办法就是需要进行重新计算,重新计算RDD3的缓存。如果想要重新计算,就一定要保留住被缓存RDD的“前置”血缘关系。
总结:
缓存的特点:
1.缓存的数据在设计上是被认为有丢失风险的(在内存中,有可能因为宕机,断电,以及内存不足等情况导致被清理掉为计算让路,在硬盘中因为硬盘的损坏也是有可能丢失的)。
2.缓存保留被缓存RDD的血缘(依赖)关系,一旦缓存丢失,可以基于血缘关系的纪录,重新计算被缓存的RDD数据。
3.缓存是分散存储的。
RDD CheckPoint:
CheckPoint技术,也是将RDD的数据,保存起来。但是和缓存技术不一样,它仅仅支持硬盘存储。
并且:它被设计认为是安全的,不保留血缘关系。(缓存是保留血缘关系的)
缓存与CheckPoint相比较:
1.CheckPoint不管分区数量多少,风险是一样的(因为都是将各个分区的数据收集起来,然后存储到了HDFS磁盘当中),缓存分区越多,风险越高(因为缓存RDD数据的时候,是每个分区分散存储的,如果丢失了其中的任意一个,就等于重新要进行缓存计算了,所以相对来说风险更高)。
2.CheckPoint支持写入HDFS(HDFS是高可靠存储),缓存不行缓存只能写到本地机器中(本地硬盘),HDFS是高可靠存储,CheckPoint被认为是安全的。
3.CheckPoint不支持内存,缓存支持内存,缓存如果写内存,性能比CheckPoint要好一点。(而且缓存如果写入硬盘的话,是相当于并发的写,因为他们每个分区是写入各自的硬盘,那么CheckPoint写入硬盘的话,是写入同一个硬盘,不能同时进行写操作)
4.CheckPoint的设计被认为是安全的,所以CheckPoint是不保留血缘关系的,但是缓存在设计上是不认为安全的,所以保留血缘关系。
比较结果总结:1.CheckPoint风险低,2.CheckPoint安全,3.缓存性能高,4.缓存保留血缘关系,CheckPoint不保存。Cache是轻量化保存RDD数据,CheckPoint是重量级保存RDD数据。
CheckPoint的DAG:
广播变量:
stu_info_list这个东西是在Driver里面的,因为是本地Python集合List。
上面有个问题就是,Executor是一个进程,进程里面的资源是共享的,不需要给两份,给一份就可以了其实,这样浪费了内存和网络IO。下面这个图更加清晰表现出,如何资源浪费的。
下面是广播变量的工作逻辑和流程:
如何将本地变量(存在Driver中的),标记成广播变量:
放进去再取出来,就是broadcast对象了。
场景:本地集合对象 和 分布式集合对象(RDD)进行关联的时候。
如果是RDD和RDD之间进行关联呢?
那他们之间进行关联就是需要Join算子,那么就会出现shuffle。
如果本地集合太大,那还是分布式与分布式关联比较好。
如果本地集合不大,那还是广播变量这种比较好。
累加器:
首先:1.按照分区分开计数的。2.最后count的返回结果居然是0。
count这个变量的定义是在本地集合中的,然后count进行累加的操作是在分区中进行的,也就是分布式中进行的。那么本地集合的count根本不会知道count进行了加的操作,更不会记录下来这个操作。
调用accumulator方法,来创造出一个累加器对象,然后进行累加操作,这样的话就不会出现之前的问题了。
累加器使用的注意事项,如果过程中的rdd数据被清空了,但是累加值依然被记载的,那么可能会导致重复累加。如下:
需要用cache缓存来解决(cache的执行必须在action算子之前,否则action算子执行完了就把rdd清空了。)
面试题:广播变量解决了什么问题?
分布式集合RDD和本地集合进行关联使用的时候,降低内存占用以及减少网络IO传输,提高性能。因为如果没有广播变量,那么可能Driver中的本地集合会单独发送给每个分区,那么这样就提高了网络IO的次数,也会占用更多Executor中的内存(因为一个Executor中有着多个分区)。但是其实没必要这样的,因为一个Executor是一个进程,那么在一个进程当中资源是共享的,所以其实一个Executor只要一份本地集合的内容就可以了。广播变量可以帮助我们做到这个事情:
1.broadcast = sc.broadcast(list);
2.value = broadcast.value;
放进去,然后取出来的这个对象就已经是被包装成广播对象的对象了。然后就可以进行通信传输了。
面试题:累加器解决了什么问题?
分布式代码执行中,进行全局累加,会出现一个问题,那就是每个分区中自己累加自己的,然后Driver中的本地变量count,并不知道你进行了累加操作。累加器解决了这个问题:
1.acmlt = sc.accumulator(0);//初始化的数值
这个可以解决累加次数不共通的问题。
Spark内核调度:
DAG:
DAG:有向无环图
有向:有方向。
无环:没有闭环。
DAG:有方向没有形成闭环的一个执行流程图。
三条流程图:
一个Action会产生一个Job(一个应用程序的子任务),每一个Job有自己的DAG图,上图可以看作是三个DAG图。
一个Action = 一个DAG = 一个JOB
如果一个代码中,写了3个Action,那么这个代码运行起来,产生3个JOB,每个JOB有自己的DAG。一个代码运行起来,在Spark中称之为:Application。
DAG和分区之间的关系:
只有在reduceByKey那里才会出现shuffle。
DAG的宽窄依赖和阶段划分:
RDD前后之间的关系可以分为:
窄依赖,宽依赖。
窄依赖的意思就是,父RDD的一个分区,将数据全部都发给了子RDD的一个分区。
宽依赖的意思就是,父RDD的一个分区,将数据发给了子RDD的多个分区。
最简单的就是看DAG的图,如果DAG图里面父RDD的右侧出现了分叉,那么就是宽依赖。
依赖划分的作用就是:对DAG进行阶段划分
基于宽依赖,将DAG划分成了2个stage,在stage的内部,一定都是:窄依赖。
内存迭代计算:
在Spark中,task就是线程的概念,是具体的工作线程,一个task就是一个线程。
上图中的阶段一,有3个独立的“内存计算管道”,由3个线程并行工作。
task1,task2,task3是三个并行的内存计算管道。
Spark默认会受到全局并行度的限制,除了个别算子有特殊分区情况,大部分的算子,都会遵循全局并行度的要求,来规划自己的分区数,如果全部并行度是3,大部分算子的分区都是3。
Spark推荐只设置全局并行度,不要再在算子上设置并行度,因为这样会改变内存计算管道的数量和情况。
面试题:Spark是怎么做内存计算的?DAG的作用?Stage阶段划分的作用?
Spark应用程序的运行会产生DAG有向无环图。那么DAG会根据分区和宽窄依赖划分出不同的阶段(stage)。一个阶段(stage)里面都是窄依赖的,阶段和阶段之间的连接是宽依赖。一个阶段里面会划分成不同的task,一个task对应一个分区,在一个task内是一个线程在工作,在进行内存计算。这样的一个task对应的是一个内存计算管道。stage划分的作用就是,让其在一个stage内进行的是按分区划分的内存计算,因为内存计算不会涉及到网络IO,也就是shuffle,提高了整个应用程序的计算效率。Spark有自己的全局并行度,大部分的算子会受到全局并行度的限制,遵守全局并行度的要求。
面试题:Spark为什么比MapReduce快?
1.首先是Spark的算子非常丰富,MapReduce算子匮乏(Map和Reduce),MapReduce这个编程模型,很难在一套MR中处理复杂的任务,因为他只有俩算子,一个是map,一个是reduce。但是Spark是有两类算子的,一个是Transformation算子,一个是Action算子。
2.Spark基于内存进行迭代计算,算子之间形成DAG,DAG又根据分区和宽窄依赖划分出来阶段,那么阶段内还会形成内存计算管道。一个内存计算管道就是一个线程,也是一个task,进行着内部的内存计算。但是MapReduce的Map和Reduce之间的交互是通过硬盘来交互的。
Spark并行度:
Spark的并行:在同一时间内,有多少个task在同时运行。
如果设置全局并行度为3,那么就会产生3个分区,3个task,3个线程,3个内存计算管道。
所以是先设置全局并行度,然后才出现的后面的概念。
全局并行度的配置参数:
spark.default.parallelism
三种设置方法:
1. 配置文件中:
conf/spark-default.conf中设置
spark.default.parallelism 100
2.在客户端提交参数中:
bin/spark-submit --conf "spark.default.parallelism=100"
3.在代码中设置:
conf = SparkConf()
conf.set("spark.default.parallelism","100");
全局并行度是推荐设置,不要针对RDD改分区,可能会影响内存计算管道的构建,产生不必要的shuffle。
集群中推荐设置全局并行度为集群CPU总核心数的2-10倍。
原因:因为这样可以保证没有CPU核心是空闲的,100个在工作,有700个在等待,处理完手上的活儿,立马就接别的活儿,这样可以最大程度的利用集群的资源。
Spark的任务调度:
Spark的任务,由Driver进行调度,这个工作包含以下四个内容:
1.逻辑DAG的产生
2.分区DAG的产生
3.Task划分
4.将Task分配给Executor并监控其工作。
Driver中有两个非常重要的组件:
1.DAG调度器:基于初始的DAG图进行处理,得到逻辑上的Task划分(带分区的DAG,并且规定几个task,task之间如何交互等信息)。
2.Task调度器: 基于DAG Scheduler的产出,来规划这些逻辑的task,应该在哪些物理的Executor上运行,以及监控管理他们的运行情况。
同一个机器内不同的Executor之间的task进行通信,也是要通过网络IO的,不过是本地回环网络。
不同机器内的不同Executor之间的task进行通信,是要通过交换机的,相比较本地回环网络来说,更慢,因为不是同一个机器内。所以其实一个机器开一个Executor比较好。
Spark运行中的概念名词:
名称 | 意义 |
Application | 用户代码被提交上去成为应用程序 |
Application jar | Java程序编写的应用程序 |
Driver program | 程序运行的调度者和管理者,可以创建程序运行入口SparkContext。 |
Cluster manager | 管理集群资源,就是整个集群的管理者(yarn模式下,这个由ResourceManager代替) |
Deploy mode | yarn模式下,选择客户端模式or集群模式 |
Worker node | worker角色,就是一个机器的资源管理者。(yarn模式下,这个由nodeManager代替) |
Executor | 程序的运行启动器,内部很多task。 |
Task | 运行在Executor内,最小的工作单元,一个线程一个task。 |
Job | 一个Action对应一个Job。(Job归属于Application,可以划分为不同的阶段) |
Stage | Stage依赖宽窄依赖进行划分 |
SparkSQL:
RDD可以处理 结构化数据,半结构化数据,非结构化数据。
SparkSQL只能处理 结构化数据。(Spark兼容Hive)
SparkSQL和Hive的相同点:
都是分布式SQL计算引擎,都是用来处理大规模结构化数据的,都可以运行在Yarn上。
SparkSQL和Hive的不同点:
SparkSql是基于内存迭代的,底层运行Spark RDD。
Hive是基于磁盘迭代的,底层运行MapReduce。
SparkSql没有元数据管理服务。
Hive有Metastore元数据管理服务。
SparkSql可以混合Sql和代码运行。
Hive只能运行Sql。
SparkSql的数据抽象:
三类数据抽象:
1.SchemaRDD对象(已废弃)
2.DataSet对象(Java,Scala)
3.DataFrame(Java,Scala,Python,R)
SparkSession对象:
在RDD阶段程序执行的入口对象是SparkContext。
在Spark2.0以后推出了SparkSession对象作为Spark编码的统一入口对象。
SparkCore编程也可以通过SparkSession,获取到SparkContext对象。
DataFrame的组成:
DataFrame是一个二维表结构,有以下三个点:
1.行
2.列
3.表结构描述(列,列名,列类型,列约束等)
在结构层面:
StructType对象描述整个DataFrame的表结构
StructField对象描述一个列的信息
在数据层面:
-Row对象记录一行数据
-Column对象记录一列数据并包含列的信息
第一种情况:是dataFrame的创建,关于schema的创建是比较死的。
这里的阶段打错了,应该是截断。
RDD可以转化成DataFrame的类型。
第二种情况:schema的定义非常灵活,通过StructType()对象来操作。
第三种情况:直接通过rdd.toDF去转换成DataFrame。
第四种情况:基于Pandas的DataFrame构建SparkSql的DataFrame对象。