Spark Push Based Shuffle 原理分析_spark remoteblockpushresolver

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新大数据全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

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

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

如果你需要这些资料,可以添加V获取:vip204888 (备注大数据)
img

正文

  1. Mapper 端读取本地 shuffle 文件,发送 shuffle 数据,这时是顺序读。
  2. ESS 把收到的同一个 shuffle partition 的写到文件里,写可以借助操心系统的写缓存。
  3. Reduce 任务运行时,从 ESS 拉取需要计算的 shuffle 数据,也是顺序读。指向 Mapper 的虚线说明当 ESS 中没有某些 Mapper 的数据时,还是用之前的模式拉取。
    Reduce 任务拉取 ESS 的数据时,ESS 顺序读取文文件。

服务端配置(指 NodeManager 上的Shuffle 服务的配置)

yarn-site.xml

yarn.nodemanager.aux-services 增加 spark_shuffle,并增加以下配置项。

<property>
  <name>yarn.nodemanager.aux-services.spark_shuffle.class</name>
  <value>org.apache.spark.network.yarn.YarnShuffleService</value>
</property>

把 spark-3.5.0-yarn-shuffle.jar 拷贝到 ${HADOOP_HOME}/share/hadoop/yarn/ 目录。

配置 ${HADOOP_CONF_DIR}/spark-shuffle-site.xml

<?xml version="1.0"?>
<configuration>
  <property>
    <name>spark.shuffle.push.server.mergedShuffleFileManagerImpl</name>
    <value>org.apache.spark.network.shuffle.RemoteBlockPushResolver</value>
  </property>
  <description>默认为 org.apache.spark.network.shuffle.
NoOpMergedShuffleFileManager </description>
</configuration>


客户端配置

spark-defaults.conf

spark.shuffle.push.enabled true
spark.master  yarn
spark.shuffle.service.enabled true
spark.serializer org.apache.spark.serializer.KryoSerializer
spark.io.encryption.enabled false

关闭 adaptive 执行,此项不是必须的,仅在测试中不自动调整下游任务的并发度。

spark.sql.adaptive.enabled false

执行流程

在这里插入图片描述

1.Get Merger Locations

因为同一个shuffle partition 的数据要放到同一个 ESS。ESS 要把收到的多个 mapper 的同一个 shuffle partition 的数据进行合并, Merger Location 就是进行合并的这些 ESS 的地址。
整个 Stage 的所有 Map Task 拿到的是相同的 merge locations。

计算逻辑:
如果正在运行 Executor 的 host 的数量大于需要的 locations 的数量,则直接返回正在运行执行器的 host 列表。
如果不够的话,加上第 2 部分。第2部分从所有运行过执行器的 host 中查找,去除第 1 部分中重叠的 host,并且去除加入黑名单的host。第 2 部分的 host 是开启 Dynamic Allocation 后,在这些 host 上启动过 Executor, 并且 Executor 没有任务正常退出。

触发时机:
第 1 版是在 Stage 启动的时候计算,发现开启 Dynamic Allocation 后,开始运行的 Executor 是 initialExecutors。造成大任务的 shuffle 数据被 push 到少数的 ESS 上。
第 2 版是 Mapper 把 shuffle 数据写到本地磁盘后,再调用 get merger locations。比第 1 版好一些。

问题: 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 的时候,知道是推测执行的任务,则不写入。

推测执行的隐藏问题:

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注大数据)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

BlockStreamCallback.length 是 0 的时候,知道是推测执行的任务,则不写入。

推测执行的隐藏问题:

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注大数据)
[外链图片转存中…(img-ErloSCUh-1713153210779)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值