标题sortShuffleManager

一、注册ShuffleHandle的策略
首先,在shuffle过程中满足以下条件,选择BypassMergeSortShuffleHandle:
1)map端没有聚合操作
2)shuffle read partitions <= spark.shuffle.sort.bypassMergeThreshold(阈值默认为200)
其次,满足以下条件,选择SerializedShuffleHandle:
1)序列化方式支持对象重新定位(意思是可以对已经序列化的对象进行排序,这种排序起到的效果和先对数据排序再序列化一致)
2)map/reduce端没有聚合操作
3)shuffle read partitions < 16777216(2^24:原因ShuffleInMemorySorter的成员变量LongArray数组中每个long类型元素只留24位给partitionId,其余40位留给了每条数据所在的page页编号及位置偏移量)
最后,如果不满足上述任意一个条件,就选择BaseShuffleHandle
二、选择shuffleWriter类型
根据ShuffleHandle类型选择shuffleWriter类型,其对照关系如下:
BypassMergeSortShuffleHandle -> BypassMergeSortShuffleWriter
SerializedShuffleHandle -> UnsafeShuffleWriter
BaseShuffleHandle -> SortShuffleWriter
三、不同类型shuffleWriter的适合场景
1)BypassMergeSortShuffleWriter
shuffleWriter期间,每个map task创建reduce个shuffle文件,根据记录key值对应的分区写到相应的文件;数据写完后,每个map task对应的所有中间文件被合并成一个数据文件和一个索引文件,索引文件中的每个Long类型数据代表前n个分区的累积字节量,比如第二个代表第一个分区的字节量,第三个代表前两个分区的字节量,注意第一个表示占位。
多个文件合并成一个文件的方式:使用FileChannel的transferTo方法执行文件流拷贝(NIO方式),NIO有个2G的限制,为了避免2G以上的文件拷贝的完整性,增加了一个循环判断机制。
缺点:对每个map task,BypassMergeSortShuffleWriter会同时打开与reduce分区同等个数的序列化器和文件流DiskBlockObjectWriter,即一次会打开多个文件,需要更多的内存分配给缓冲区


2)UnsafeShuffleWriter

shuffleWriter期间,每个map task首先将记录序列化写入MyByteArrayOutputStream的缓冲区(初始容量1M,自动扩容),然后计算分区并插入ShuffleExternalSorter,它会溢写产生许多临时shuffle文件,合并交给UnsafeShuffleWriter,可以避免数据反序列化。数据溢写过程和shuffle文件合并过程如下:
ShuffleExternalSorter溢写过程:
每当记录数达到某一阈值(spark.shuffle.spill.numElementsForceSpillThreshold默认Integer.MAX_VALUE)时,调用ShuffleExternalSorter溢写数据到磁盘,溢写前会对数据进行排序(默认使用RadixSort排序方式,可以通过参数spark.shuffle.sort.useRadixSort配置选择TimSort排序)。
ShuffleExternalSorter用于管理内存的申请和释放。它有两个重要的成员变量:分配的page链表和ShuffleInMemorySorter(内含longArray数组),记录被插入page页的同时也会进入longArray。
这里的内存是以内存页Page的形式存在。Page个数上限为213,每个page申请的内存为MAX(pageSize,require)的最大值,其中pageSize值为MIN(227byte=128MB, spark.buffer.pageSize)的最小值。
在申请内存期间,会按顺序判断以下条件:条件一,如果page申请内存大于(2^31-1)*8byte~17GB(常量MAXIMUM_PAGE_SIZE_BYTES),就会抛出TooLargePageException异常;条件二,如果未申请到所需大小内存,将触发数据溢写过程,首先从其他MemoryConsumer溢写数据释放内存,释放的内存仍然不够才会选择从当前MemoryConsumer释放内存,这样可以减少溢写次数,避免产生太多的溢写文件。
PS:
(1)spark.buffer.pageSize是spark的配置参数,其默认值是根据资源情况动态计算获得,位于1M~64M之间,也可以配置默认值。
(2)MAXIMUM_PAGE_SIZE_BYTES是TaskMemoryManager类的常量,其值为(2^31-1)*8L,
ShuffleInMemorySorter是专门用来给数据排序的,它内部包含了一个存储数据指针的LongArray数组,存放原信息编码后的数据,原信息为partitionId + pageNum + offsetInPage,它们的长度分别是24,13,27位。LongArray数组初始容量的大小为4096(32K),最大为Integer.MAX_VALUE(由溢写记录上限阈值决定),该数组的可用空间由排序方式决定,默认排序方式RadixSort可用空间占总空间的1/2,TimSort占2/3,当可用空间达到该阈值,就会对数组进行扩容或溢写数据到磁盘。溢写数据前,按partitionId对数据做排序(注意没有key)。
然后循环遍历排序后的LongArray数组,从编码后的原信息数据中反解出当前记录所在内存Page页编号及Page页中的偏移offset,分批次将相同分区记录从内存页中拷贝到字节数组对象中并溢写到临时文件中,
每批次拷贝的数据都是同一分区,且最大数据量为字节数组的大小(spark.shuffle.spill.diskWriteBufferSize,默认为1MB),所有数据溢写完后释放内存页并重置LongArray数组。
每次溢写完数据都会生成一个临时文件,该临时文件对应一个SpillInfo对象,里面记录着临时文件所在磁盘位置、所有分区下每个分区的数据量。多次溢写创建的SpillInfo对象会被一一放入LinkedList集合中,据此将所有临时文件合并成一个最终文件。
UnsafeShuffleWriter合并临时shuffle文件过程:
一个map task触发多次溢写产生的多个磁盘临时文件,UnsafeShuffleWriter会根据LinkedList对其进行合并处理。合并之前,会将最后一批还未溢写到磁盘的数据按溢写过程写到临时文件,然后开始临时文件的合并过程,根据临时文件个数和IO压缩编码选择不同的合并策略(现有4种压缩编码lz4、lzf、snappy、zstd,默认值lz4)。如果临时文件只有一个,就通过move方式将临时文件变更为最终文件;如果临时文件超过一个,有快速合并(默认)和慢速合并两种方式。

扩充ShuffleExternalSorter


扩充溢写过程

3)SortShuffleWriter
溢写过程:
shuffleMapTask写数据时会创建对象ExternalSort,完成数据的排序、溢写以及临时文件合并。如果map端有聚合,ExternalSort内部使用PartitionedAppendOnlyMap数据结构当缓冲区,否则使用PartitionedPairBuffer缓冲区,它们的初始容量均为64。
这两种缓冲区内部都采用一维Array[AnyRef]来放数据,偶数位放(partitionId,key),奇数位放value,数组中存放的是对象的引用,这种方式非常节省数组占用的内存空间,它可以实现动态扩容。
当数据量达到阈值(spark.shuffle.spill.initialMemoryThreshold,默认5MB),或数据记录数超过阈值(spark.shuffle.spill.numElementsForceSpillThreshold默认Integer.MAX_VALUE)时,触发溢写操作,此时按partitionid和key排序数据(TimSort),
然后使用迭代器开始遍历缓冲区数据,按批次溢写数据到磁盘临时文件,直至缓冲区数据溢写完毕,批次大小由参数spark.shuffle.spill.batchSize控制,默认值10000。
临时文件合并:
一个shuffleMapTask触发多次溢写产生的多个磁盘临时文件,ExternalSort会将这些文件合并成一个文件,并生成索引文件。

四、shuffleReader
关键点:fetch数据
按缓冲区大小拉去数据,每次拉去数据量spark.reducer.maxSizeInFlight默认48M

shuffle read的拉取过程是一边拉取一边进行聚合的。每个shuffle read task都会有一个自己的buffer缓冲,每次都只能拉取与buffer缓冲相同大小的数据,然后通过内存中的一个Map进行聚合等操作。
(1) sparkSQL之聚合策略
聚合策略分两种,hash和sort,首选前者,没有额外的排序操作,而且hash聚合运算符使用堆外内存做缓存,没有GC。后者需要排序操作,当聚合后的所有数据类型(比如sum,avg等)都是基本的数据类型时,使用堆外内存,否则堆内内存。

ObjectHashAggregate是基于hash的聚合方法。它是在spark2.2.0版本引进来的,是HashAggregate升级版,弥补了之前不支持用户自定义的UDAF和一些聚合相关的函数(比如:collect_list,collect_set)的不足。它将同一分区的数据拉取到同一个excutor后,按行开始逐一遍历,把相同key的数据缓存到Map的聚合缓冲区。
如果Map中的键数量达到回退阈值(默认128,HashAggregateExec触发溢写的机制是遍历的行数达到回退阈值Int.MaxValue或者申请不到更多的内存),将对数据按key做排序并溢写到磁盘,同时将当前Map的排序信息保存到磁盘。之后再新建一个Map,用于处理余下数据,如果再次发生溢写,每次都将新旧Map排序信息合并在一起。
所有数据处理完后,Map的排序信息被用来作为SortAggregateExec的输入。

SortAggregate是基于排序的聚合方法。首先将reshuffle(shuffle过程)后相同分区的数据从多个excutor拉取到同一个excutor,然后按key排序,以便将相同key的数据放在一起。之后,开始第一分组(相同key)遍历,处理完毕后输出结果并清空缓存,开启下一分组的遍历,直至处理完所有分组。
https://zhuanlan.zhihu.com/p/563588812

(2) sparkSQL之join策略
join策略分五种,BroadcastHashJoin、ShuffleHashJoin、SortMergeJoin、CartesianProduct、BroadcastNestedLoopJoin。它们均适用于等值连接,只有笛卡尔乘积和广播嵌套循环支持非等值连接。
join策略的选择规则是,首先判断是否为等值连接,由于常用的是等值连接,这里只阐述等值连接的join策略选取规则。开发人员指定的连接提示对于策略的选取具有最高优先级,如果没有指定连接提示或者连接提示不符合连接条件,将根据条件选取合适的join策略。
如果join的两张表中,存在一张小表,它的数据量小于阈值spark.sql.autoBroadcastJoinThreshold(默认10MB),将选取BroadcastHashJoin,将小表全量数据拉取到driver端,

https://mp.weixin.qq.com/s?__biz=Mzg5NTE5ODUzMA==&mid=2247484988&idx=1&sn=ae57f6f36bb6a1f87fafde1e3486d723&chksm=c012b211f7653b071a356af84b9579f8d641e37f798bd1146b8e313e6983176ba6816df9b88f&cur_album_id=1962974926114488321&scene=189#wechat_redirect