什么是shuffle?
发生 shuffle 操作主要是以下几个算子:groupByKey、reduceByKey、countByKey、join,等等。
什么时候需要shuffle writer?
前一个stage的ShuffleMapTask进行shuffle write,把数据存储在blockManager上面,并且把数据位置元信息上报到driver的mapOutTrack组件中,下一个stage根据数据位置元信息,进行shuffle read ,拉取上个stage的输出数据
spark2.1之前的shuffle方式
Hash Based Shuffle(该方式已于spark2.0废弃) :
刚开始 每一个Mapper会根据Redude的数量创建出相应的bucket,bucket的数量是MR, 其中 M是Map的个数,R是Reduce的个数。这样会产生大量的小文件,对文件系统压力很大,而且不利于IO吞吐量。
后来做了一些优化,把在同一core上运行的多个Mapper输出的合并到同一个文件,这样文件数目就变成了cores R个了。这就是Consolidation机制。
Consolidation开启方式: new SparkConf().set("spark.shuffle.consolidateFiles", "true")
Sort Based Shuffle
后面就引入了 Sort Based Shuffle , spark1.2以后默认使用该方式,map端的任务会按照Partition id以及key对记录进行排序。同时将全部结果写到一个数据文件中,同时生成一个索引文件。
Tungsten-Sort Based Shuffle
再后面就引入了Tungsten-Sort Based Shuffle,这个是直接使用堆外内存和新的内存管理模型,节省了内存空间和大量的gc,是为了提升性能。
spark2.1的shuffle
spark 2.1中分为三种writer , BypassMergeSortShuffleWriter, SortShuffleWriter 和 Unskai'qiafeShuffleWriter
BypassMergeSortShuffleWriter
实现细节:
BypassMergeSortShuffleWriter 和 Hash Shuffle的HashShuffleWriter实现基本一致,唯一的区别在于,map端的多个输出文件会被汇总为一个文件。所有分区的数据会合并为同一个文件,会生成一个索引文件,是为了索引到每个分区的起始地址,可以随时access某个partiton的所有数据
需要注意的是,这种方式不宜有太多分区,因为过程中会并发打开分区对应的临时文件,会对文件系统造成相当大的压力。
具体实现就是给每一个分区分配一个临时文件,对每个record的key使用分区器(模式是hash,如果用户自定义就使用自定义的分区器)找到对应分区的输出文件句柄,直接写入文件,没有再内存中使用buffer.最后copyStream方法把所有的临时分区文件拷贝到最终的输出文件中,并且记录每个分区的文件起始写入位置,把这些位置数据写入到索引文件中。
sortShuffleWriter:
SortShuffleWriter中的处理步骤就是:
使用PartitionedAppendOnlyMap 或者 PartitonedPairBuffer 在内存中进行排序,排序的键值对 K 是 (partitionId,hash(key))这样的一个元组 如果超过内存limit,就会split到一个文件中,这个文件中元素也是有序的,首先是按照partitonId的排序,如果partitonId相同,再根据hash(key)进行比较排序如果需要输出全局有序的文件的时候,就需要对之前所有的输出文件 和 当前内存中的数据结构中的数据进行 merge sort ,进行全局排序
SortShuffleWriter使用ExternalSorter来对内存中的数据进行排序,ExternalSorter内部维护了两个集合PartitionedAppendOnlyMay,PartitonedPairBuffer,两者都是使用了hash table数据结构,如果需要进行aggregation,就使用PartitonedAppendOnlyMap(支持lookup 某个key,如果之前存储过相同key的K-V元素,就需要进行aggregation,然后再存入aggregation后的K-V),否则使用PartitonedPairBuffer(只进行添加K-V元素)
在aggregation的时候,就是使用定义的func进行聚合,比如你的算子是reduceByKey(+),这个func就是加法运算,如果两个key相同,就会先找到所有相同的key进行reduce(+)操作,算出一个总结果result,然后输出数据(K,Result)元素
unsafeShuffleWriter:
Serializer支持relocation:原始数据首先被序列化处理,并且再也不需要反序列,在其对应的元数据被排序后,需要Serializer支持relocation,在指定位置读取对应数据
UnsafeShuffleWriter里面维护了一个外部排序, 每次根据partitionId进行排序,如果内存满了直接split磁盘上,这个和sortShuffleWriter有什么区别呢,这里只根据record的partition id先在内存ShuffleInMemorySorter中进行排序,排好序的数据经过序列化压缩输出到一个临时文件中的一段,并记录每个分区段的seek位置,方便后续可以单独读取每个分区的数据,读取流经过解压反序列化,就可以正常读取了
unsageShuffleWriter限制条件:
unsageShuffleWriter需要Serializer支持relocation,并且不支持key排序
使用哪种writer的判断依据:
a.是否开启了mapSideCombine 并且分区数目是否小于spark.shuffle.sort.bypassMergeThreshold(默认值为200),如果是的话 执行BypassMergeSortShuffleWriter:
b.如果不满足条件a ,并且serializer支持relocation ,没有聚合操作,以及分区数目小于16777215,则使用unsageShuffleWriter,否则使用SortShuffleWrite