spark的shuffle有几种方式:
什么是shuffle
Shuffle 过程本质上都是将 Map 端获得的数据使用分区器进行划分,并将数据发送给对应的 Reducer 的过程。
前一个stage的ShuffleMapTask进行shuffle write,把数据存储在blockManager上面,并且把数据元信息上报到dirver的mapOutTarck组件中,下一个stage根据数据位置源信息,进行shuffle read,拉取上一个stage的输出数据
shuffle流程:
shuffle操作分为两种,分别是写操作和读操作
写操作:基于hash的shuffle操作和基于排序的shuffle操作,spark1.2之前用基于hash的写操作
1、基于哈希的shuffle操作:基于哈希的shuffle操作的原理在于将Mapper(stage)生成的中间数据,按照Reduce的数量(Reduce任务的数量取决于当前stage的RDD的分区数量)进行切分。切分成多个bucket,每个bucket对应一个文件。当reduce任务运行时,会根据任务的编号和所依赖的mapper编号远程或者从本地取得相应的bucket作为reduce任务的输入数据进行处理()
2、基于排序的shuffle操作: 基于哈希的shuffle操作会产生很多文件,这对文件系统来说是一个非诚大的负担,而且在总数据量不大而文件特别多的情况下,随机读写会严重降低I/O性能。大量文件的带来的问题还包括缓存。缓存所占用的内存过多是一笔很大的开销。每个shuffle map task只会产生一个索引文件,以及这个文件的索引,其中索引中 记载着,这个文件的那些数据是被下游的那些reduce task(stage)任务使用()
读操作:将根据不同的写入方式采取相同的读取方式,读取的数据放在哈希列表中用于后续处理
a、起始点是shuffleRDD.computer()发起的,在该方法中会调用shuffleManage的getReader方法。shuffleManeger实在SparkEnv启动的时候被实例化的,一起被实例化的还包括shuffleManager、BlockManager、MapOutputTracker(0读取数据时,用来获取元数据时使用的
b、getReader()采用的是HashShuffleReader或者SortShuffleReader的read方法(这两种方法均实例化BlockStoreShuffleReader)。,在HashShuffleReader.read()方法中,先实例化ShuffleBlockFetchIterator(位于BlockStoreShuffleReader中),在实例化过程中,获取上游shuffleMapTask输出的元数据
c、得到元数据之后,返回ShuffleBlockFetchIterator中的initialize方法,通过splitLocalRemoteBlocks方法对获取的数据位置信息进行分区,来判断这些节点是本地节点还是远程节点。不同的位置采用不同的方法来获取数据,远程节点上的数据利用fetchUpToMaxBytes()方法,本地数据利用fetchLocalBlock方法获取
d、数据读取完毕,回到BlockStoreShuffleReader的read方法,判断是否定义聚合。如果需要进行聚合,则根据简直进行聚合。聚合完,使用外部排序(externalSorter)对数据进行排序并放入内存
Hashshuffle(spark1.2之前):
spark的shuffle输出的map任务会为每个reduce创建相应的bucket,map产生的结果会根据设置的partition得到相应的bucketID,然后填充到相应的bucket(缓冲区,默认大小32kb)每个map的输出结果可能包含所有的Reduce所需要的数据,所以每个mao会创建R个(R是Reduce的个数),M个map总工会创建M*R个bucket
map创建的bucket其实对应磁盘上的一个文件,map的结果写到每个bucket其实就是写到那个磁盘文件。这个文件也被称为blockFile,是disk block manager管理器通过文件名的hash值对应到本地目录的子目录中创建。每个map要在节点上创建磁盘文件用于结果输出,map的结果是直接输出到磁盘文件上的,100kb的内存缓冲是用来创建fast buffered OutputStream输出流。这种方式的缺点就是Shuffle文件过多
Sort-based shuffle:
Sort-Based Shuffle有几种不同的策略:BypassMergeSortShuffleWriter、SortShuffleWriter和UnasfeSortShuffleWriter
BypassMergeSortShuffleWriter(速度较快,不需要排序,当reduce数量少,map端不进行聚合时)
map端多个输出文件会被汇总成一个文件,copyStream方法把所有的分区数据的临时文件拷贝到最终的输出文件,会生成一个索引文件,是为了索引到每个分区的起始地址,可以随机access某个partition的所有数据(这种方式不宜太多分区,因为过程中会打开所有分区对应的临时文件,对文件系统造成很大压力,所以设置阈值)
SortShuffleWriter(排序级别partitionID,key)
我们可以先考虑一个问题,假如我有 100亿条数据,但是我们的内存只有1M,但是我们磁盘很大, 我们现在要对这100亿条数据进行排序,是没法把所有的数据一次性的load进行内存进行排序的,这就涉及到一个外部排序的问题,我们的1M内存只能装进1亿条数据,每次都只能对这 1亿条数据进行排序,排好序后输出到磁盘,总共输出100个文件,最后怎么把这100个文件进行merge成一个全局有序的大文件。我们可以每个文件(有序的)都取一部分头部数据最为一个 buffer, 并且把这 100个 buffer放在一个堆里面,进行堆排序,比较方式就是对所有堆元素(buffer)的head元素进行比较大小, 然后不断的把每个堆顶的 buffer 的head 元素 pop 出来输出到最终文件中, 然后继续堆排序,继续输出。如果哪个buffer 空了,就去对应的文件中继续补充一部分数据。最终就得到一个全局有序的大文件
·使用ExternalSort(PartitionAppendOnlyMap或者PartitionPairBuffer)·在内存中进行排序,排序K是(partitionId,hash(key))这样一个元组
·如果超过内存limit,就spill到一个文件中,这个文件中元素也是有序的,首先是partitionId的排序,如果partitionId相同,再根据hash(key)进行比较排序,读取时也是按顺序读取
·如果需要输出全局有序的文件的时候,就需要对之前所有的输出文件和当前内存中的数据结构中的数据进行mergesort,StreamBuffer读取数据放到mergehead进行排序
UnsafeShuffleWriter(排序级别只有partitionID,外部排序,不用反序列化)
整个过程就是不断地在ShuffleMemorySorter插入数据,如果没有内存就申请内存,如果申请不到就spill到文件,最终合并成一个依据partitionID全局有序的大文件( 每次插入一条 record 到page 中, 就把 partionId + pageNumber + offset in page, 作为一个元素插入到 LongArray 中, 最终读取数据的时候, 对LongArray 进行 RadixSort 排序, 排序后依次根据指针元素索引原始数据,就做到 partition 级别有序了。)
没有指定 aggregation 或者key排序, 因为 key 没有编码到排序指针中,所以只有 partition 级别的排序
原始数据首先被序列化处理,并且再也不需要反序列,在其对应的元数据被排序后,需要Serializer支持relocation,在指定位置读取对应数据。 KryoSerializer 和 spark sql 自定义的序列化器 支持这个特性。
分区数目必须小于 16777216 ,因为 partition number 使用24bit 表示的。
因为每个分区使用 27 位来表示 record offset, 所以一个 record 不能大于这个值。
spill 文件的时候, UnsafeShuffleInMemorySorter 生成一个数据迭代器, 会返回一个根据partition id 排过序迭代器,该迭代器粒度每个元素就是一个指针,对应 PackedRecordPointer 这个数据结构, 这个 PackedRecordPointer 定义的数据结构就是 [24 bit partition number][13 bit memory page number][27 bit offset in page] 然后到根据该指针可以拿到真实的record, 在一开始进入UnsafeShuffleExternalSorter 就已经被序列化了,所以在这里就纯粹变成写字节数组了。一个文件里不同的partiton的数据用fileSegment来表示,对应的信息存在 SpillInfo 数据结构中。
注意:
BypassMergeSortShuffleWriter会临时输出reduce个小文件,所以分区数必须小于一个阈值,默认是200
UnsafeShuffleWriter需要Serializer支持relocation:原始数据首先被序列化处理,并且再反序列化,在其对应的元数据被排序后,需要Serializer支持relocation,在指定位置读取对应数据
简述MR的shuffle的spark的shuffle过程