Apache Spark——Shuffle 过程

    如果我们将 map 端划分数据、持久化数据的过程称为 shuffle write,而将 reducer 读入数据、aggregate 数据的过程称为shuffle read。那么我们来了解一下,在spark中,如何将shuffle write和shuffle read加入到逻辑或者物理执行图中并高效实现。


Shuffle write

     spark的shuffle通常使用HashMap对数据进行aggregate,并不会对数据进行提前排序(Hadoop MapReduce 则是mapper 对每段数据先做排序,reducer 的 shuffle 对排好序的每段数据做归并),因此shuffle write 的任务很简单:将数据 partition 好,并持久化。之所以要持久化,一方面是要减少内存存储空间压力,另一方面也是为了 fault-tolerance。那么实现也很简单:将 shuffle write 的处理逻辑加入到 ShuffleMapStage(ShuffleMapTask 所在的 stage) 的最后,该 stage 的 final RDD 每输出一个 record 就将其 partition 并持久化。


    上图有 4 个 ShuffleMapTask 要在同一个 worker node 上运行,CPU core 数为 2,可以同时运行两个 task。每个 task 的执行结果(该 stage 的 finalRDD 中某个 partition 包含的 records)被逐一写到本地磁盘上。每个 task 包含 R 个缓冲区,R =reducer 个数(也就是下一个 stage 中 task 的个数),缓冲区被称为 bucket,其大小默认是 32KB(Spark 1.1 版本以前是 100KB)。

    ShuffleMapTask 的执行过程如下:

    1.先利用 pipeline 计算得到 finalRDD 中对应 partition 的 records。每得到一个 record 就

将其送到对应的 bucket 里;

    2.每个 bucket 里面的数据会不断被写到本地磁盘上,形成一个 ShuffleBlockFile,或者简称 FileSegment。在一个 core 上连续执行的 ShuffleMapTasks 可以共用一个输出文件 ShuffleFile。i后执行的 ShuffleMapTask 可以将输出数据直接追加到 ShuffleBlock i 后面。这样,每个 worker 持有的文件数降为 cores * reducer

    3.下一个 stage 的 reducer 只需要 fetch 整个 ShuffleFile 就行了。


Shuffle read

    1.在什么时候 fetch,parent stage 中的一个 ShuffleMapTask 执行完还是等全部 ShuffleMapTasks 执行完?

    当 parent stage 的所有 ShuffleMapTasks 结束后再 fetch。(当parent stages执行完成后,当前stage才可以提交执行)因为 fetch 来的 FileSegments 要先放到softBuffer 缓冲区,所以一次fetch 的 FileSegments 总大小不能太大。默认大小为 48MB。

    2.边 fetch 边处理还是一次性 fetch 完再处理?

    边 fetch 边处理。与MapReduce不同,因为 Spark 不要求 shuffle 后的数据全局有序,因此没必要等到全部数据shuffle 完成后再处理。(此处的处理指的是沿着RDD computing chain执行直至得出finalRDD 中对应partition中的一个record)

    那么如何实现边 shuffle 边处理,而且流入的 records 是无序的?答案是使用可以 aggregate 的数据结构,比如 HashMap。每 shuffle 得到(从缓冲的 FileSegment 中 deserialize 出来)一个 \ record,直接将其放进HashMap 里面。如果该 HashMap 已经存在相应的 Key,那么直接进行 aggregate也就是 func(hashMap.get(Key),Value) ,并将 func 的结果重新 put(key) 到HashMap 中去。(Spark 中的 func 的输入参数是固定的,一个是上一个 record 的处理结果,另一个是当前读入的 record,它们经过 func 处理后的结果被下一个 record 处理时使用。如:data.reduceByKey(_+_)/ 如:data.reduceByKey((a,b) => a+b))


    3.fetch 来的数据存放到哪里?

    刚 fetch 来的 FileSegment 存放在 softBuffer 缓冲区,经过处理后的数据放在内存 + 磁盘上。

    4.怎么获得要 fetch 的数据的存放位置?

    reducer 在 shuffle 的时候是要去 driver 里面的 MapOutputTrackerMaster 询问 ShuffleMapTask 输出

的数据位置的。每个 ShuffleMapTask 完成时会将 FileSegment 的存储位置信息汇报给 MapOutputTrackerMaster。


Shuffle read中的 HashMap

    HashMap 是 Spark shuffle read 过程中频繁使用的、用于 aggregate 的数据结构。Spark 设计了两种:一种是全内存的AppendOnlyMap,另一种是内存+磁盘的 ExternalAppendOnlyMap。

1. AppendOnlyMap

    AppendOnlyMap 类似 HashMap,但没有 remove(key) 方法。其实现原理很简单,开一个大 Object 数组,蓝色部分存储 Key,白色部分存储 Value。如下图:


    当要 put(K, V) 时,先 hash(K) 找存放位置,如果存放位置已经被占用,就使用 Quadratic probing 探测方法来找下一个空闲位置。对于图中的 K6 来说,第三次查找找到 K4 后面的空闲位置,放进去即可。get(K6) 的时候类似,找三次找到 K6,取出紧挨着的 V6,与先来的 value 做 func,结果重新放到 V6 的位置。

    迭代 AppendOnlyMap 中的元素的时候,从前到后扫描输出。如果 Array 的利用率达到 70%,那么就扩张一倍,并对所有 key 进行 rehash 后,重新排列每个 key 的位置。

2. ExternalAppendOnlyMap

    ExternalAppendOnlyMap 持有一个 AppendOnlyMap,shuffle 来的一个个 (K, V) record 先 insert 到 AppendOnlyMap 中,insert 过程与原始的 AppendOnlyMap 一模一样。如果 AppendOnlyMap 快被装满时检查一下内存剩余空间是否可以够扩展,够就直接在内存中扩展,不够就 sort 一下 AppendOnlyMap,将其内部所有 records 都 spill 到磁盘上。最后一个 (K, V) record insert 到 AppendOnlyMap 后,表示所有 shuffle 来的 records 都被放到了 ExternalAppendOnlyMap 中,但不表示 records已经被处理完,因为每次 insert 的时候,新来的 record 只与 AppendOnlyMap 中的 records 进行 aggregate,并不是与所有的 records 进行 aggregate(一些 records 已经被 spill 到磁盘上了)。因此当需要 aggregate 的最终结果时,需要对AppendOnlyMap 和所有的 spilledMaps 进行全局 merge-aggregate。


    相比 MapReduce 固定的 shuffle-combine-merge-reduce 策略,Spark 更加灵活,会根据不同的transformation() 的语义去设计不同的 shuffle-aggregate 策略,再加上不同的内存数据结构来混搭出合理的执行流程。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值