本章节讲一讲spark的Shuffle模块
shuffle模块作用是将若干node节点上面的数据重新分割,再划分到不同的节点中, 也就是将上一个stage中的各个task的中间结果整合起来,然后再重新分组,以供下一个stage的task对它们做运算。原因就是spark的设计就是把相具有某种共同特征的一类数据需要汇聚到一个计算节点上面进行计算。 所以shuffle的过程就像是把牌收集到了一起,洗一下,重新再发给牌友一样。
那么这里面的关键问题是:
1、一个shuffletask的输入来自于多个前置的task的汇聚,那么,shuffletask上面的内存使用量会增加。
2、为了节约带宽,可能需要将数据压缩以后再传
3、需要通过网络传输,序列化和反序列化也是个问题。
针对第1个问题,spark中的shuffle需要将数据汇聚,那么进行数据汇聚的那个点的数据是来自于多个其他的节点的,所以,可能就会存在单个节点的内存不够用的情况,这个时候,spark会将中间的数据写到磁盘里面。(*写文件然后又要读文件就会导致了spark运行时候效率变慢。)
Spark为什么要持久化shuffle的结果
task可以做到在内存中计算,某个过程出错后,可以从头开始。但是对于shuffle来说,一旦数据丢失了,需要从新计算这个结果,代价有点大,因为丢给shuffle的结果一般都是经过了好多个RDD后的结果。所以,shuffle被设计成为了可以持久化。
持久化的时机是shuffle中的任务计算完成后,这个过程其实是叫做shuffle write过程,从spark0.8以后,shuffle write 就被设计成为需要写磁盘。
shuffle结果是怎么样传送给下游的task的?
shuffle任务做完了以后,会通知scheduler.mapStatus,然后下游的task会通过这个mapStatus拉数据
如何确定shuffle后文件的数量呢?
首先,writer通过shuffleDependency#partition#numPartition获得需要生成的partition的数量,这个partition的数量是和下游的task的数量是相等的。对于单个task说,task处理的数据应该被设计成为可以完全能够载入内存,所以partition的大小的选择要能够满足task能够在内存中将任务做完。每个task处理一个partition。
shuffle算法
HashBasedShuffle
在spark中,shuffle的数据是没有排序的,所以,在shuffle的过程中会快。不过,正是由于数据是没有排序的,那么就不晓得数据的上下界,其实有的时候就会有不必要的网络开销。如果一开始就能够保证数据是有序的,那么后面的排序过程其实并没有那么多的时间开销。所以,感觉这个排序不排序是个需要折衷的方案。在mapreduce中,数据是需要排序的,在goldfish中,数据也是预先进行排序的,在GF中,数据排序后的收益是显著的。
需要注意的是,shuffle的结果只会写到本地文件系统,下游的task对应的task可能会来自于多台机器。
运行时候的图如下:shuffle也不是单节点的shuffle(spark也不会那么傻= =),多个shuffle任务共同做shuffle,然后后继的task再汇聚自己想要的数据。
有的问题:
(1)、打开文件的数量 , 比如上图中,文件就有followingtask*shufftask数目 4*4 = 16个
(2)、打开文件读取后内存占用
(3)、随机读取问题
除此之外,我想到的还有读取文件的排队问题,导致了下层任务推进变慢。
为了结果shuffle过程中产生文件过多的问题。在spark0.8.1中加入了shuffle consolidate Files 机制。
shuffle consolidate Files 机制
这个机制将属于一个core的同一个partition的不同的shuffle task 输出到同一个 partition file中。图示如下:
此外,除了hash based shuffle,还有sort based shuffle
sort based shuffle
hash based shuffle 的一个问题是,好生成的中间文件太多了。在sort based sort 中,同一个shuffle map task的输出会写到一个文件里面,同时,还会生成一个index 文件。reducer通过这个index文件来读取到自己所需要的输入文件。sort based shuffle按照partition的ID对数据进行排序,同一个partition的key是不会进行sort的。对于那种需要排序的操作,比如sortByKey是通过reducer来完成的。
那么,这具体是怎么做到的呢?
首先,Spark为每个partition在内存里面都创建了array,然后,将属于某个array的K/V对插入到这个array。这么看起来,与其说是排序,不如说就是在内存中对数据进行分组。
当某个array的内存超过阈值,就会将这个Array的数据写到磁盘,建立这个array的文件,记录partitionid,还有数据量。最后,将外部存储的文件进行归并排序,最后生成一个大文件,然后再生成一个index文件,作为索引,索引每个partition的起始位置。个人理解其实这里就有问题了,问题①一个array的内存超过阈值,不能够代表其他的array占用了很多空间,这个时候,内存可能还有很多的空间,因为其它的array可能没啥数据。问题②如果一个array就建立了一个文件,那么这个sortbased在写数据上面的性能又退化到了hash的方式其实,此外还有一个是归并排序的时间消耗问题。那么Spark之所以这样做的话,应该就是打开了文件后然后就关闭,因为中间有时间是错开的,那么在某个时刻,打开的文件数可能并不多,还有就是array在内存中充当了一个缓冲区的作用,这样使得数据量是批量写入的,这样的话,可以加大写文件的效率,还有就是如果有的array并不超多阈值,实际上是不用写文件的。 对此我的改进措施是 写到一个大的文件加index向量的方式,不过这样做依然会有问题,那就是,随着时间的推移,本来在一个partition的数据,会被分散在两个很远的磁盘位置上面,这样可能会增加磁盘寻道路的可能
如何计算出partition的数量?
计算的partition的个数依然是和following task 的数目是一样的。
在following task 如何 在index中,如果确定哪些部分的数据是自己要处理的呢?
shuffle map task运算结果的处理
这个和(二)中的一样。涉及到了executor和driver端对数据的处理。