Spark shuffle及宽窄依赖

本文深入解析Spark中的Shuffle机制,包括HashShuffleManager与SortShuffleManager的发展历程、运行原理及优化策略。同时,对比宽窄依赖的区别,阐述其对Spark作业性能的影响。

1.Spark shuffle 发展

ShuffleManager发展概述

	负责shuffle过程的执行、计算和处理的组件主要就是ShuffleManager,也即shuffle管理器。
	在Spark1.2以前,默认的shuffle计算引擎是HashShuffleManager。该ShuffleManager而
HashShuffleManager有着一个非常严重的弊端,就是会产生大量的中间磁盘文件,进而由大量的磁盘IO操作影响了性能。
	因此在Spark1.2以后的版本中,默认的ShuffleManager变成SortShuffleManager,
SortShuffleManager相较于HashShuffleManager来说,有了一定的改进。主要就在于,每个Task在进行
shuffle操作时,虽然也会产生较多的临时磁盘文件,但是最后会将所有的临时文件合并(merge)成一个磁盘文
件,因此每个Task就只有一个磁盘文件。在下一个stage的shuffle-read-task拉取自己的数据时,只要根据索引
读取每个磁盘文件中的部分数据即可。

HashShuffleManager运行原理:
未经优化的HashShuffleManager:
	shuffle-write阶段,主要就是在一个stage结束计算之后,为了下一个stage可以执行shuffle类的算子
(比如reduceByKey),而将每个task处理的数据按key进行“分类”。所谓“分类”,就是对相同的key执行hash算
法,从而将相同key都写入同一个磁盘文件中,而每一个磁盘文件都只属于下游stage的一个task。在将数据写入磁
盘之前,会先将数据写入内存缓冲中,当内存缓冲填满之后,才会溢写到磁盘文件中去。
	那么每个执行shuffle-write的task,要为下一个stage创建多少个磁盘文件呢?下一个stage的task有多
少个,当前stage的每个task就要创建多少份磁盘文件。比如下一个stage总共有100个task,那么当前stage的每
个task都要创建100份磁盘文件。如果当前stage有50个task,总共有10个Executor,每个Executor执行5个
Task,那么每个Executor上总共就要创建500个磁盘文件,所有Executor上会创建5000个磁盘文件。shuffle-read,
通常就是一个stage刚开始时要做的事情。此时该stage的每一个task就需要将上一个stage的计算结果中的所有相
同key,从各个节点上通过网络都拉取到自己所在的节点上,然后进行key的聚合或连接等操作。由于shufflewrite
的过程中,task给下游stage的每个task都创建了一个磁盘文件,因此shuffleread的过程中,每个task只要从上
游stage的所有task所在节点上,拉取属于自己的那一个磁盘文件即可。

优化后的HashShuffleManager:
	spark.shuffle.consolidateFiles。该参数默认值为false,将其设置为true即可开启优化机制。
	开启consolidate机制之后,在shuffle-write过程中,task就不是为下游stage的每个task创建一个磁盘
文件了。此时会出现shuffleFileGroup的概念,每个shuffleFileGroup会对应一批磁盘文件,磁盘文件的数量
与下游stage的task数量是相同的。一个Executor上有多少个CPU core,就可以并行执行多少个task。而第一批
并行执行的每个task都会创建一个shuffleFileGroup,并将数据写入对应的磁盘文件内。
	当Executor的CPU-core执行完一批task,接着执行下一批task时,下一批task就会复用之前已有的
shuffleFileGroup,包括其中的磁盘文件。也就是说,此时task会将数据写入已有的磁盘文件中,而不会写入新
的磁盘文件中。因此,consolidate机制允许不同的task复用同一批磁盘文件,这样就可以有效将多个task的磁盘
文件进行一定程度上的合并,从而大幅度减少磁盘文件的数量,进而提升shuffle write的性能假设第二个stage有
100个task,第一个stage有50个task,总共还是有10个Executor,每个Executor执行5个task。那么原本使用
未经优化的HashShuffleManager时,每个Executor会产生500个磁盘文件,所有Executor会产生5000个磁盘文
件的。但是此时经过优化之后,每个Executor创建的磁盘文件的数量的计算公式为:CPUcore的数量*下一个stage
的task数量。也就是说,每个Executor此时只会创建100个磁盘文件,所有Executor只会创建1000个磁盘文件。

SortShuffleManager运行原理:
	一种是普通运行机制,另一种是bypass运行机制。当shuffle read task的数量小于等于
spark.shuffle.sort.bypassMergeThreshold参数的值时(默认为200),就会启用bypass机制。
普通运行机制:
	普通的SortShuffleManager的原理。在该模式下,数据会先写入一个内存数据结构中,此时根据不同的
shuffle算子,可能选用不同的数据结构。如果是reduceByKey这种聚合类的shuffle算子,那么会选用Map数据结
构,一边通过Map进行聚合,一边写入内存;如果是join这种普通的shuffle算子,那么会选用Array数据结构,直
接写入内存。接着,每写一条数据进入内存数据结构之后,就会判断一下,是否达到了某个临界阈值。如果达到临界
阈值的话,那么就会尝试将内存数据结构中的数据溢写到磁盘,然后清空内存数据结构。
	在溢写到磁盘文件之前,会先根据key对内存数据结构中已有的数据进行排序。排序过后,会分批将数据写入磁
盘文件。默认的batch数量是10000条,也就是说,排序好的数据,会以每批1万条数据的形式分批写入磁盘文件。写
入磁盘文件是通过Java的BufferedOutputStream实现的。BufferedOutputStream是Java的缓冲输出流,首先
会将数据缓冲在内存中,当内存缓冲满溢之后再一次写入磁盘文件中,这样可以减少磁盘IO次数,提升性能。
	一个task将所有数据写入内存数据结构的过程中,会发生多次磁盘溢写操作,也就会产生多个临时文件。最后会
将之前所有的临时磁盘文件都进行合并,这就是merge过程,此时会将之前所有临时磁盘文件中的数据读取出来,然
后依次写入最终的磁盘文件之中。此外,由于一个task就只对应一个磁盘文件,也就意味着该task为下游stage的
task准备的数据都在这一个文件中,因此还会单独写一份索引文件,其中标识了下游各个task的数据在文件中的
start offset与end offset。
	SortShuffleManager由于有一个磁盘文件merge的过程,因此大大减少了文件数量。比如第一个stage有50
个task,总共有10个Executor,每个Executor执行5个task,而第二个stage有100个task。由于每个task最终
只有一个磁盘文件,因此此时每个Executor上只有5个磁盘文件,所有Executor只有50个磁盘文件。

bypass运行机制:
bypass运行机制的触发条件如下:
	1、shuffle map task数量小于spark.shuffle.sort.bypassMergeThreshold参数的值。
	2、不是聚合类的shuffle算子(比如reduceByKey)。
  此时task会为每个下游task都创建一个临时磁盘文件,并将数据按key进行hash然后根据key的hash值,将
key写入对应的磁盘文件之中。当然,写入磁盘文件时也是先写入内存缓冲,缓冲写满之后再溢写到磁盘文件的。最
后,同样会将所有临时磁盘文件都合并成一个磁盘文件,并创建一个单独的索引文件。
  该过程的磁盘写机制其实跟未经优化的HashShuffleManager是一模一样的,因为都要创建数量惊人的磁盘文
件,只是在最后会做一个磁盘文件的合并而已。因此少量的最终磁盘文件,也让该机制相对未经优化的
HashShuffleManager来说,shuffle read的性能会更好。
  而该机制与普通SortShuffleManager运行机制的不同在于:第一,磁盘写机制不同;第二,不会进行排序。也
就是说,启用该机制的最大好处在于,shuffle write过程中,不需要进行数据的排序操作,也就节省掉了这部分的
性能开销。

shuffle讲解

1、shuffle的概念:
    shuffle是spark中数据重分发的一种机制,以便于在跨分区进行数据的分组。shuffle通常会引起executor
与节点之间的数据复制,这期间会有大量的网络I/O,磁盘I/O和数据的序列化。这使得shuffle操作十分地复杂和昂贵。
    在shuffle内部,单个map tasks的结果被保存在内存中,直到放不下为止。然后,根据目标分区对它们进行
排序,并将它们写入单个文件。在reduce端,tasks会读取相关的经过排序的数据块。
     shuffle还会在磁盘上产生大量的中间文件,这样做是为了当触发重算的时候这些中间文件不用被重新创建。
垃圾收集可能会发生在很长的一段时间之后,如果应用程序保留了对这些RDD的引用,或者垃圾收集不经常启动的话这
意味着对于一个运行时长较长的spark作业,它可能会消耗大量的磁盘空间。这些中间文件的存储目录在配置Spark 
Context时由spark.local.dir参数明确指定。
    
2、shuffle算子:
    重分区算子、ByKey算子(除了countByKey)、join()算子、cogroup/partitionBy都会触发shuffle。

2.宽窄依赖

1、介绍
窄依赖:父RDD的一个分区最多只能被一个子RDD使用。
    算子:map、union、map join和broadcast join。
宽依赖:父RDD的一个分区可被多个子RDD使用。
    算子:reduceByKey,groupByKey,常用join。 
 
2、原因
DAG:
    Spark中RDD的高效与DAG执行引擎有着莫大的关系,而在DAG调度过程中一个stage的拆分依赖的就是rdd间的
宽窄依赖关系。一个stage遇到一个宽依赖就会被拆分成两个stage,后一个stage的task会从前一个stage的task
所在节点拉取数据,这就是shuffle。一个stage遇到一个窄依赖,则会将窄依赖对应rdd纳入该stage进行计算。
 
重算:
    对于窄依赖,子rdd一个分区数据丢失只需要对一个父rdd进行重算,重算利用率100%。
    对于宽依赖,子rdd一个分区数据丢失需要多该分区依赖的所有父rdd分区进行重算,重算利用率低。  
 
3、并行度
    一个stage的并行度由stage的最后一个rdd的分区决定。
    可以通过spark.default.parallelism可以设置当前stage的并行度。
    提高并行度:shuffle类算子第二个参数、重分区算子。
 
4、数据落地
    suffle write(可能落盘)
    持久化(取决于持久化策略)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值