文章目录
1. RDD(Resilient Distributed Dataset)弹性分布式数据集
哪里体现RDD的弹性?
- RDD的partition的个数可多可少
- RDD之间的依赖关系(容错特性)
哪里体现了RDD的分布式?
RDD的partition分布在多个节点上
RDD不存数据,partition也不存数据
1.1 创建RDD
Scala:
sc.textFile(…,minNumpartition)
sc.parallize(…,numpartition)
sc.makeRdd(…,numpartition)
java:
sc.textFile(…,minNumpartition)
sc.parallize(…,numPartition)
sc.parallizePairs(list(tuple2),numpartition)
actions算子(函数),是触发之前的算子执行的算子
transformations算子是懒加载的,是有action算子触发执行的,否则在spark中是不会执行的
transformations算子都是RDD到RDD
actions算子都是RDD到非RDD,因为这是要计算结果的
sample抽样(是否放回抽样,抽样比例,种子)设置了种子,抽样的结果永远都是相同的
1.2 持久化
默认将RDD数据持久化到内存中,cache是懒加载
cache和persist都是懒加载,必须由一个action类算子来触发执行
cache和persist算子的返回值都可以赋值给一个变量,在其他job中直接使用这个变量就可以使用持久化的数据
持久化的单位是partition
chche和persist算子后都不能紧跟action算子
checkpoint将RDD持久化到磁盘,还可以切断RDD之间的依赖关系
checkpoint的执行原理:
1.当RDD的job执行完毕之后,会从finalRDD从后往前回溯
2.当回溯到某一个RDD调用了checkpoint方法,会对当前的RDD做一个标记
3.spark框架会自动启动一个新的job,重新计算这个RDD的数据,将数据持久化到HDFS上
优化策略:当使用checkpoint对一个RDD做标记的时候,最好先对此RDD做一次cache,先持久化到内存上,这样启动新的job只需要将内存中的数据拷贝到HDFS上即可,省去了重新计算步骤。
2. 提交任务
2.1 Standalone-client模式提交任务
客户端:在客户端提交任务,Driver会在客户端启动,然后Driver向Master申请资源,当Master找到满足资源的节点后,Driver发送task监控task,回收结果。
Master:Master掌握了集群的资源,所有的worker向Master会报资源,当Driver向自己申请资源的时候,Master找到满足资源的节点。
Standalone-client模式提交任务,会在客户端看到task的执行情况与结果。当在客户端提交多个application时候,每个Application都会启动自己的Driver,Driver与集群中worker会有大量的通信,会造成客户端网卡流量激增问题。这种模式适用于程序测试,不使用与生产环境。
2.2 Standalone-cluster模式提交任务
客户端:客户端提交Application,向Master申请启动Driver,
Master:所有的worker向Master汇报资源内容,此时Master掌握集群的资源,Master收到客户端的请求之后,随机在一台worker节点开启启动一个Driver,此时worker上的driver启动之后,由worker向master申请资源,Master找到满足资源的worker节点,此时driver与worker之间建立联系,发送task,监控task,回收结果
Standalone-cluster模式提交任务,Driver会在集群中的随机一台Worker启动,如果提交多个Application,那么每个application的Driver会分散到集群中的Worker节点,相当于将Client模式的客户端网卡流量激增问题分散到集群中,这种模式适用于生产环境。
./spark-submit --master spark://node01:7077 --deploy-mode cluster --class org.apache.spark.examples.SparkPi ../lib/spark-examples-1.6.0-hadoop2.6.0.jar 100
此时任务提交之后在本地是看不见任何的输出,输出的结果与运行的状态都是在http://node01:8080 可以查看到
2.3 Yarn-client模式提交任务
客户端:客户端启动Driver,提交Application,Driver向RS申请启动ApplicationMaster,当Driver收到注册信息以后,发送task,回收结果
ResourceManager:所有的NodeManager向ResourceManager汇报资源,ResourceManager掌握集群的资源,收到客户端的请求之后,ResourceManager会随机找一台NodeManager启动ApplicationMaster,AM启动之后向RS申请资源,RS收到AM的请求之后返回一批NM节点给AM。此时AM就连接NM启动其中的Executor(包含ThreadPool线程池),Executor启动之后反向注册给Driver。
Yarn-client模式提交任务,Driver在客户端启动,当提交多个Application,每个Application的Driver都会在客户端启动,也会有网卡流量激增问题,这种模式适用于程序测试,不适用于生产环境。
yarn-daemon.sh start resourcemanager手动启动resourceManager节点
2.4 Yarn-cluster模式提交任务
客户端:在客户端提交到Application,向RS申请启动Application
ResourceManager:NodeManager向ResourceManager汇报资源情况,RS掌握所有的集群资源,收到来自客户端的请求之后,随机找到一台NM节点启动AM(等同于Driver),AM启动之后向RS申请资源,RS向AM返回一批NM,此时AM连接NM并且启动Executor(其中包含ThreadPool),Executor启动之后反向注册给AM(Driver),此时AM发送task监控task并回收结果
Yarn-cluster模式提交任务:适用于生产环境,AM(Driver)随机在一台NM上启动,当提交多个Application的时候,每个application的Driver会分散到集群中的NM中启动,相当于将Yarn-client模式中的网卡流量激增问题分散到集群中去。在客户端看不到task执行和结果,需要去webui中查看。
3. 算子RDD
transformation:
RDD:join,lefeOuterJoin,rightOuterJoin,fullOuterJoin
作用:作用在k,v格式的RDD上,根据k进行连接,对 (k,v) join (k,w)返回(k,(v,w))
注意:join后的分区数与父RDD分区数最多的那一个相同
算子 | 作用 | 注意点 |
---|---|---|
transformation | ||
join,lefeOuterJoin,rightOuterJoin,fullOuterJoin | 作用在k,v格式的RDD上,根据k进行连接,对 (k,v) join (k,w)返回(k,(v,w)) | join后的分区数与父RDD分区数最多的那一个相同 |
union | 合并两个数据集,两个数据集类型需要保持一致 | 返回新的RDD分区数是之前合并的RDD的分区数之和 |
intersection | 取两个数据集的交集 | |
subtract | 取两个数据集的差集 | |
mapPartition | 与map类似,遍历的单位是每个partition上的数据 | |
distinct | 实现数据集的去重操作 | 底层实现为:map+reduceByKey+map |
cogroup | 当调用类型的数据为相同的key时,即(k,v) (k,w),返回一个数据集(K,(Iterable<V>,Iterable<W>)) | |
action | ||
foreachPartition | 遍历的数据是每个partition的数据(其中的数据集是Iterable) |
算子 | 作用 | 注意 |
---|---|---|
transformation | ||
mapPartitionWithIndex | 类似于mapPartitions,除此之外还会携带分区的索引值 | |
repartition | 增加或减少分区,会产生shuffle过程 | 多个分区分到一个分区不会产生shuffle |
coalesce | 常用来减少分区,可以设置是否产生shuffle | repartition=coalesce |
groupByKey | 作用在K,V格式的RDD上,根据Key进行分组,返回(K,Iterable<K,V>) | |
zip | 将两个RDD中的元素(KV格式/非KV格式)变成一个KV格式的RDD | 注意两个RDD的每个分区元素必须相等 |
zipWithIndex | 将两个RDD根据索引号组成(K,V)对 | |
action | ||
countByKey | 作用到K,V格式的RDD上,根据Key计数相同Key的数据集元素 | |
countByValue | 根据数据集的每个元素相同的value值来计数,返回相同元素内容对应的条数 | |
reduce | 根据聚合逻辑聚合数据集中的每个元素 |
coalesce:此时如果RDD有X个分区,需要重新划分为Y个分区
1.如果x<y,说明x个分区里有数据有分布不均匀的情况,利用HashPartition把x个分区重新划分成了y个分区,此时需要把shuffle设置为true,设置为false,此时父RDD和子RDD之间是窄依赖,这是并不会增加RDD的分区
2.如果x>y,需要先把x分区中的某个分区合并成一个分区,最终合并成y个分区,此时需要把coalesce方法shuffle设置为fale
总结:如果想要增加分区的时候,可以用repartition或者coalesce,true都行,但是一定要有shuffle操作,分区数量才会增加,为了让该函数并行执行,通常把shuffle的值设置成true。
3.1 RDD的宽窄依赖
RDD窄依赖:
- 父RDD与子RDD partition之间的关系是一对一
- 父RDD与子RDD partition之间的关系是多对一
RDD宽依赖(shuffle过程):
- 父RDD与子RDD partition之间的关系是一对多
4. Spark pipeline计算模式
Spark处理数据的模式(pipeline计算模式)
f3(f2(f1(…)))高阶函数展开形式处理数据
MR: 1+1=2 2+1=3 Spark: 1+1+1=3
问题:
- Stage中的并行度由谁决定?
- 由Stage中的final RDD的partition个数决定
- 如何提高Stage的并行度?
- reduceByKey(…,numpartition)
- join(…,numpartition)
- 管道中的数据何时落地(磁盘落地)?
- shuffle write
- 对RDD进行持久化的时候,此时数据需要进入磁盘存储,数据落地
5. Spark的资源调度和任务调度
流程:
启动集群以后,Worker节点会向Master节点会报资源情况,Master掌握集群资源情况,当Spark提交了一个Application后,根据RDD之间的依赖关系形成一个有向无环图DAG,任务提交以后Spark会在Driver端创建两个对象:DAGScheduler和TaskScheduler。
DAGScheduler是任务调度的高层调度器,是一个对象,DAGScheduler的主要作用就是将DAG根据RDD之间的宽窄关系划分为一个个的Stage,然后将这些Stage以TaskSet的形式提交给TaskScheduler,TaskScheduler是一个任务调度的底层调度器,这里的TaskSet其实就是一个集合,里面封装的就是一个个的Task任务,也就是Stage中的并行度task任务。TaskScheduler会遍历TaskSet集合,拿到每个task后会将task发送到计算节点Executor中去执行(实质是发送到Executor中的线程池ThreadPool中去执行),task在Executor线程池中的运行情况会想TaskScheduler反馈,一旦task执行失败,由TaskScheduler负责重试,将task重新发送到Executor中去执行,默认重试的次数为3次,如果3次之后task还是失败,则代表这个task所在的stage失败,此时由上一级DAGScheduler来重试,重新发送TaskSet到TaskScheduler,DAGScheduler默认重试4次,如果重试4次以后依然失败,那么这个job就失败了,代表这个Application也就失败了。
TaskScheduler不仅会重试失败的task,还会重试straggling(落后的)task节点,如果有运行缓慢的task,此时TaskScheduler检测到之后,会启动一个新的task来与这个运行缓慢的task执行相同的处理逻辑,此时两个task谁先执行完,就以谁的输出结果为准,上述阐述的是Spark的推测执行机制。在Spark中这种机制是默认关闭的,可以通过spark.sprculation
属性来进行设置。
推测执行机制需要注意:
- 对于ETL类型要入数据库的业务要关闭推测执行机制,否则会产生重复的数据录入数据库
- 如果遇到数据倾斜的情况,开启推测执行机制则有可能导致一直会有task重新启动处理相同的逻辑,任务可能处于一直处理不完的状态
详细流程:
资源调度流程:
Worker向Master汇报资源,Master掌握集群的资源,客户端会启动Driver,创建Application,Diriver中会创建DAGScheduler和TaskScheduler两个对象,此时当代码执行到val sc = new SparkContext(conf)
的时候,此时TaskScheduler向master申请资源,Master节点收到请求之后,找到满足资源的节点,启动Executor,Executor启动之后反向注册给TaskScheduler,Driver掌握一批计算资源。
资源调度结束,开启任务调度
任务调度流程:
代码运行到action算子的代码处的时候,此时启动了job(一个action与job是一对一的关系),DAGSchduler拿到每个job中的RDD依赖关系形成的DAG有向无环图,依照RDD之间的宽窄依赖关系切割Job划分Stage,将Stage封装成TaskSet对象提交给TaskScheduler,TaskScheduler拿到TaskSet之后,会遍历TaskSet拿到一个个的Task,发送到Executor中的ThreadPool中去执行。此时发送task到ThreadPool中去,TaskScheduler监控task执行回收结果。
5.1 粗粒度资源申请:Spark
当Application执行的之前,会将所有的资源申请完毕,申请到资源,就执行application,申请不到就一直等待。当所有的task都执行完毕才会释放这批资源。
- 优点:application执行之前将资源申请完毕,每个task执行时候就不需要自己去申请资源,task执行就快了,于是job就快了,application执行速度也就快了
- 缺点:所有的task执行完毕才会释放这些资源,容易使集群资源不能得到充分利用(注:主要是当有一个task运行十分缓慢,整个集群的资源是不能够被其他的job所使用的)
5.2 细粒度资源申请:MR
当application执行时,由执行任务的task自己去申请资源,使用完毕之后自己释放资源
- 优点:集群资源充分利用
- 缺点:task自己申请资源,自己释放资源,task执行速度慢,application执行慢
6. Spark-submit提交参数
参数选项 | 含义 |
---|---|
–master | MASTER_URL,可以是spark://host:port, mesos://host:port,yarn,yarn-cluster,yarn-client,local |
–deploy-mode | DEPLOY_MODE,Driver程序运行的地方,client或者cluster,默认是client |
–class | CLASS_NAME,主类名称,含包名 |
–jars | 逗号分隔本地的JARS,Driver和executor依赖的第三方jar包 |
–files | 用逗号分隔开的文件列表,会防止在每个executor工作目录中 |
–driver-memory | Driver程序使用内存大小(例如:1000M,5G),默认为1024M |
–executor-memory | 每个executor内存大小(如1000M,2G),默认1G |
Spark standalone with cluster deploy mode only | |
–drivers-cores | Driver程序使用的core个数(默认为1),仅限于Spark standalone模式 |
Spark standalone or Mesos with cluster deploy mode only | |
–supervise | 失败后是否重启Driver,仅限于Spark standalone或者Mesos模式 |
Spark standalone and Mesos Only | |
–total-executor-cores | executor使用的总核数,仅限于Spark standalone,Mesos模式 |
Spark standalone and YARN only | |
–executor-cores | 每个executor使用的core数,Spark on Yarn默认为1,standalone默认为worker上所有可用的core |
Yarn-only | |
–driver-cores | driver使用的core,仅在cluster模式下,默认为1 |
–queue | QUEUE_NAME 指定资源队列的名称,默认为:default |
–num-executors | 一共启动的executor数量,默认是2个 |
7. 源码分析
7.1 资源调度分析
资源调度
资源调度Master:源码:org.apache.spark.deploy.master.Master
资源调度,首先./start-all.sh
开启spark服务,然后通过ssh协议与worker节点通信,通信过程中,worker节点反向向Master节点注册,并汇报资源,此时在Master中存在val workers = new HashSet[WorkInfo]
任务提交SparkSubmit源码:org.apache.spark.deploy.SparkSubmit
此时client端提交任务,向Master端请求启动一个Driver,此时就从变长数组中val waitingDriver = new ArrayBuffer[DriverInfo]
中选择一个Driver启动,此时启动的是一个DriverWarrap
进程,启动之后,此进程向Master为当前的Application请求资源。
资源调度Worker源码:org.apache.spark.deploy.worker.Worker
当为Application请求资源的时候,此时val waitingApps = new ArrayBuffer[ApplicationInfo]
,此时Master会在worker上调度和启动Executor。
总结:
- Executor在集群中分散启动时,有利于task计算的数据本地化(源码中方法
scheduleExecutorsOnWorkers
)如果不分散启动的话,会一直在当前的worker启动Executor,直到资源耗尽为止,如果分散启动的话,会在下一个worker中轮询启动executor - 默认情况下(提交任务的时候没有设置
--executor-cores
选项),每一个Worker为当前的Application启动一个Executor,这个Executor会使用这个Worker的所有的cores和1G内存 - 如果想在Worker中启动多个Executor,提交Application的时候需要加
--executor-cores
这个选项 - 默认情况下设置
--total-executor-cores
,一个Application会使用Spark集群中的所有的cores
针对上面的结论的演示:
- 默认情况下每个worker为当前的Application启动一个Executor,这个Executor使用集群中所有的cores和1G内存
./spark-submit --master spark://node01:7077 --class org.apache.spark.examples.SparkPi ../lib/spark-examples-1.6.0-hadoop2.6.0.jar 1000
源代码中的解释为,如果是没有设置的话,只会启动一个Executor,但是资源会全都分配给它。所有使用了集群中所有的cores和1G的内存。
- 在worker上启动多个Executor,设置
--executor-cores
参数指定每个executor使用的core数量./spark-submit --master spark://node01:7077 --executor-cores 1 --class org.apache.spark.examples.SparkPi ../lib/spark-examples-1.6.0-hadoop2.6.0.jar 100
我这总共有6core,所以启动了6个Executor
- 内存不足的情况下启动core的情况,Spark启动Executor不仅看core的配置参数,也要看配置的core的内存是否够用
./spark-submit --master spark://node01:7077 --executor-cores 1 --executor-memory 3g --class org.apache.spark.examples.SparkPi ../lib/spark-examples-1.6.0-hadoop2.6.0.jar 100
,此时虽然一个worker从core的角度来说可以启动多个executor,但是因为内存的3G限制,导致一个worker上只能启动一个Executor
4. --total-executor-cores
设置集群中共使用多少cores,注意一个进程中不能让多个节点共同启动./spark-submit --master spark://node01:7077 --executor-cores 1 --executor-memory 2g --total-executor-cores 3 --class org.apache.spark.examples.SparkPi ../lib/spark-examples-1.6.0-hadoop2.6.0.jar 100
此时共有的core为3个,此时一个work只能启动一个。
7.2 任务调度分析
任务调度
代码运行到action算子的代码处的时候,此时启动了job(一个action与job是一对一的关系),DAGSchduler拿到每个job中的RDD依赖关系形成的DAG有向无环图,依照RDD之间的宽窄依赖关系切割Job划分Stage,将Stage封装成TaskSet对象提交给TaskScheduler,TaskScheduler拿到TaskSet之后,会遍历TaskSet拿到一个个的Task,发送到Executor中的ThreadPool中去执行。此时发送task到ThreadPool中去,TaskScheduler监控task执行回收结果。
任务调度从Action算子开始,任务调度从一个Action类算子开始,这是因为Action类算子会触发一个job的执行,划分stage,以taskSet的形式提交任务。(DAGScheduler类总的getMissingParentStages())方法是切割job划分stage,可以结合下图分析(源代码中是根据宽依赖和窄依赖压栈,然后出栈处理)
8. 排序与分组
8.1 排序的实现
自定义类型,实现Comparable接口,重写CompareTo方法,如果涉及的数据比较多的话,建议构造对象实现Comparable重写CompareTo方法实现排序。
8.2 分组取topN
- groupByKey()+Collections.sort()
- groupByKey()+定长数组
示例:取每个班级的前三名
object class_topN{
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
conf.setMaster("local").setAppName("classTopN")
val sc = new SparkContext(conf)
val rdd: RDD[String] = sc.textFile("class.txt")
rdd.map(m=>{
val info = m.split(" ")
// 班级和分数
(info(0),info(1).toInt)
}).groupByKey().map(m=>{
// 从这里就取到了两组的数据
val className = m._1
// 对每组数据取前三名
val grade: Array[Int] = m._2.toArray.sortWith(_>_).take(3)
(className,grade)
}).foreach(m=>{
val className = m._1
println("班级:"+className+"的前三名为:")
m._2.foreach(println)
})
}
}
class1 98
class2 90
class2 92
class1 96
class1 100
class2 89
class2 68
class1 81
class2 90
9. 广播变量与累加器变量
9.1 广播变量
广播变量的意义:
如果我们要在分布式计算里面分发大对象,例如:字典,集合,黑白名单等,这个都会由Driver端进行分发,一般来讲,如果这个变量不是广播变量,那么每个task就会分发一份,当task数目十分多的情况下Driver的带宽会成为系统的瓶颈,而且会大量消耗task服务器上的资源,如果将这个变量声明为广播变量,那么只是每个executor拥有一份,这个executor启动的task会共享这个变量,节省了通信的成本和服务器的资源。
注意事项:
- 能不能将一个RDD使用广播变量广播出去?不能,因为RDD是不存储数据的,可以将RDD的结果广播出去
- 广播变量只能在Driver端定义,不能再Executor端定义
- 在Driver端可以修改广播变量的值,在Executor端无法修改广播变量的的值
object broadCast_variable {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
conf.setMaster("local").setAppName("broadCastVariable")
val sc = new SparkContext(conf)
val list = List("hello hadoop")
val broadCast = sc.broadcast(list)
val lineRDD = sc.textFile("wordCount.txt")
// 这里面运行的时候就是在executor中运行的,也就是广播变量会被一个executor中的所有task共享
lineRDD.filter { x => broadCast.value.contains(x) }.foreach { println}
sc.stop()
}
}
9.2 累加器变量
在Spark中不存在像Java中的那种全局变量,所有单纯的使用sum放进去executor中执行,会产生两个,但是打印出来还是上面定义的,因为在executor中执行的sum单纯的变量是取不到的。所以就有了累加器变量的定义。
val conf = new SparkConf()
conf.setMaster("local").setAppName("broadCastVariable")
val sc = new SparkContext(conf)
/**
* 累加器变量的使用
*/
val accumulator = sc.accumulator(0)
sc.textFile("wordCount.txt").foreach(x=>{
// 不能使用accumulator.value获取值,只能使用accumulator来获取
println(accumulator)
accumulator.add(1)
})
println(accumulator.value)
注意事项:
- 累加器在Driver端定义赋初始值,累加器只能在Driver端使用.value读取,在Executor端更新
- 累加器不能再executor端使用.value获取值,如果实在要获取可以直接使用累加器变量获取
10. SparkShell和Spark WebUI
10.1 SparkShell的使用
概念:
SparkShell是Spark自带的一个快速原型开发工具,也可以说是Spark的scala REPL(Read-Eval-Print-Loop),即交互式shell,支持使用scala语言来进行Spark的交互式编程
使用:
启动Standalone集群,./start-all.sh
在客户端上启动spark-shell:./spark-shell --master spark://node01:7077
启动hdfs,创建目录hdfs dfs -put -p /spark/test/
上传文件hdfs dfs -put -f /a/wordCount.txt /spark/test/
在SparkShell启动的shell端运行wordCount:sc.textFile("hdfs://node02:8020/spark/test/wordCount.txt").flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_).foreach(println)
最后的结果到Spark WebUI 可以查看到详细的信息
10.2 SparkUI
SprakUI界面介绍:
可以指定提交Application的名称./spark-shell --master spark://node01:7077 --name myapp
配置historyServer:
临时配置,仅对本次提交的应用程序起作用
./spark-shell --master spark://node01:7077 --name myapp
--conf spark.eventLog.enabled=true
--conf spark.eventLog.dir=hdfs://node01:8020/spark/test/
直接在配置文件中可以永久配置:
/conf/spark-defaults.conf
中配置:
spark.eventLog.enabled = true
开启日志记录功能
spark.eventLog.dir=hdfs://node01:8020/spark/test
日志的存储目录
spark.history.fs.logDirectory=hdfs://node01:8020/spark/test
设置HistoryServer加载事件日志的位置
spark.eventLog.comparess=true
日志优化选项,压缩日志,防止日志占用过大的内存
启动HistoryServer:./start-history-server.sh
访问HistoryServer:http://node01:18080/ 端口18080
11. Master HA高可用
Master高可用原理:
Standalone集群只有一个Master,如果Master挂了就无法提交应用程序,需要给Master进行高可用配置,Master的高可用可以使用fileSystem(文件系统)和Zookeeper(分布式协调服务)
fileSystem只有存储功能,可以存储Master的元数据信息,用fileSystem搭建的Master高可用,在Master失败时,需要我们手动启动另外的备用Master,这种方式不推荐使用
zookeeper有选举和存储的功能,可以存储Master的元数据信息,使用Zookeeper搭建的Master高可用,当Master挂掉时,备用的Master会自动切换,推荐这种方式搭建
Master HA搭建
- 在SparkMaster节点上配置主Master,配置spark-env.sh,添加配置
export SPARK_DAEMON_JAVA_OPTS=" -Dspark.deploy.recoveryMode=ZOOKEEPER -Dspark.deploy.zookeeper.url=node3:2181,node4:2181,node5:2181 -Dspark.deploy.zookeeper.dir=/sparkmaster0821"
- 分发到其他节点,找到备用节点修改备用Master,修改spark-env.sh中的MasterIP,
export SPARK_MASTER_IP=node02
- 启动spark集群之前启动zookeeper集群,启动主master以后,需要手动启动备用Master
./start-master.sh
- 打开主Master和备用Master WebUI页面,观察状态
注意点:
- 主备切换过程中不能提交Application
- 主备切换过程中不影响已经在集群中运行的Application,因为Spark是粗粒度资源调度。(执行之前就将资源申请完毕,所以即使主备切换其中并不包含资源的申请环节,所以主备切换不影响已经运行的Application)
上述的测试可以在启动之后,提交一个任务后,然后使用kill -9 主节点进程号
,观察现象。提交任务./spark-submit --master spark://node01:7077,node02:7077 --class org.apache.spark.examples.SparkPi ../lib/spark-examples-1.6.0-hadoop2.6.0.jar 1000
12. SparkShuffle
SparkShuffle的概念:
reduceByKey会将上一个RDD中的每一个key对应的所有的value聚合成一个value,然后生成一个新的RDD,元素类型是<key,value>键值对的形式,这样每一个key对应一个聚合起来的value
问题:聚合之前,每一个key对应的value不一定都是在一个partition中,也不太可能在同一个节点上,因为RDD是分布式的弹性数据集,RDD的partition极有可能分布在各个节点上
如何聚合?
- Shuffle Write:上一个stage的每个map task就必须保证自己处理的当前分区的数据相同的key写入到一个分区文件中,可能会写入到多个不同的分区文件中
- Shuffle Read:reduce task就会从上一个stage的所有task所在的机器上寻找已有的那些分区文件,这样就可以保证每一个key所对应的value都会汇聚到同一个节点山去处理与聚合
Spark中有两种Shuffle类型,HashShuffle和SortShuffle
Spark1.2之前是HashShuffle默认的分区器是HashPartitioner
Spark1.2引入SortShuffle默认的分区器是RangePartitioner
11.1 HashShuffle
1、普通机制:普通机制示意图
执行流程:
- 每一个map task将不同的结果写到不同的buffer中,每个buffer的大小为32k,buffer起到缓存的作用
- 每个buffer文件对应一个磁盘小文件
- reduce task最后来拉取对应的磁盘小文件
总结:
- map task的计算结果会根据分区器(默认是hashPartitioner)来决定写入到哪一个磁盘小文件中,ReduceTask会去Map端拉取相应的文件
- 产生磁盘的小文件个数=M(map task的个数)*R(recduce task个数)
存在的问题:主要是产生的小文件个数过多导致
- 在Shuffle Write过程中会产生很多写磁盘小文件的过程
- 在Shuffle Read过程中会产生很多读磁盘小文件的对象
- 在JVM堆内存中对象过多会造成频繁的GC,GC还无法解决运行所需要的内存的话,就会OOM
- 在数据传输过程中会有频繁的网络通信,频繁的网络通信出现通信故障的可能性大大增加,一旦网络通信出现了故障会导致shuffle file connot find,这个错误将会导致task失败,TaskScheduler不负责重试,由DAGScheduler负责重试stage
2、合并机制:示意图
产生磁盘小文件的个数:C(core的个数)*R(reduce task的个数),相比普通机制可以节省一半的文件个数/3、
11.2 SortShuffle
1、普通机制:示意图
执行流程:
- map task的计算结果会写入到一个内存数据结构里面去,内存的数据结构默认是5M
- 在shuffle的时候会有一个定时器,不定期的去估算这个内存结构的大小,当内存结构中的数据超过5M时,会去当前数据大小两倍的内存来,例如5.01>5 => 5.01*2-5=5.02M的内给内存数据结构
- 如果申请成功不会进行磁盘的溢写,如果不成功的话,此时会发生磁盘的溢写
- 在溢写之前内存数据结构中的数据会进行排序分区
- 然后开始溢写磁盘,写磁盘是以batch形式去写,一个batch是一万条数据
- map task执行完成之后,会将这些磁盘文小文件合成一个大的磁盘文件,同时生成一个索引文件
- reduce task去map端拉取数据的时候,首先解析索引文件根据索引文件再去拉取对应的数据
产生的磁盘个数:2*M(map task的个数)
2、bypass机制:示意图
总结:
- bypass机制与上面的普通机制差不多,只是少了内存溢写和排序的过程
- bypass运行机制的触发条件如下,shuffle reduce task的数量小于spark.shuffle.sore.bypassMergeThreshold的参数值,默认为200
- 产生的磁盘小文件个数:2*M(map task个数)
12. Shuffle文件寻址
相关模块介绍及功能:
- MapOutputTracker:是spark结构中的一个模块,是一个主从架构,管理磁盘小文件的地址信息
- MapOutputTrackerMaster是主对象,存在于Driver中
- MapOutputTrackerWorker是从对象,存在于Worker中
- BlockManager:块管理者,是spark架构中的一个模块,主从架构
- BlockManagerMaster,主对象,存在于Driver中,BlockManagerMaster会在集群中有用到广播变量和缓存数据或者删除数据的时候,通知BlockManagerSlave传输或者删除数据
- BlockManagerWorker,从对象,存在于Executor中,会与BolckManagerWork相互通信
- 无论是Driver端的BlockManager还是Executor端的BlocakManager都含有四个对象
- DiskStore:负责磁盘的管理
- MemoryStore:负责内存的管理
- ConnectionManager:负责连接其他的BlockManagerWorker
- BlockTransferService:负责数据的传输
shuffle文件寻址流程:
- 当map task执行完毕的时候,会将task的执行情况和磁盘小文件的地址封装到MapStatus对象中,通过MapOutputTrackerWorker对象向Driver中的MapOutputTrackerMaster汇报
- 当所有的map task执行完毕之后,Driver中就掌握了所有的磁盘小文件的地址信息
- 在reduce task执行之前会通过Executor中的MapOutputTrackerWorker向Driver端的MapOutputTrackerMaster获取磁盘小文件的地址
- 获取到磁盘小文件的地址后,会通过BlockManager中的ConnectionManager连接数据所在节点的ConnnectionManager,然后通过BlockTransferService进行数据的传输
- BlockTransferService默认启动5个task去节点拉取数据,默认情况下,5个task拉取数据量不能超过48M
13. Spark内存管理
Spark执行应用程序时,spark集群会启动Driver和Executor两种JVM进程,Driver负责创建SparkContext上下文,提交任务,task的分发等。Executor负责task的计算任务,并将结返回给Driver。同时需要为持久化的RDD提供存储,Driver端的内存管理比较简单,这里主要是说的Spark内存管理对Executor端的内存管理。
Spark内存管理分为静态内存管理和统一内存管理,Spark1.6之前使用的是今天内存管理,Spark1.6以后引入了统一内存管理。
静态内存管理中存储内存、执行内存和其他内存的大小在Spark应用程序运行期间均为固定的,单用户可以在应用程序启动之前进行配置
统一内存管理与静态内存管理的区别在于存储内存和执行内存共享同一块空间,可以互相借用对方的空间
1、静态内存管理分布图
2、统一内存管理分布图
14. Shuffle调优
三种方式
- 在代码中使用SparkConf().set(…),硬编码,不推荐
- 提交Spark任务的时候使用
--conf
,推荐使用,可以针对每一个需要提交的任务执行优化 - 在conf下的spark-default.conf配置文件中,不推荐,因为所配置的参数会使得之后所有的应用程序都使用,太死板
小结
这段时间在学的就是Spark,进度比较慢,但是学习的过程中发现Spark比Hadoop我个人来说是要更好用,之前学习了Scala语言,主要就是transformation和action算子的使用,基本就是这两种,在spark中最终要的就是RDD也就是算子,所有的都是围绕着算子来的,Spark与Hadoop之间最大的区别就在于Spark是基于内存的,而Hadoop是基于磁盘的,所以Spark速度快。Spark是粗粒度申请资源,Hadoop是细粒度申请资源,Hadoop的MR是需要自己编写逻辑实现,而Spark中有各种算子对应各种业务。这对于学Scala的要求就高了,学好了Scala学习Spark就很轻松了,还是要多联系,Spark的知识点实在是太多了,在日常的使用中还是要多学习,多看官方文档。大数据学习加油!