一、Shuffle Writer
### --- Shuffle Writer:ShuffleWriter(抽象类),有3个具体的实现:
~~~ SortShuffleWriter。sortShulleWriter 需要在 Map 排序
~~~ UnsafeShuffleWriter。使用 Java Unsafe 直接操作内存,
~~~ 避免Java对象多余的开销和GC 延迟,效率高
~~~ BypassMergeSortShuffleWriter。
~~~ 和Hash Shuffle的实现基本相同,区别在于map task输出汇总一个文件,同时还会产生一个index file
### --- Shuffle Writer
~~~ # 源码编程实现
ShuffleWriter(org.apache.spark.shuffle.sort)
BypassMergeSortShuffleWriter(org.apache.spark.shuffle.sort)
UnsafeShuffleWriter(org.apache.spark.shuffle.sort)
SortShuffleWriter(org.apache.spark.shuffle.sort)
### --- 以上 ShuffleWriter 有各自的应用场景。分别如下:
~~~ 不满足以下条件使用 SortShuffleWriter
~~~ 没有map端聚合,RDD的partitions分区数小于16,777,216,
~~~ 且 Serializer支持 relocation【Serializer 可以对已经序列化的对象进行排序,
~~~ 这种排序起到的效果和先对数据排序再序列化一致】,使用UnsafeShuffleWriter
~~~ 没有map端聚合操作 且 RDD的partition分区数小于200个,使用 BypassMergerSortShuffleWriter
### --- bypass运行机制:bypass运行机制的触发条件如下:
~~~ shuffle map task数量 <= spark.shuffle.sort.bypassMergeThreshold (缺省200)
~~~ 不是聚合类的shuffle算子
### --- Bypass机制 Writer 流程如下:
~~~ 每个Map Task为每个下游 reduce task 创建一个临时磁盘文件,
~~~ 并将数据按key进行hash然后根据hash值写入内存缓冲,缓冲写满之后再溢写到磁盘文件;
~~~ 最后将所有临时磁盘文件都合并成一个磁盘文件,并创建索引文件;
~~~ Bypass方式的Shuffle Writer机制与Hash Shuffle是类似的,在shuffle过程中会创建很多磁盘文件,
~~~ 最后多了一个磁盘文件合并的过程。Shuffle Read的性能会更好;
### --- Bypass方式与普通的Sort Shuffle方式的不同在于:
~~~ 磁盘写机制不同
~~~ 根据key求hash,减少了数据排序操作,提高了性能
### --- 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
二、Shuffle MapOutputTracker
### --- Spark的shuffle过程分为Writer和Reader:
~~~ Writer负责生成中间数据
~~~ Reader负责整合中间数据
~~~ 而中间数据的元信息,则由MapOutputTracker负责管理。 它负责Writer和Reader的沟通。
~~~ Shuffle Writer会将中间数据保存到Block里面,然后将数据的位置发送给MapOutputTracker。
~~~ Shuffle Reader通过向 MapOutputTracker 获取中间数据的位置之后,才能读取到数据。
### --- 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
四、Shuffle Reader
### --- Shuffle Reader
~~~ Map Task 执行完毕后会将文件位置、计算状态等信息封装到 MapStatus 对象中,
~~~ 再由本进程中的MapOutPutTrackerWorker 对象将其发送给Driver进程的
~~~ MapOutPutTrackerMaster对象
~~~ Reduce Task开始执行之前会先让本进程中的 MapOutputTrackerWorker
~~~ 向 Driver 进程中的MapOutputTrackerMaster 发动请求,获取磁盘文件位置等信息
~~~ 当所有的Map Task执行完毕后,Driver进程中的 MapOutputTrackerMaster
~~~ 就掌握了所有的Shuffle文件的信息。
~~~ 此时MapOutPutTrackerMaster会告诉MapOutPutTrackerWorker磁盘小文件的位置信息
~~~ 完成之前的操作之后,由 BlockTransforService 去 Executor 所在的节点拉数据,
~~~ 默认会启动五个子线程。每次拉取的数据量不能超过48M