Spark
两种Shuffle
在spark1.1以前只有hashshuffle,1.1版本引入了sortshuffle,1.2版本以后默认方式改为sort方式,2.0版本以后移除了hashshuffle。
HashShuffle
- 执行原理:
Map阶段的shuffle是为了下一个stage的task拉取数据作的。
- 每个Map阶段task把要输出的数据按key进行hash
- 根据hash得到的值,生成和下一个stage的task数量相同的磁盘文件并写入。
- 在将数据写入磁盘之前,会先将数据写入内存缓冲中,当内存缓冲填满之后,才会溢写到磁盘文件中去。
- 产生文件:
假设下一个stage有st个task,当前stage有e个exector,每个excutor有t个task,那么一共会产生e*t*st个磁盘文件 - shuffleRead:
通常是一个stage刚开始时要做的事情,把本stage需要的数据从上一个stage阶段shuffle的数据拉进来。在HashShuffle下,shuffleRead只用让每个task从自己的那个文件里拉取数据进缓冲区进行聚合,并且一边拉取一边进行聚合,每次拉取buffer缓冲区大小的数据,再传输到内存中。 - 原理图:
基于Consolidate(合并)机制的HashShuffle
配置spark.shuffle.consolidateFiles为true,默认为false
- 执行原理:
基础的hashShuffle会产生大量的文件,因此优化的HashShuffle对它进行优化。其他不变,在基础HashShuffle写出文件时,上一stage执行的第一批task会为每个下一stage的task创建一个磁盘文件,每个task生成的这些文件称为一个shuffleFileGroup,接下来运行的每个task就不再产生新的文件,而是根据自己的shuffleFileGroup,把数据hash到属于自己组的文件中。 - 产生文件:
假设下一个stage有st个task,当前有cores个cpu核数(每个核运行一个task),那么一共会产生cors*st个磁盘文件 - 原理图:
HashShuffle的优缺点
- 优点
- 避免排序带来的cpu和内存的开销
- 缺点
- 产生的磁盘文件太多,存在内存、磁盘的开销,同时io操作太耗时。
普通的SortShuffle
在该模式下,数据会先写入一个内存数据结构中,此时根据不同的 shuffle 算子,可能选用不同的数据结构。如果是 reduceByKey 这种聚合类的 shuffle 算子,那么会选用 Map 数据结构,一边通过 Map 进行聚合,一边写入内存;如果是 join 这种普通的 shuffle 算子,那么会选用 Array 数据结构,直接写入内存。接着,每写一条数据进入内存数据结构之后,就会判断一下,是否达到了某个临界阈值。如果达到临界阈值的话,那么就会尝试将内存数据结构中的数据溢写到磁盘,然后清空内存数据结构。
- 执行原理:
- 在写入磁盘之前,会针对key对数据进行sort操作,然后分批写入磁盘,默认的batch大小是一万条,即每次写入一万条数据到一个磁盘文件(一个磁盘文件可能有多批的数据),此时产生的个个溢写文件是临时文件 。注意,排序后并不是直接写入磁盘,而是通过java的BufferedOutputStream写入的,也就是说首先会写入内存缓冲区,缓冲区满之后在溢写到磁盘。
- 每个task进行分批溢写磁盘之后,会有一个合并操作,将每次溢写产生的临时磁盘文件读取出来以后依次写入同一个文件,也就意味着一个task只会生成一个磁盘文件,同时,该task会给这个文件生成一个索引文件,其中标识了下游各个 task 的数据在文件中的 start offset 与 end offset。即每个task会生成两个文件(数据文件、索引文件)。
- 产生文件:
假设当前stage有n个task,则最终会产生n*2个文件 - shuffleread:
下游stage开始的读取数据阶段时,直接通过索引文件在每个task生成的文件中拉取即可。 - 原理图:
基于bypass的SortShuffle
当下一个stage的task数较少的情况下,hashShuffle会比sortShuffle方式更快,因为它少了排序开销。当 下一个stage任务数少于配置属性spark.shuffle.sort.bypassMergeThreshold
设置的个数时,会启用该机制进行优化。
触发条件:
- 下一个stage任务数少于配置属性
spark.shuffle.sort.bypassMergeThreshold
设置的个数 - 不是聚合类的shuffle算子
- 执行原理:
- 执行采用普通的HashShuffle机制进行,即当前task会为下一stage的每个task都产生一个临时文件(通过缓冲的方式),但是在最后会把这些临时文件合并成对应当前task的一个文件,这对于下一stage的拉取数据更高效。
- 产生文件:
假设当前stage有n个task,则最终会产生n*2个文件 - shuffleread:
下游stage开始的读取数据阶段时,直接通过索引文件在每个task生成的文件中拉取即可。 - 原理图:
Tungsten Sort Shuffle
这种方式过程和普通的SortShuffle方式一样,但是sort操作时针对序列化后的字节数组指针进行排序的,而不是对原始数据进行排序,而是对二进制数据进行排序,因此算法效率会高很多。但是可以看出它不能适应需要对内容进行聚合的操作。
触发这种Shuffle方式条件比较苛刻:
- Shuffle 依赖中不带聚合操作或没有对输出进行排序的要求。
- Shuffle 的序列化器支持序列化值的重定位(当前仅支持 KryoSerializer Spark SQL 框架自定义的序列化器)。
- Shuffle 过程中的输出分区个数少于 16777216 个
- 其他的一些限制
使用哪种sortshuffle的判断流程
- 通过 SortShuffleWriter.shouldBypassMergeSort 方法判断是否需要回退到 Hash 风格的 Shuffle 实现机制(bypass机制)
- 当该方法返回的条件不满足时,则通过 SortShuffleManager.canUseSerializedShuffle方法判断是否需要采用基于 Tungsten Sort Shuffle 实现机制
- 当这两个方法返回都为 false,即都不满足对应的条件时,会自动采用普通运行机制。
三种SortShuffle的优缺点
- 普通的SortShuffle会对文件排序,所以会有排序开销,在下一stage阶段task较少的情况下会比较低效。但是它通过合并文件的机制减少了最终的磁盘数量,并且中间文件的数量也比hash方式少,加快了速度,减少了io开销。
- bypass机制的SortShuffle针对普通SortShuffle在下一stage阶段task较少的情况下会比较低效的缺点,当满足出发条件时采用hashShuffle的方式进行Shuffle操作,但是最后会把文件合并,减少最终生成的文件数量,提高了shuffleread的效率,并且没有排序操作,提高了少文件数时的速度。
- Tungsten的使用存在限制,不能适应带有聚合操作的shuffle,触发条件苛刻,