1.Shuffle定义
2.Spark Shuffle的两个阶段
Spark Shuffle 的流程简单抽象为以下几步:
Shuffle Write
Map Side combine(if needed)
Write to local output file
Shuffle Read
Block fetch
Reduce side combine
Sort(if needed)
3.Spark Shuffle 技术演进
- Spark 1.1 以前是Hash Shuffle
- Spark 1.1 引入了Sort Shuffle
- Spark 1.6 将Tungsten-sort并入Sort Shuffle(利用对外内存进行排序)
- Spark 2.0 Hash Shuffle退出历史舞台
3.1 Hash Shuffle V1
- 生成大量文件,占用文件描述符,同时引入 DiskObjectWriter 带来的 WriterHandler 的缓存也非常消耗内存
- 如果在 Reduce Task 时需要合并操作的话,会把数据放在一个 HashMap 中进行合并,如果数据量较大,很容易引发 OOM
3.2 Hash Base Shuffle V2 -- File Consolidation
3.3、Sort Base Shuffle V1
这里通过减少文件数来避免因为打开过多文件而导致的OOM,同时在reduce端引入磁盘溢写的方式来避免reduce Task的OOM。
3.4 Sort Shuffle V2
从Spark2.0 开始,Spark 把 Hash Shuffle 移除, Spark2.x 中只有一种 Shuffle,即为 Sort Shuffle。
到此shuffle的演进就结束了 下面说说shuffle的细节。
4.Shuffle Writer
ShuffleWriter(抽象类),有3个具体的实现:
SortShuffleWriter。sortShulleWriter 需要在 Map 排序
UnsafeShuffleWriter。使用 Java Unsafe 直接操作内存,避免Java对象多余的开销和GC 延迟,效率高
BypassMergeSortShuffleWriter。和Hash Shuffle的实现基本相同,区别在于 map task输出汇总一个文件,同时还会产生一个index file
以上 ShuffleWriter 有各自的应用场景。分别如下:
- 不满足以下条件使用 SortShuffleWriter,保底方案
- 没有map端聚合(groubykey没有map端的聚合),RDD的partitions分区数小于16,777,216,且 Serializer支持 relocation【Serializer 可以对已经序列化的对象进行排序,这种排序起到的效果和先对数据排序再序列化一致】,使用UnsafeShuffleWriter
- 没有map端聚合操作 且 RDD的partition分区数小于200个而且不是聚合类的shuffle算子,使用 BypassMergerSortShuffleWriter
Shuffle Writer 流程
数据先写入一个内存数据结构中。不同的shuffle算子,可能选用不同的数据结构
如果是 reduceByKey 聚合类的算子,选用 Map 数据结构,一边通过 Map进行聚合,一边写入内存
如果是 join 类的 shuffle 算子,那么选用 Array 数据结构,直接写入内存
检查是否达到内存阈值。每写一条数据进入内存数据结构之后,就会判断一下, 是否达到了某个临界阈值。如果达到临界阈值的话,那么就会将内存数据结构中的数据溢写到磁盘,并清空内存数据结构。
数据排序。在溢写到磁盘文件之前,会先根据key对内存数据结构中已有的数据进行排序。排序过后,会分批将数据写入磁盘文件。默认的batch数量是10000条,也就是说,排序好的数据,会以每批1万条数据的形式分批写入磁盘文件
数据写入缓冲区。写入磁盘文件是通过Java的BufferedOutputStream 实现的。 BufferedOutputStream 是Java的缓冲输出流,首先会将数据缓冲在内存中,当内存缓冲满溢之后再一次写入磁盘文件中,这样可以减少磁盘IO次数,提升性能。
重复写多个临时文件。一个 Task 将所有数据写入内存数据结构的过程中,会发生多次磁盘溢写操作,会产生多个临时文件。
临时文件合并。最后将所有的临时磁盘文件进行合并,这就是merge过程。此时会将之前所有临时磁盘文件中的数据读取出来,然后依次写入最终的磁盘文件之中。
写索引文件。由于一个Task 就只对应一个磁盘文件,也就意味着该task为下游 stage的task准备的数据都在这一个文件中,因此还会单独写一份索引文件,其中标识了下游各个Task 的数据在文件中的 start offset 与 end offset。
5.Shuffle MapOutputTracker
- Writer负责生成中间数据
- Reader负责整合中间数据
Shuffle Reader 需要提供 shuffleId 、 mapId 、 reduceId 才能确定一个中间数据:
- shuffleId,表示此次shuffle的唯一id
- mapId,表示map端 rdd 的分区索引,表示由哪个父分区产生的数据
- reduceId,表示reduce端的分区索引,表示属于子分区的那部分数据
MapOutputTracker在executor和driver端都存在:
- MapOutputTrackerMaster 和 MapOutputTrackerMasterEndpoint(负责通信) 存在于driver
- MapOutputTrackerWorker 存在于 executor 端
- MapOutputTrackerMaster 负责管理所有 shuffleMapTask 的输出数据,每个shuffleMapTask 执行完后会把执行结果(MapStatus对象)注册到 MapOutputTrackerMaster
- MapOutputTrackerMaster 会处理 executor 发送的 GetMapOutputStatuses请求,并返回serializedMapStatus 给 executor 端
- MapOutputTrackerWorker 负责为 reduce 任务提供 shuffleMapTask 的输出数据信息(MapStatus对象)
- 如果MapOutputTrackerWorker在本地没有找到请求的 shuffle 的mapStatus,则会向MapOutputTrackerMasterEndpoint 发送 GetMapOutputStatuses 请求获取对应的 mapStatus
6.Shuffle Reader
- Map Task 执行完毕后会将文件位置、计算状态等信息封装到 MapStatus 对象中,再由本进程中的 MapOutPutTrackerWorker 对象将其发送给Driver进程的MapOutPutTrackerMaster对象
- Reduce Task开始执行之前会先让本进程中的 MapOutputTrackerWorker 向Driver 进程中的 MapOutputTrackerMaster 发动请求,获取磁盘文件位置等信息
- 当所有的Map Task执行完毕后,Driver进程中的 MapOutputTrackerMaster 就掌握了所有的Shuffle文件的信息。此时MapOutPutTrackerMaster会告诉MapOutPutTrackerWorker磁盘小文件的位置信息
- 完成之前的操作之后,由 BlockTransforService 去 Executor 所在的节点拉数据,默认会启动五个子线程。每次拉取的数据量不能超过48M
7.Hadoop Shuffle 与 Spark Shuffle 的区别
共同点:
二者从功能上看是相似的;从High Level来看,没有本质区别,实现(细节)上有区别
- Hadoop中有一个Map完成,Reduce便可以去fetch数据了,不必等到所有Map任务完成;而Spark的必须等到父stage完成,也就是父stage的 map 操作全部 完成才能去fetch数据。这是因为spark必须等到父stage执行完,才能执行子 stage,主要是为了迎合stage规则。子stage需要从父stage的多个分区中拉取数据。
- Hadoop的Shuffle是sort-base的,那么不管是Map的输出,还是Reduce的输出,都是partition内有序的,而spark不要求这一点。
- Hadoop的Reduce要等到fetch完全部数据,才将数据传入reduce函数进行聚合,而 Spark是一边fetch一边聚合。
8.Shuffle优化
开发过程中对 Shuffle 的优化:
- 减少Shuffle过程中的数据量
- 避免Shuffle
优化一:调节 map 端缓冲区大小
- 合理设置参数,性能会有 1%~5% 的提升
- spark.shuffle.file.buffer 默认值为32K,shuffle write阶段buffer缓冲大小。将数据写到磁盘文件之前,会先写入buffer缓冲区,缓冲写满后才溢写到磁盘。
- 调节map端缓冲的大小,避免频繁的磁盘IO操作,进而提升任务整体性能
- spark.reducer.maxSizeInFlight 默认值为48M。设置shuffle read阶段buffer缓冲区大小,这个buffer缓冲决定了每次能够拉取多少数据
- 在内存资源充足的情况下,可适当增加参数的大小(如96m),减少拉取数据的次数及网络传输次数,进而提升性能
- 合理设置参数,性能会有 1%~5% 的提升
- Shuffle read阶段拉取数据时,如果因为网络异常导致拉取失败,会自动进行重试
- spark.shuffle.io.maxRetries 默认值3。最大重试次数
- spark.shuffle.io.retryWait,默认值5s。每次重试拉取数据的等待间隔
- 一般调高最大重试次数,不调整时间间隔
- 如果shuffle reduce task的数量小于阈值,则shuffle write过程中不会进行排序操作,而是直接按未经优化的Hash Shuffle方式写数据,最后将每个task产生的所有临时磁盘文件都合并成一个文件,并创建单独的索引文件
- spark.shuffle.sort.bypassMergeThreshold,默认值为200
- 当使用SortShuffleManager时,如果的确不需要排序操作,建议将这个参数调大
- Spark给 Shuffle 阶段分配了专门的内存区域,这部分内存称为执行内存
- 如果内存充足,而且很少使用持久化操作,建议调高这个比例,给 shuffle 聚合操作更多内存,以避免由于内存不足导致聚合过程中频繁读写磁盘
- 合理调节该参数可以将性能提升10%左右