1、DAG
定义
Spark的核心是根据RDD
来实现的,SparkScheduler
则为Spark核心实现的重要一环,其作用就是任务调度。Spark的任务调度就是如何组织任务去处理RDD
中每个分区的数据,根据 RDD
的依赖关系构建 DAG
,基于DAG
划分Stage
,将每个Stage
中的任务发到指定节点运行。基于Spark的任务调度原理,可以合理规划资源利用,做到尽可能用最少的资源高效地完成任务计算。
以词频统计WordCount程序为例,DAG图:
DAG: 有向无环图,DAG
有方向没有形成闭环的一个执行流程图。
- 有向: 有方向;
- 无环: 没有闭环。
1.1、Job和Action
Action: 返回值不是RDD
的算子。它的作用是一个触发开关,会将action
算子之前的一串rdd
依赖链条执行起来。
结论
1个Action
会产生1个DAG
,如果在代码中有3个Action
就产生3个DAG
。
即:一个Action
产生的一个DAG
,会在程序运行中产生一个JOB
。
所以:1个ACTION = 1个DAG = 1个JOB
如果一个代码中,写了3个Action
,那么这个代码运行起来产生3个JOB
,每个JOB
有自己的DAG
一个代码运行起来,在Spark中称之为 : Application
层级关系: 1个Application
中,可以有多个JOB
每一个JOB
内含一个DAG
,同时每一个JOB
都是由一个Action
产生的。
1.2、DAG
和分区
DAG
是Spark代码的逻辑执行图,这个DAG
的最终作用是;为了构建物理上的Spark详细执行计划而生。所以,由于Spark是分布式(多分区)的,那么DAG
和分区之间也是有关联的。
# 假设,全部RDD都是3个分区在执行
rdd1=sc.textFile()
rdd2 =rdd1.flatMap()
rdd3=rdd2.map()
rdd4=rdd3.reduceByKey()
rdd4.action()
2、DAG的宽窄依赖和阶段划分
2.1、宽窄依赖
在SparkRDD
前后之间的关系,分为:
-
窄依赖: 父
RDD
的一个分区,全部将数据发给子RDD
的一个分区; -
宽依赖(
shuffle
): 父RDD
的一个分区,将数据发给子RDD
的多个分区。
2.2、阶段划分
对于Spark来说,会根据DAG
,按照宽依赖,划分不同的DAG
阶段
划分依据: 从后向前,遇到宽依赖就划分出一个阶段,称之为stage
。
如图,可以看到,在DAG
中,基于宽依赖.将DAG
划分成了2个stage
;在stage
的内部,一定都是:窄依赖。
3、内存迭代计算
如图,基于带有分区的DAG
以及阶段划分。可以从图中得到逻辑上最优的task
分配一个task
是一个线程来具体执行。那么如上图task1
中rdd1 rdd2 rdd3
的迭代计算,都是由一个task
(线程完成),这一阶段的这一条线,是纯内存计算。
如上图,task1 task2 task3
,就形成了三个并行的内存计算管道。
Spark默认受到全局并行度的限制,除了个别算子有特殊分区情况,大部分的算子,都会遵循全局并行度的要求,来规划自己的分区数。如果全局并行度是3其实大部分算子分区都是3。
Spark我们一般推荐只设置全局并行度,不要再算子上设置并行度;除了一些排序算子外,计算算子就让他默认开分区就可以了。
面试题
3.1、Spark是怎么做内存计算的?DAG的作用?Stage阶段划分的作用?
- Spark会产生
DAG
图; DAG
图会基于分区和宽窄依赖关系划分阶段;- 一个阶段的内部都是窄依赖,窄依赖内,如果形成前后1:1的分区对应关系,就可以产生许多内存迭代计算的管道;
- 这些内存选代计算的管道,就是一个个具体的执行
Task
; - 一个
Task
是一个具体的线程,任务跑在一个线程内,就是走内存计算了。
3.2、Spark为什么比MapReduce快?
- Spark的算子丰富,
MapReduce
算子匮乏(Map
和Reduce
),MapReduce
这个编程模型,很难在一套MR
中处理复杂的任务;很多的复杂任务,是需要写多个MapReduce
进行串联多个MR
串联通过磁盘交互数据。 - Spark可以执行内存迭代,算子之间形成
DAG
基于依赖划分阶段后,在阶段内形成内存迭代管道,但是MapReduce
的Map
和Reduce
之间的交互依旧是通过硬盘来交互的。
编程模型上Spark占优(算子够多);
算子交互上和计算上可以尽量多的内存计算而非磁盘迭代。
4、Spark并行度
Spark的并行: 在同一时间内,有多少个task
在同时运行;
并行度: 并行能力的设置。
比如设置并行度6其实就是要6个
task
并行在跑,在有了6个task
并行的前提下,rdd
的分区就被规划成6个分区了。
4.1、如何设置并行度
可以在代码中和配置文件中以及提交程序的客户端参数中设置,优先级从高到低:
-
代码中
-
客户端提交参数中
-
配置文件中
-
默认(1,但是不会全部以1来跑,多数时候基于读取文件的分片数量来作为默认并行度)
全局并行度配置的参数:spark.default.parallelism
4.2、全局并行度
- 配置文件中:
# conf/spark-defaults.conf中设置
spark.default.parallelism 100
- 在客户端提交参数中:
bin/spark-submit --conf "spark.default.parallelism=100"
- 在代码中设置:
conf = SaprkConf()
conf.set("spark.default.parallelism", "100")
全局并行度是推荐设置,不要针对
RDD
改分区,可能会影响内存迭代管道的构建,或者会产生额外的Shuffle
。
4.3、针对RDD的并行设置(不推荐)
只能在代码写,算子:
repartition
算子coalesce
算子partitionBy
算子
4.4、集群中如何规划并行度
结论: 设置为 CPU
总核心的2 ~ 10倍,比如集群可用CPU
核心是100个,我们建议并行度是200~ 1000。
确保是
CPU
核心的整数倍即可,最小是2倍,最大一般10倍或更高(适量)均可。
4.5、为什么要设置至少2倍?
CPU
的一个核心一时间只能干一件事情;- 所以,在100个核心的情况下,设置100个并行,就能让
CPU
100%出力; - 这种设置下,如果
task
的压力不均衡,某个task
先执行完了。就导致某个CPU
核心空闲; - 所以,我们将
Task
(并行)分配的数量变多,比如800个并行,同一时间只有100个在运行,700个在等待; - 但是可以确保,某个
task
运行完了。后续有task
补上,不让cpu
闲下来,最大程度利用集群的资源。
规划并行度,只看集群总CPU
核数。
5、Spark的任务调度
5.1、Spark程序调度流程
Driver
被构建出来;- 构建
SparkContext
(执行环境入口对象); - 基于
DAG Scheduler
(DAG
调度器)构建逻辑Task
分配; - 基于
TaskScheduler
(Task
调度器)将逻辑Task
分配到各个Executor
上干活,并监控它们; Worker(Executor)
,被TaskScheduler
管理监控,听从它们的指令干活,并定期汇报进度。
1,2,3,4都是
Driver
的工作,5是Worker
的工作。
5.2、Driver调度过程
- 逻辑
DAG
产生; - 分区
DAG
产生; Task
划分;- 将
Task
分配给Executor
并监控其工作。
5.3、Driver内的两个组件
- DAG调度器: 将逻辑的
DAG
图进行处理,最终得到逻辑上的Task
划分; - Task调度器: 基于
DAG Scheduler
的产出,来规划这些逻辑的task
,应该在哪些物理的executor
上运行,以及监控管理它们的运行。
6、Spark层次关系
- 一个Spark环境可以运行多个
Application
; - 一个代码运行起来,会成为一个
Application
; Application
内部可以有多个Job
;- 每个
Job
由一个Action
产生, 并且每个Job
有自己的DAG
执行图; - 一个
Job
的DAG
图会基于宽窄依赖划分成不同的阶段; - 不同阶段内基于分区数量,形成多个并行的内存迭代管道;
- 每一个内存迭代管道形成一个
Task
(DAG
调度器划分将Job
内划分出具体的task
任务,一个Job
被划分出来的task
在逻辑上称之为这个job
的taskset
)。
7、Spark Shuffle
Spark在DAG
调度阶段会将一个Job
划分为多 个Stage
,上游Stage
做map
工作, 下游Stage
做reduce
工作,其本质上还是MapReduce
计算框架。Shuffle
是连接map
和reduce
之间的桥梁,它将 map
的输出对应到 reduce
输入中, 涉及到序列化反序列化、跨节点网络IO
以及磁盘读写IO
等。
Spark的Shuffle
分为Write
和Read
两个阶段,分属于两个不同的Stage
,前者是ParentStage
的最后一步,后者是Child Stage
的第一步。
执行Shuffle
的主体是Stage
中的并发任务,这些任务分ShuffleMapTask
和ResultTask
两种,ShuffleMapTask
要进行Shuffle
,ResultTask
负责返回计算结果,一个Job
中只有最后的Stage
采用ResultTask
,其他的均为ShuffleMapTask
。如果要按照map
端和reduce
端来分析的话,ShuffleMapTask
可以即是map
端任务,又是reduce
端任务,因为Spark中的Shuffle
是可以串行的;ResultTask
则只能充当reduce
端任务的角色。
8、Hash Shuffle
8.1、阶段划分
- shuffle write:
mapper
阶段,上一个stage
得到最后的结果写出; - shuffle read:
reduce
阶段,下一个stage
拉取上一个stage
进行合并
未经优化的 hashShuffleManager
: HashShuffle
是根据task
的计算结果的key
值的hashcode%ReduceTask
来决定放入哪一个区分,这样保证相同的数据一定放入一个分区,Hash Shuffle
过程如下:
根据下游的task
决定生成几个文件,先生成缓冲区文件在写入磁盘文件,再将block
文件进行合并。未经优化的shuffle write
操作所产生的磁盘文件的数量是极其惊人的。提出如下解决方案。
经过优化的 hashShuffleManager
: 在shuffle write
过程中,task
就不是为下游stage
的每个task
创建一个磁盘文件了。此时会出现shuffleFileGroup
的概念,每个shuffleFileGroup
会对应一批磁盘文件,每一个Group
磁盘文件的数量与下游stage
的task
数量是相同的。
8.2、优化前后对比
-
未经优化:
- 上游的
task
数量:m - 下游的
task
数量:n - 上游的
executor
数量:k (m>=k) - 总共的磁盘文件:mn
- 上游的
-
*优化之后的:
-
- 上游的
task
数量:m - 下游的
task
数量:n - 上游的
executor
数量:k (m>=k)
- 总共的磁盘文件:kn
- 上游的
9、Sort Shuffle Manager
SortShuffleManager
的运行机制主要分成两种,一种是普通运行机制,另一种是bypass
运行机制。当shuffle write task
的数量小于等于spark.shuffle.sort.bypassMergeThreshold
参数的值时(默认为200),就会启用bypass
机制。
-
该模式下,数据会先写入一个内存数据结构中(默认5M),此时根据不同的
shuffle
算子,可能选用不同的数据结构。如果是reduceByKey
这种聚合类的shuffle
算子,那么会选用Map
数据结构,一边通过Map
进行聚合,一边写入内存;如果是join
这种普通的shuffle
算子,那么会选用Array
数据结构,直接写入内存。 -
每写一条数据进入内存数据结构之后,就会判断一下,是否达到了某个临界阈值。如果达到临界阈值的话,那么就会尝试将内存数据结构中的数据溢写到磁盘,然后清空内存数据结构。
-
排序: 在溢写到磁盘文件之前,会先根据
key
对内存数据结构中已有的数据进行排序。 -
溢写: 排序过后,会分批将数据写入磁盘文件。默认的
batch
数量是10000条,也就是说,排序好的数据,会以每批1万条数据的形式分批写入磁盘文件。 -
merge: 一个
task
将所有数据写入内存数据结构的过程中,会发生多次磁盘溢写操作,也就会产生多个临时文件。最后会将之前所有的临时磁盘文件都进行合并成1个磁盘文件,这就是merge
过程。
由于一个task
就只对应一个磁盘文件,也就意味着该task
为Reduce
端的stage
的task
准备的数据都在这一个文件中,因此还会单独写一份索引文件,其中标识了下游各个task
的数据在文件中的start offset
与end offset
。
9.1、bypass运行机制
bypass
运行机制的触发条件如下:
shuffle map task
数量小于spark.shuffle.sort.bypassMergeThreshold=200
参数的值。- 不是
map combine
聚合的shuffle
算子(比如reduceByKey
有map combie
)。
- 此时
task
会为每个reduce
端的task
都创建一个临时磁盘文件,并将数据按key
进行hash
,然后根据key
的hash
值,将key
写入对应的磁盘文件之中。当然,写入磁盘文件时也是先写入内存缓冲,缓冲写满之后再溢写到磁盘文件的。最后,同样会将所有临时磁盘文件都合并成一个磁盘文件,并创建一个单独的索引文件。 - 该过程的磁盘写机制其实跟未经优化的
HashShuffleManager
是一模一样的,因为都要创建数量惊人的磁盘文件,只是在最后会做一个磁盘文件的合并而已。因此少量的最终磁盘文件,也让该机制相对未经优化的HashShuffleManager
来说,shuffle read
的性能会更好。
而该机制与普通SortShuffleManager
运行机制的不同在于:
-
磁盘写机制不同;
-
不会进行排序。也就是说,启用该机制的最大好处在于,
shuffle write
过程中,不需要进行数据的排序操作,也就节省掉了这部分的性能开销。
9.2、总结
SortShuffle
也分为普通机制和bypass
机制- 普通机制在内存数据结构(默认为5M)完成排序,会产生2M个磁盘小文件。
- 而当
shuffle map task
数量小于spark.shuffle.sort.bypassMergeThreshold
参数的值。或者算子不是聚合类的shuffle
算子(比如reduceByKey
)的时候会触发SortShuffle
的bypass
机制,SortShuffle的bypass
机制不会进行排序,极大的提高了其性能。
10、Shuffle的配置选项
Shuffle
阶段划分:
- shuffle write:
mapper
阶段,上一个stage
得到最后的结果写出; - shuffle read:
reduce
阶段,下一个stage
拉取上一个stage
进行合并。
细节
Spark 的shuffle
调优: 主要是调整缓冲的大小,拉取次数重试重试次数与等待时间,内存比例分配,是否进行排序操作等等。
-
spark.shuffle.file.buffer
- 参数说明: 该参数用于设置
shuffle write task
的BufferedOutputStream
的buffer
缓冲大小(默认是32K)。将数据写到磁盘文件之前,会先写入buffer
缓冲中,待缓冲写满之后,才会溢写到磁盘; - 调优建议: 如果作业可用的内存资源较为充足的话,可以适当增加这个参数的大小(比如64k),从而减少
shuffle write
过程中溢写磁盘文件的次数,也就可以减少磁盘IO次数,进而提升性能。在实践中发现,合理调节该参数,性能会有1%~5%的提升。
- 参数说明: 该参数用于设置
-
spark.reducer.maxSizeInFlight
- 参数说明: 该参数用于设置
shuffle read task的buffer
缓冲大小,而这个buffer
缓冲决定了每次能够拉取多少数据。(默认48M); - 调优建议: 如果作业可用的内存资源较为充足的话,可以适当增加这个参数的大小(比如96m),从而减少拉取数据的次数,也就可以减少网络传输的次数,进而提升性能。在实践中发现,合理调节该参数,性能会有1%~5%的提升。
- 参数说明: 该参数用于设置
-
spark.shuffle.io.maxRetries and spark.shuffle.io.retryWait
- spark.shuffle.io.maxRetries :
shuffle read task
从shuffle write task
所在节点拉取属于自己的数据时,如果因为网络异常导致拉取失败,是会自动进行重试的。该参数就代表了可以重试的最大次数。(默认是3次); - spark.shuffle.io.retryWait: 该参数代表了每次重试拉取数据的等待间隔。(默认为5s);
- 调优建议: 一般的调优都是将重试次数调高,不调整时间间隔。
- spark.shuffle.io.maxRetries :
-
spark.shuffle.memoryFraction
- 参数说明: 该参数代表了
Executor
内存中,分配给shuffle read task
进行聚合操作内存比例。
- 参数说明: 该参数代表了
-
spark.shuffle.manager
- 参数说明: 该参数用于设置
shufflemanager
的类型(默认为sort
)。Spark1.5x
以后有三个可选项:- Hash:
spark1.x
版本的默认值,HashShuffleManager
; - Sort:
spark2.x
版本的默认值,普通机制,当shuffle read task
的数量小于等于。spark.shuffle.sort.bypassMergeThreshold
参数,自动开启bypass
机制
- Hash:
- 参数说明: 该参数用于设置
-
spark.shuffle.sort.bypassMergeThreshold
- 参数说明: 当
ShuffleManager
为SortShuffleManager
时,如果shuffle read task
的数量小于这个阈值(默认是200),则shuffle write
过程中不会进行排序操作; - 调优建议: 当你使用
SortShuffleManager
时,如果的确不需要排序操作,那么建议将这个参数调大一些。
- 参数说明: 当