shuffle过程详解(spark与mr)

MR shuffle

在这里插入图片描述
shuffle属于不断被优化和改进的代码库,是MapReduce的“心脏”。
shuffle可以将其定义为:map的输出到reduce的输入(在一些语境中,代表reduce接受map输出的这部分)

1、map端

我们知道map产生的输出是临时写到本地磁盘的,但是他并不是简单的写到本地磁盘中,这个过程更为复杂,如图:
在这里插入图片描述
① 分区partition
② 写入环形内存缓冲区
③ 执行溢出写
排序sort—>合并combiner—>生成溢出写文件
④ 归并merge

  1. 分区Partition
    在将map()函数处理后得到的(key,value)对写入到缓冲区之前,需要先进行分区操作,这样就能把map任务处理的结果发送给指定的reducer去执行,从而达到负载均衡,避免数据倾斜
  2. 写入缓存
    频繁的磁盘I/O操作,“中间结果”优先存储到map节点的“环形内存缓冲区”.当写入的数据量(被缓冲的(key,value)对已经被序列化)达到预先设置的阙值(80%)后便会执行一次I/O操作将数据写入到磁盘.
  3. 执行溢写出
    缓冲区内容达到阈值,并在每个分区中对其中的键值对按键进行sort排序,具体是将数据按照partition和key两个关键字进行排序,排序结果为缓冲区内的数据按照partition为单位聚集在一起,同一个partition内的数据按照key有序。
    (如果客户端自定义了Combiner(相当于map阶段的reduce),则会在分区排序后到溢写出前自动调用combiner,将相同的key的value相加,这样的好处就是减少溢写到磁盘的数据量。这个过程叫“合并”)
  4. 归并merge
    一个map task处理的数据很大,就会生成多个spill文件.这时进行归并生成最终的一个已分区且已排序的大文件.Copy阶段分区输出文件通过http的方式提供给reducer)

他会首先使用缓冲的方式写入到内存中,并且处于效率的考虑进行预排序。每个map都有一个缓冲区用于存储任务输出,这个缓冲区的大小默认为100MB,可以通过io.sort.mb属性调整。一旦缓冲的内容达到预设的阀值(通过io.sort.spill.percent,默认是0.8或80%),一个后台进程便把内容溢出(spill)到磁盘。在溢出过程map输出会继续写到缓冲区,如果在此期间被填满,则发生堵塞,直到写磁盘过程完成。这个溢出写的过程会将数据写到mapreduce.cluster.local.dir指定的目录内。在写硬盘之前会根据输出的reduce进行分区(partition),然后对每个分区内容进行排序,如果有combiner函数,则在排序之后执行combiner。每次内存缓冲区达到溢出的阀值,就会新建一个溢出文件(spill file)。最终会有几个溢出文件,这些溢出文件会被合并成一个已分区且已排序的输出文件,io.sort.factor控制一次最多能够合并多少流,默认是10。如果至少有3个溢出文件(这个值由min.num.spills.for.combine属性设置)则就会在输出文件写到磁盘之前在此运行以此combiner。将输出进行压缩可以减少输出及传递到reduce的网络开销,可以设置mapreduce.compress.map.output设置为true,使用mapreduce.map.output.compression.codec指定压缩方法。

2、reduce端

在这里插入图片描述
①复制copy
②归并merge
③reduce

  1. 复制copy
    Reduce进程启动一些数据copy线程,使用心跳机制通知它们的application master通过HTTP方式请求MapTask所在的NodeManager以获取(只需拷贝与自己对应的partition中的数据即可)的输出文件。
  2. 归并merge 和sort
    Copy 过来的数据会先放入内存缓冲区中,Map的输出数据已经是有序的,Merge进行一次合并排序,所谓Reduce端的sort过程就是这个合并的过程,采取的排序方法跟map阶段一样,针对键进行排序。
    最终Reduce shuffle过程会输出一个整体有序的数据块。

reduce任务需要集群中若干个map的输出作为其输入,但是每个map的完成时间并不一样,所以只要有一个map输出,reduce就开始复制其输出,这就是reduce端的复制阶段。reduce有少量的复制线程,默认是5个,这个值由mapreduce.reduce.parallel.copies属性改变。
那么reduce如何知道从哪台机器获取map输出呢?
map任务完成后,会通知其父tasktracker,tasktracker会通知jobtracker(在MR2中是applicationMaster),从而jobtracker(applicationMaster)知道了tasktracker与map的映射关系,reduce中的一个线程会定期向applicationMaster(或者jobtracker)进行询问,以便获取map输出的位置。
复制完成后,reduce开始进入排序阶段(其实是合并节阶段,因为排序是在map端进行的),这个阶段合并map输出,保持其排好的顺序。这个合并是循环进行的,可以设置合并因子io.sort.factor,默认是10,即每趟合并10个文件,假设总共50个map,总共进行5趟,最终有5个中文文件。之后是reduce阶段,直接把数据输入到reduce函数,而不用将这5个文件合并称一个大文件。reduce函数输出直接写到HDFS上。

配置调优

map端的调优属性:
属性名称类型默认值说明
io.sort.mbint100map输出所使用的内存缓冲区大小,以MB为单位
io.sort.spill.percentfloat0.80缓冲区预设的阀值,超过这个百分比开始将内容溢到磁盘
io.sort.factoryint10排序文件时一次最多合并的流数,在reduce端也是用
min.num.spills.for.combineint3运行combiner所需要最少溢出文件数
mapreduce.compress.map.outputBooleanfalse压缩map输出 mapreduce.map.output.compression.codec Class Name org.apache.hadoop.io.compress.DefaultCodec 用于map输出的压缩编码器
tasktracker.http.threadsint40每个tasktracker运行的线程数,用于将map输出到reduce,在YARN不适用

这个过程总的来说就是要为shuffle分配更多的内存,但是这时候可能还需要考虑到map函数和reduce函数能够得到足够运行的内。所以一般map函数和reduce函数在编写的时候尽量少占内存。map端可以通过避免多次溢出写磁盘来获得最佳性能,一次是最佳的情况。

reduce端的调优属性
属性名称类型默认值说明
mapreduce.reduce.parallel.copiesint5用于把map的输出复制到reduce的线程数
mapreduce.reduce.copy.backoffint300在声明失败之前,reducer获取一个map输出所花的最大时间,以秒为单位,如果失败,reducer可以在此时间内尝试重传
io.sort.factorint10排序合并时的合并因子
mapreduce.iob.shuffle.merge.percentfloat0.66map输出缓冲区(上面定义的那个)的阀值使用比例,用于启动合并输出和磁盘溢出写的过程
mapreduce.inmem.merge.thresholdint1000启动合并输出和磁盘溢出写过程的map的输出的阀值数。0或更小,意味着没有阀值限制
mapreduce.iob.reduce.input.buffer.percentfloat0.0在reduce过程,在内存中保存map输出的空间占整个堆空间的比例。reduce阶段开始时,内存中的map输出不能大于这个值

spark shuffle

一.定义

Shuffle的本意是洗牌,目的是为了把牌弄乱。
Spark、Hadoop中的shuffle可不是为了把数据弄乱,而是为了将随机排列的数据转换成具有一定规则的数据.

二.演变

在这里插入图片描述
Spark1.1.x addSortShuffle 有排序
Spark1.5 addd unsafe shuffle
spark1.6 hash+sort +unsafe => interagrate
spark2.0 remove hash

三.Hash Shuffle V1

在这里插入图片描述

有多少个reduce任务就会产生多少个中间文件(一个task—>所产生的文件数量=== reduce task数量)

Hash Shuffle V1 过程

1:shuffle write阶段

主要就是在一个stage结束计算之后,为了下一个stage可以执行shuffle类的算子(比如reduceByKey,groupByKey),而将每个task处理的数据按key进行“分区”。所谓“分区”,就是对相同的key执行hash算法,从而将相同key都写入同一个磁盘文件中,而每一个磁盘文件都只属于reduce端的stage的一个task。在将数据写入磁盘之前,会先将数据写入内存缓冲中,当内存缓冲填满之后,才会溢写到磁盘文件中去。

2:shuffle read阶段

shuffle read,通常就是一个stage刚开始时要做的事情。此时该stage的每一个task就需要将上一个stage的计算结果中的所有相同key,从各个节点上通过网络都拉取到自己所在的节点上,然后进行key的聚合或连接等操作。由于shuffle write的过程中,task给Reduce端的stage的每个task都创建了一个磁盘文件,因此shuffle read的过程中,每个task只要从上游stage的所有task所在节点上,拉取属于自己的那一个磁盘文件即可。
shuffle read的拉取过程是一边拉取一边进行聚合的。每个shuffle read task都会有一个自己的buffer缓冲,每次都只能拉取与buffer缓冲相同大小的数据,然后通过内存中的一个Map进行聚合等操作。聚合完一批数据后,再拉取下一批数据,并放到buffer缓冲中进行聚合操作。以此类推,直到最后将所有数据到拉取完,并得到最终的结果。

注意:

1).buffer起到的是缓存作用,缓存能够加速写磁盘,提高计算的效率,buffer的默认大小32k。
分区器:根据hash/numRedcue取模决定数据由几个Reduce处理,也决定了写入几个buffer中
block file:磁盘小文件,从图中我们可以知道磁盘小文件的个数计算公式:
block file=M*R
2).M为map task的数量,R为Reduce的数量,一般Reduce的数量等于buffer的数量,都是由分区器决定的
Hash shuffle普通机制的问题
1).Shuffle前在磁盘上会产生海量的小文件,建立通信和拉取数据的次数变多,此时会产生大量耗时低效的 IO 操作 (因為产生过多的小文件)
2).可能导致OOM,大量耗时低效的 IO 操作 ,导致写磁盘时的对象过多,读磁盘时候的对象也过多,这些对象存储在堆内存中,会导致堆内存不足,相应会导致频繁的GC,GC会导致OOM。由于内存中需要保存海量文件操作句柄和临时信息,如果数据处理的规模比较庞大的话,内存不可承受,会出现 OOM 等问题。

四.Hash Shuffle V2( spark.shuffle.consolidateFiles = true)

在这里插入图片描述

一个Executor—>所产生的文件数量=== reduce task数量

Hash Shuffle V2 过程:

  1. 开启consolidate机制之后,在shuffle write过程中,task就不是为下游stage的每个task创建一个磁盘文件了。此时会出现shuffleFileGroup的概念,每个shuffleFileGroup会对应一批磁盘文件,磁盘文件的数量与下游stage的task数量是相同的。一个Executor上有多少个CPU core,就可以并行执行多少个task。而第一批并行执行的每个task都会创建一个shuffleFileGroup,并将数据写入对应的磁盘文件内。
  2. Executor的CPU core执行完一批task,接着执行下一批task时,下一批task就会复用之前已有的shuffleFileGroup,包括其中的磁盘文件。也就是说,此时task会将数据写入已有的磁盘文件中,而不会写入新的磁盘文件中。因此,consolidate机制允许不同的task复用同一批磁盘文件,这样就可以有效将多个task的磁盘文件进行一定程度上的合并,从而大幅度减少磁盘文件的数量,进而提升shuffle write的性能。
注意:

1).启动HashShuffle的合并机制ConsolidatedShuffle的配置:
spark.shuffle.consolidateFiles=true
2).block file=Core*R
Core为CPU的核数,R为Reduce的数量
Hash shuffle合并机制的问题
如果 Reducer 端的并行任务或者是数据分片过多的话则 Core * Reducer Task 依旧过大,也会产生很多小文件。

五.Sort Shuffle

在这里插入图片描述

一个task—>所产生的文件数量===2

Sort Shuffle 过程

该图说明了普通的SortShuffleManager的原理。在该模式下,数据会先写入一个内存数据结构中(默认5M),此时根据不同的shuffle算子,可能选用不同的数据结构。如果是reduceByKey这种聚合类的shuffle算子,那么会选用Map数据结构,一边通过Map进行聚合,一边写入内存;如果是join这种普通的shuffle算子,那么会选用Array数据结构,直接写入内存。接着,每写一条数据进入内存数据结构之后,就会判断一下,是否达到了某个临界阈值。如果达到临界阈值的话,那么就会尝试将内存数据结构中的数据溢写到磁盘,然后清空内存数据结构。

注意:

shuffle中的定时器:定时器会检查内存数据结构的大小,如果内存数据结构空间不够,那么会申请额外的内存,申请的大小满足如下公式:
applyMemory=nowMenory2-oldMemory
**申请的内存=当前的内存情况
2-上一次的内嵌情况**
意思就是说内存数据结构的大小的动态变化,如果存储的数据超出内存数据结构的大小,将申请内存数据结构存储的数据*2-内存数据结构的设定值的内存大小空间。申请到了,内存数据结构的大小变大,内存不够,申请不到,则发生溢写

  1. 排序
    在溢写到磁盘文件之前,会先根据key对内存数据结构中已有的数据进行排序。
  2. 溢写
    排序过后,会分批将数据写入磁盘文件。默认的batch数量是10000条,也就是说,排序好的数据,会以每批1万条数据的形式分批写入磁盘文件。写入磁盘文件是通过Java的BufferedOutputStream实现的。BufferedOutputStream是Java的缓冲输出流,首先会将数据缓冲在内存中,当内存缓冲满溢之后再一次写入磁盘文件中,这样可以减少磁盘IO次数,提升性能。
  3. merge
    一个task将所有数据写入内存数据结构的过程中,会发生多次磁盘溢写操作,也就会产生多个临时文件。最后会将之前所有的临时磁盘文件都进行合并,这就是merge过程,此时会将之前所有临时磁盘文件中的数据读取出来,然后依次写入最终的磁盘文件之中。此外,由于一个task就只对应一个磁盘文件,也就意味着该task为Reduce端的stage的task准备的数据都在这一个文件中,因此还会单独写一份索引文件,其中标识了下游各个task的数据在文件中的start offset与end offset。
注意:
  1. block file= 2M
    一个map task会产生一个索引文件和一个数据大文件
  2. m*r>2m(r>2):SortShuffle会使得磁盘小文件的个数再次的减少

Sort Shuffle–bypass

在这里插入图片描述
也可以无排序,按照key做hash节省了性能开销—Sort Shuffle–bypass

一个task—>所产生的文件数量===2

bypass运行机制的触发条件如下:

  1. shuffle map task数量小于spark.shuffle.sort.bypassMergeThreshold参数的值。
  2. 不是聚合类的shuffle算子(比如reduceByKey)。

Sort Shuffle–bypass过程:

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

shuffle过程中的临时文件写到哪里?

1.在 /tmp 下
2.配置路径
SPARK_LOCAL_DIRS=/hom/lyb/spark_log1,/hom/lyb/spark_log1,
备注:
1、需在每个worker节点上设置,可设置成不一样的值
2、worker节点的磁盘不做raid磁盘列阵
3、可设置以逗号分隔的多个值,提高IO性能。

总结:

  1. Shuffle 过程本质上都是将 Map 端获得的数据使用分区器进行划分,并将数据发送给对应的 Reducer 的过程。
  2. shuffle作为处理连接map端和reduce端的枢纽,其shuffle的性能高低直接影响了整个程序的性能和吞吐量。map端的shuffle一般为shuffle的Write阶段,reduce端的shuffle一般为shuffle的read阶段。Hadoop和spark的shuffle在实现上面存在很大的不同,spark的shuffle分为两种实现,分别为HashShuffle和SortShuffle,
    HashShuffle又分为普通机制和合并机制,普通机制因为其会产生MR个数的巨量磁盘小文件而产生大量性能低下的Io操作,从而性能较低,因为其巨量的磁盘小文件还可能导致OOM,HashShuffle的合并机制通过重复利用buffer从而将磁盘小文件的数量降低到CoreR个,但是当Reducer 端的并行任务或者是数据分片过多的时候,依然会产生大量的磁盘小文件。
  3. SortShuffle也分为普通机制和bypass机制,普通机制在内存数据结构(默认为5M)完成排序,会产生2M个磁盘小文件。而当shuffle map task数量小于spark.shuffle.sort.bypassMergeThreshold参数的值。或者算子不是聚合类的shuffle算子(比如reduceByKey)的时候会触发SortShuffle的bypass机制,SortShuffle的bypass机制不会进行排序,极大的提高了其性能
  4. 在Spark 1.2以前,默认的shuffle计算引擎是HashShuffleManager,因为HashShuffleManager会产生大量的磁盘小文件而性能低下,在Spark 1.2以后的版本中,默认的ShuffleManager改成了SortShuffleManager。SortShuffleManager相较于HashShuffleManager来说,有了一定的改进。主要就在于,每个Task在进行shuffle操作时,虽然也会产生较多的临时磁盘文件,但是最后会将所有的临时文件合并(merge)成一个磁盘文件,因此每个Task就只有一个磁盘文件。在下一个stage的shuffle read task拉取自己的数据时,只要根据索引读取每个磁盘文件中的部分数据即可。

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

  • 11
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值