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
### 执行流程
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/bf9d0aea63d44f37b22155c068712469.png)
#### 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 进行随机排序(不按固定的顺序发送)。
3. 汇报发送完之后的状态。给 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-1 | Map-2 | Map-3 |
| --- | --- | --- | --- |
| 1 | Map-1-Block-1 | | |
| 2 | | Map-2-Block-1 | |
| 3 | | | Map-3-Block-1 |
| 4 | | Map-2-Block-2 | |
| 5 | | | Map-3-Block-2 |
| 6 | Map-1-Block-2 | | |
| 7 | | | onComplete |
| 8 | onComplete | | |
| 9 | | onComplete | |
同时接收到 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 ArrayMapStatus
val mergeStatuses = new ArrayMergeStatus
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。
当开启shuffle merge 时,第一个 block(仅一个) 是 ShuffleMergedBlockId, 代表所有已经 merge 的shuffle数据,后面是没有 merge 的数据(包括 shuffle 数据量为 0 和 shuffle 数据量超过限额,和其他原因失败造成没有 merge 的数据)
对于 ShuffleMergedBlockId,分为两种情形,数据在本地和数据在远程。分别调用不同的代码。
对于本地 merge shuffle,index 文件存储 chunk 的 offset,根据 offset 把 data file 分为一个一个的 FileSegmentManagedBuffer, 每次读取文件的一个 chunk,并把 chunk 交给计算框架。
对于远程 merge shuffle, 先调用 getMergedBlockMeta(存储 numChunks 和 chunks 的bitmap).
**自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。**
**深知大多数大数据工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**
**因此收集整理了一份《2024年大数据全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。**
![img](https://img-blog.csdnimg.cn/img_convert/9dda88af1713789e3c96660ba260703f.png)
![img](https://img-blog.csdnimg.cn/img_convert/c774de5bdefb8a76267602d4e47aa349.png)
![img](https://img-blog.csdnimg.cn/img_convert/eff057e62e98ad383cc37c1031353af3.png)
![img](https://img-blog.csdnimg.cn/img_convert/fde855dccdbc24d9f3ff3664671f470b.png)
![img](https://img-blog.csdnimg.cn/img_convert/d2309bba651cea094cc1ea78d65e5143.png)
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上大数据开发知识点,真正体系化!**
**由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新**
**如果你觉得这些内容对你有帮助,可以添加VX:vip204888 (备注大数据获取)**
![img](https://img-blog.csdnimg.cn/img_convert/249400a79141a2af17f9952bfa83be9a.png)
0-1712860853146)]
[外链图片转存中...(img-6SrpQTmv-1712860853146)]
[外链图片转存中...(img-1cT3s9TK-1712860853146)]
[外链图片转存中...(img-cJioeghR-1712860853147)]
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上大数据开发知识点,真正体系化!**
**由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新**
**如果你觉得这些内容对你有帮助,可以添加VX:vip204888 (备注大数据获取)**
[外链图片转存中...(img-kMwLdoUL-1712860853147)]