spark shuffle有四种方式,分别是
- hashshuffle
- 优化后的hashshuffle
- sortshuffle
- bypass
一、hashshuffle与优化
一开始spark的shuffle方式是hashshuffle。hashshuffle有一个严重的问题,就是产生的小文件数量比较多。
我们知道,shuffle分为map端的shuffle write 和reduce端的shuffle read。
hashshuffle,每个task为下游每一个task都创建了一个文件,所以就产生了M*R的小文件数量,其中M是map的task数量,R是reduce的数量。
为了减少小文件的数量,spark随后提出了优化后的hashshuffle,即每个core处理的task文件的结果合并起来
比如:task1 write了3个小文件,里面是keyA keyB keyC,分别叫BlockA BlockB BlockC 吧
这时又来了一个task2,优化前task2 也产生三个小文件BlockA BlockB BlockC。这样就有6个小文件
但是task2 和task1是同一个core处理的,所以task2的BlockA BlockB BlockC 写入了task1 的BlockA BlockB BlockC,由于同一个文件中是同一个下游task处理的,所以只要追加写入就可以了。
该优化方法,本质上是小文件的合并(同一个core的小文件数量合并),朝着这个方向,我们可以继续优化成“同一个executor(jvm)产生的同一个下游task的小文件合并”,和同一台机器合并,不过spark并没有实现这些优化。
由于同一个stage里的map的task不分先后顺序,但是同一个core里的task是先后处理的。所以同一个jvm或者同一台机器的小文件合并,没有同一个core的合并那么简单。
spark也提出过内存共享的优化方法,跨executor(jvm)去共享一台机器上面的内存。
该优化最终将小文件数量下降了X倍,X为平均每个core处理的task数量,
- 比如,有100个map,下游是500个reduce,那原本是100*500=5w的小文件数量,现在map端分配的core是20个(每个core处理5个task),就变成了20*500=1w,缩小了5倍。
该优化方法虽然好,但是受限于机器并行的能力,如果机器并行的能力强,分配的core数量接近于map的数量,该优化就十分有限。
二、sortshuffle
为了实现更进一步的小文件合并,spark在随后提出了sortshuffle。
sortshuffle的目的,还是实现"更宽"的“共享”。
上述讲到hashshuffle对spark的小文件做了同一个core的合并,但是由于不同core的task没有先后顺序,很难合并。sortshuffle就是为了实现“每台机器上的map task小文件都合并”。
因为一台机器上的task之间执行没有顺序,所以要等待所有的task执行完成。无法有效利用并发能力。
如果按照之前的做法,为每个reduce task创建一个文件,并且同一台机器上的同一个reduce task的文件合并的话,这样最多能节省的倍数为平均一台机器处理的task数量,
- 比如,有100个map,下游是500个reduce,那原本是100*500=5w的小文件数量,现在map端分配的core是20个,20个core在5台机器上(机器是4核的),就变成了5*500=2000,缩小了20倍。
显然优化者不满足于这种程度的优化,于是优化者这次对"*500" 动手了(reduce task的数量)。如何优化"*500", 原来一个map task 会产生500个task,现在将这些都合并了。所以就没有了"*500"
- 比如,有100个map,下游是500个reduce,那原本是100*500=5w的小文件数量,现在合并了同一个map task产生的reduce task,那么文件数量就变成了100个。虽小了500倍,(由于要增加索引文件,所以实际上要除以2,是250倍)
只不过如果一个map,为500个reduce task 产生一个文件,这500个reduce task要怎么使用呢?答案是排序+索引,这就是sortshuffle的由来。
为了实现500个reduce task对一个文件的高效操作,map write的文件内部是有序的,并且还为该文件提供了一个索引文件。这样reduce task 就可以根据索引文件在该文件中找到自己对应的数据了。
该方式的map write的方式也发生了改变,类似于lsm的操作那样,每次内存写满了就一次性把内存数据进行排序,写入到磁盘生成一个文件,等到所有的数据都写入磁盘后,再对所有生成的文件进行一个归并排序。
这种shuffle的好处是大大减少了小文件的数量,且优化的力度不受限于reduce task的数量,只受限于map的数量。坏处是增加了一个排序的开销。
既然map write产生的文件都是有序的,为什么不再对同一台机器或core的的文件进行归并呢。答案是没有必要,太多的小文件会增大网络IO的开销,太少的文件对降低并发的利用率。不过,如果上游的数据量很大(map数量很多),而该key的基数(去重后的数量)比较小的话,还是可以考虑按core或jvm或机器级别进行合并的(合并还要考虑到索引的合并),不过spark没有提供这类优化。
三、bypass
bypass是sortshuffle的一种优化。上述提到,map write的文件内部做了全局排序。是为了多个reduce task能找到各自需要read的数据。但是在一个reduce task内,数据并不需要有序,而reduce task的数量一般远远小于数据行数(数据key的基数),所以这就造成了计算的浪费。
为了停止这种浪费,spark提出了新的shuffle优化bypass,即改变map write的方式,在write的时候,原来要对每一个flush到磁盘的小文件进行排序,现在不排序了,复制之前hashshuffle的做法,为每个reduce task写一个小文件。最后,将这些同一个map产生的小文件合并成一个大文件,合并的方式很简单,就直接追加就可以了,最后对结果文件增加一个索引,这样下游每一个reduce task都能找到自己要读的数据。这样既省下了排序的开销,又将小文件数量缩小到了2*M的数量(和sortshuffle一样)。可谓是兼具了hashshuffle和sortshuffle的优点。
不过该方法也有限制的地方。该shuffle不支持预聚合,map的数量也尽量要小(和最初的hashshuffle一样,map数量过大会产生过多的临时文件)
触发bypass的map数量上限可以用参数 spark.shuffle.sort.bypassMergeThreshold 设置