Spark Push Based Shuffle 原理分析_spark remoteblockpushresolver

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以戳这里获取

问题: merger location 是现在运行的 executor 地址。如 application 刚开始运行时队列资源紧张,第 1 个 mapper 处理完时,getMergerLocation 时,申请到的 executor 不多,但是任务很大,后续可能申请到更多 executor。就会出现把 shuffle 数据 push 到少数 ess 上, 照成 shuffle 数据倾斜。

是否可能用所有的 ESS 地址参加 get merger locations 的计算?这样就不会有 shuffle 数据倾斜的问题。难点:如果一个 node 上没有运行 application 的 executor,就不会向 ess 注册 applicationStart 等事件,被选中作为 ess 时,没有 application 信息。applicationEnd 事件时,ess 要清理所有的 shuffle 数据。不符合现有的机制。

Executor 端发送 shuffle 数据工作

发送 shuffle 数据工作内容: 把 shuffle 数据发往对应的 ess 的地址。
发送要解决的问题:

  1. 如果多个 Executor 都往一个 ESS 发送数据,ESS 可能压力很大,要限制。
  2. ESS 可能在开始计算时正常,然后失败,导致连接不上。
  3. 如果数据倾斜导致对应 reduce id 的数据量大,则不 push。
  4. push 操作不能影响正常的计算线程。

在输出当前 Shuffle Map Task 的 shuffle 数据后。从 driver 获取当前 shuffle 的 mergerLocs。启动一个发送 shuffle 数据的过程。
发送 shuffle 数据分为3个阶段。
1): 把 shuffle 数据分为Seq[PushRequest], 一个 PushRequest 代表一个目标位置的数据一次发送的数据,可以有多个数据块,一个 block 代表对应一个 shuffle partition 的数据。
2): 真正的数据发送
3): 汇报发送完之后的状态。

1:数据的切分受以下参数的限制:
spark.shuffle.push.maxBlockSizeToPush:仅发送数据块小于此参数的 shuffle 数据,默认 1m bytes.
spark.shuffle.push.maxBlockBatchSize:一个 PushRequest 中数据大小的限制, 默认3m。

2:真正的数据发送是在新的线程中执行。
发送受以下参数的限制:
spark.reducer.maxSizeInFlight:重用以前的参数,限制同一个 ESS 最大发送的未应答数据量。
spark.reducer.maxReqsInFlight: 所有目标地址最大发送的 PushRequest 的数量, 默认 Int.MaxValue。
spark.reducer.maxBlocksInFlightPerAddress: 一个目标地址最大发送数据块数量, 默认 Int.MaxValue。一个 PushRequest 包含多个 block。
spark.shuffle.push.numPushThreads:发送线程数,如果没有配置,则为 Executor 的 vcores 数量。

发送队列两个,一个是之前计算的 Seq[PushRequest] ,另一个是 deferredPushRequests, 当 PushRequest 目标地址达到限制时,先不发送,放到 deferredPushRequests 中。
每个不断取 Seq[PushRequest] 的 PushRequest,判断是否达到目标地址的限制,如果达到,则放到 deferredPushRequests 中,如果没有达到,则真正的发送。如果发送失败并且是目标地址连接不上,则把目标地址所有的 PushRequest 都清除。发送完后,如果Seq[PushRequest] 不为空,则开始新的过程。

那么所有的 mapper 任务运行后,都按照 getMergerLocation 的 ess 顺序发送数据,照成服务端竞争。 为了避免服务端竞争, 会把多个 PushRequest 进行随机排序(不按固定的顺序发送)。

  1. 汇报发送完之后的状态。给 Driver 发送一个 ShufflePushCompletion(shuffleId: Int, shuffleMergeId: Int, mapIndex: Int) 信息

参考代码:
ShuffleMapTask、ShuffleWriteProcessor、ShuffleBlockPusher

思考:发送线程数可能没有效。

  1. 因为是在主线程里,调用 pushUpToMax,在此方法里,顺序调用 send,仅在 send 的 callback 里再次调用 pushUpToMax。
  2. pushUpToMax 是同步方法,发送一个 mapper shuffle 数据的线程只有一个可用进入。不同的 Mapper Task 相互没有影响,因为每个 Mapper Task 创建不同的 ShuffleBlockPusher 对象。
Driver 端收到 ShufflePushCompletion 的处理逻辑

dagScheduler 收到信息后,增加当前 shuffle 的已经完成 push 的 mapper 数量,判断已经完成的 mapper 比例是否达到指定比例(默认100%), 如果达到则调用 scheduleShuffleMergeFinalize(mapStage, delay = 0)。进行 finalizeShuffleMerge 操作。使用一个线程池, 向所有的 mergerLocs 发送 FinalizeShuffleMerge(appId, comparableAppAttemptId, shuffleId, shuffleMergeId) 信息。

参考: DAGScheduler

ESS

ESS 负责的内容包括
1): 处理 push shuffle 数据的请求。
2): 处理 Driver 发送的 FinalizeShuffleMerge
3): 处理下游任务拉取 suffle 数据的请求。
4): 整个 shuffle 生命周期的管理,如不用的 shuffle 数据及时删除。

每个 shuffle partition 有 3 个文件,data file, meta file, index file。

  1. data file 存储同一个 shuffle partition 的数据。同一个 map 的 shuffle 的数据要顺序存储。不按 map-id 的顺序存储。示例如下('|'仅用于说明,不存在)。
    | map-2 shuffle data | map-0 shuffle data | map-1 shuffle-data | map-3 shuffle-data |
  2. meta file
    由于一个 map的 shuffle data 数据量比较小。reduce 端读取时,那么如果一次发送这么小的数据量会影响性能。把若干个 map 的 shuffle 数据组成一个 chunk。chunk 最小值由参数 spark.shuffle.push.server.minChunkSizeInMergedShuffleFile 决定(默认2m)。
    以下是 metafile 文件的示例:
    | map-2,map-0 | map-1,map-3 |
  3. index file
    index file 存储每个 chunk 在 data file 的 offset。
收到 PushBlockStream 的处理
  • 附注: 接收端 StreamInterceptor 处理
    StreamInterceptor 是一个请求的封装,一个请求可能跨越多个网络 block, 每个网络 block 对应一个 ByteBuf。为了减少数据拷贝的开销。StreamInterceptor 一个请求多次交付,接收到每个 block 调用对应 onData 方法。一个 rpc 请求的所有block 都接收后,调用 onComplete 方法。

StreamInterceptor 里存储了每个请求的数据量大小 byteCount,已经读取的 byteRead,会从网络一直读到 byteCount。如果最后一个 网络 block 仅有部分内容是当前 rpc 的,会限定 ByteBuf 的范围。

当数据类型是 PushBlockStream 时, 使用 PushBlockStreamCallback。

对应 PushBlockStreamCallback 的实现

目标: 把同一个 shuffle partition 的数据写入同一个 data 文件,并且 meta 文件记录 chunk 信息,index 文件记录 chunk 的 offset 信息。

一次 PushBlockStreamCallback 就是写入一个分区的数据。每个 PushBlockStream 都会创建不同的 PushBlockStreamCallback 对象。这点很重要,因为多个 mapper 的数据可能同时发过来,如果不能写入,会缓存到 PushBlockStreamCallback 里。

每个 local dirs 下都会建立一个 merge_manager 目录,如 appcache/application_1707014072051_0006/merge_manager。因为可能接收多个 reducer 的数据,每个目录下默认建立 64 个子目录。

ESS 为每个 merger partition 创建一个 partitionInfo 对象,里面 currentMapIndex 记录正在写入 mapper 的 id,为 -1 时代表没有 mapper 写入。

一个 shuffle block 的请求可能包括多个网络数据块,每个网络数据块为一个 ByteBuf。所以一个 shuffle block 会调用多次 onData, 调用一次 onComplete。

onData

onData 方法首先会获取要写入的 partitionInfo 对象。
先判断是否能写入,如果能写入,则直接把接收到的 ByteBuf 写入文件。否则添加到 deferredBufs 缓存起来。

能写入的判断条件:partitionInfo.currentMapIndex == block 的 mapper index 或者 partitionInfo.currentMapIndex = -1

onComplete

如果 deferredBufs 不为空,并且能写入,则把所有 deferredBufs 写入文件里。
如果如果 deferredBufs 不为空,并且不能写入,则把 deferredBufs 清除并返回。

记录 mapIndex 的数据已经写入。

mapTracker.add(mapIndex);
chunkTracker.add(mapIndex);

如果 chunk 已经达到阈值,则记录 chunkTracker 信息输出到 metafile,并更新 indexfile,重新初始化 chunkTracker。

示例:

时间点Map-1Map-2Map-3
1Map-1-Block-1
2Map-2-Block-1
3Map-3-Block-1
4Map-2-Block-2
5Map-3-Block-2
6Map-1-Block-2
7onComplete
8onComplete
9onComplete

同时接收到 Map-1,Map-2,Map-3 发送过来的 shuffle 数据,创建 3 个PushBlockStreamCallback 对象。

在时间点1,接收 Map-1-Block-1, partition 里 currentMapIndex 设置为1。把 Map-1-Block-1 写入文件。

在时间点2,接收 Map-2-Block-1, partition 里 currentMapIndex 不等于当前 map index。把当前 block 添加到 deferredBufs。

在时间点3,接收 Map-3-Block-1, partition 里 currentMapIndex 不等于当前 map index。把当前 block 添加到 deferredBufs。

在时间点4,接收 Map-2-Block-2, partition 里 currentMapIndex 不等于当前 map index。把当前 block 添加到 deferredBufs。

在时间点5,接收 Map-3-Block-2, partition 里 currentMapIndex 不等于当前 map index。把当前 block 添加到 deferredBufs。

在时间点 6,接收 Map-1-Block-2, partition 里 currentMapIndex 等于当前 map index。把 Map-1-Block-2 写入文件。

在时间点 7, Map-3 结束,partition 里 currentMapIndex 不等于当前 map index,清空所有 deferredBufs。Map-3 对应的 shuffle 数据永远不写入。

在时间点 8, Map-1 结束,清空 currentMapIndex。

在时间点 9, Map-2 结束,onComplete 判断 currentMapIndex < 0, 输出所有的 deferredBufs,然后清空 currentMapIndex。

Optimize RemoteBlockPushResolver with a memory pool 描述了当 Mapper Task 数量比较多, reduce task 数量比较小时,经常抛出 onComplete 时不能写入,抛出异常。如有 100 万个 mapper task,1 个 shuffle partition,所有 mapper task 结束后都往同一个 ESS 发送数据,ESS 同时仅能写入一个 mapper 的 block,其他的都放到内存里。commit 已经废弃。

问题:chunkTracker 有明显性能问题,每个 chunk 创建一个 Roaringbitmap 对象,一个 chunk 多个 mapper_id 都放到此 Roaringbitmap 对象中,把 Roaringbitmap 输出到 meta file。Roaringbitmap 在数据量少的时候占用空间大。

推测执行的处理

如 map1-attempt1 和 map1-attempt2, 两个 Task attempt 计算的是同一份数据。当 map1-attempt1 把 shuffle 数据 push 后,mapTracker 里已经记录了map1 id。当 map1-attempt2 发送数据后,mapTracker 已经存在 map1 id,会认为此 job 是重复的,不会再存储此 block.

如果两个推测执行的 shuffle 数据同时达到,由于锁的限制,会先后执行时,后边的请求执行时,currentMapIndex 都等于当前 map 的 index,也不会有问题。因为第一个 mapper task 的数据写入后,会更新 PushBlockStreamCallback 的 length 字段。如果第2个 mapper task 看到 currentMapIndex 等于当前的 mapper index 但是 PushBlockStreamCallback.length 是 0 的时候,知道是推测执行的任务,则不写入。

推测执行的隐藏问题:
insert into  t3 select distinct c1 from t2 distribute by rand(); 

如以上函数,如果 Map1-1 和 Map1-2 两个任务。
Map1-1 第 1 条记录对应 shuffle partition 1。第 2 条记录对应 shuffle partition 2。
Map1-2 第 1 条记录对应 shuffle partition 2。第 2 条记录对应 shuffle partition 1。

Map1-1 把第1 个shuffle partition 1 推送成功。shuffle partition 2 推送失败。
Map1-2 把第1 个shuffle partition 1 推送失败。shuffle partition 2 推送成功。

会造成第 1 条记录出现 2 次,而第 2 条记录不会出现。

原来的机制不会有此问题,原因在于 一个mapper 的 MapStatus 只有一个。

改造如下:能否推送 shufffle 数据由 driver 进行判断。一个mapper 仅能一个 task 推送 shuffle 数据。

FinalizeShuffleMerge 处理

Driver 判断已经完成的 mapper 比例是否达到指定比例(默认100%), 向所有 ESS 发送 FinalizeShuffleMerge 消息。
ESS 收到 Driver 的请求后,先调用 partition.finalizePartition() 关闭相关的文件;
并且把每个 partition 的 reduce_id, maptracker(包含哪些 mapper 的 shuffle 数据), shuffle 数据量。并且加上 shuffle id, shuffleMergeId 组装为 MergeStatuses 对象发往driver.

代码:RemoteBlockPushResolver

Driver 在 FinalizeShuffleMerge 的回调

mapOutputTracker.registerMergeResults(stage.shuffleDep.shuffleId, mergeStatuses)

把 mergeStatus 放到 ShuffleStatus 对象中。

ShuffleStatus 存储一个 map stage 的所有 shuffle 信息。
val mapStatuses = new Array[MapStatus](numPartitions)
val mergeStatuses = new Array[MergeStatus](numReducers)

mapStatuses 每个 mapper 任务一个对象,因为一个 map 仅在一台服务器上运行。里面存储了对应 blockmanager 的地址。所有的 reduce 任务都把所有的 mapStatuses 拿到,连接 blockmanager, 获取对应 reduce 的 shuffle 数据。

mergeStatuses 每个 reduce 任务一个对象。

Reducer 端计算逻辑

Reducer 对应的 RDD 是 Shuffle RDD。Shuffle RDD 的 compute 就是拉取 shuffle 数据组成 Iterator。和 MapReduce 框架不同 Spark 是边拉取边计算的模式,并不等全部数据拉取完毕后才计算。

Shuffle 数据拉取的流程:

  1. 先从 Driver 获取 ShuffleStatus
  2. 计算要拉取的数据所在的地址
  3. 连接地址,进行数据拉取,并且成功后交付给计算框架。
  • 调用 getStatuses 返回需要的 shuffle 元数据 (Array[MapStatus], Array[MergeStatus])。每个 reduce 拿到的都是一个 shuffle 所有的 MapStatus 和 MergeStatus。

思考:现在同一个 shuffle id 的 MapStatus 和 MergeStatus 仅序列化一次,存储缓存后的字节数组。现在并没有有效的利用 broadcast 功能传递这些数据。MapStatus 和 MergeStatus 都是 M * N 级别。如果 5000 Map 和 5000 Reduce 产生的数据量 25M。每个 MapStatus 数据存储每个 reduce 对应的 size。MergeStatus 存储每个 Mapper 的 ID,每个元素占 8 字节。25M * 8 * 2 = 400 M字节的内存空间。400M 字节的数据要拉取 5000 次,Driver 容易称为瓶颈。

  • 计算要拉取的数据所在的地址
    拿到 (Array[MapStatus], Array[MergeStatus]) 后,计算完之后放在对象splitsByAddress 中。
val splitsByAddress = new HashMap[BlockManagerId, ListBuffer[(BlockId, Long, Int)]]

先从 mergeStatuses 获取。如果对应的 map id 的数据不存在,从 mapStatuses 获取。返回进行组合后的地址列表。 shuffle block 有两种 ShuffleMergedBlockId 和 ShuffleBlockId。

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以戳这里获取

链图片转存中…(img-YdNt0x1F-1715688775206)]
[外链图片转存中…(img-h6eWxJfD-1715688775206)]
[外链图片转存中…(img-0lITjAEm-1715688775206)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以戳这里获取

  • 22
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值