Spark 核心之 Shuffle 文件寻址详解

摘要:Reduce Task 如何找到成百上千个 Map Task 输出的 Shuffle 文件?如何精准定位到目标分区的数据块?本文从 MapOutputTracker 全局注册表、MapStatus 元数据结构、IndexShuffleBlockResolver 的 Index→Data 解析链、Netty 远程 Block 拉取、ExternalShuffleService 退出守护五个维度,配合 2 张架构图 + 源码追踪,完整拆解 Spark Shuffle 文件寻址的端到端流程。

关键词:MapOutputTracker, MapStatus, ShuffleBlockId, IndexShuffleBlockResolver, BlockManager, ExternalShuffleService


一、开篇:Reduce Task 的寻址困境

假设一个 ShuffleDependency 有 1000 个 Map Task ,200 个 Reduce 分区。Reduce Task-5 需要从 1000 个不同的 Executor 上拉取属于自己分区的数据。它是怎么知道去哪个节点、读取哪个文件、文件中的哪一段数据的?

复制代码
Shuffle 寻址三要素
① 哪个节点?→ MapOutputTracker 查 MapStatus.location
② 哪个文件?→ shuffle_X_Y_0.data
③ 哪段数据?→ IndexFile[offset+length] → DataFile.seek(offset)

二、MapOutputTracker 架构全景

2.1 MapOutputTrackerMaster --- 全局注册表

scala 复制代码
class MapOutputTrackerMaster(conf: SparkConf) {
  // key: shuffleId, value: Array[MapStatus] 按 mapId 索引
  val mapStatuses = new ConcurrentHashMap[Int, Array[MapStatus]]()

  // Map Task 完成时调用
  def registerMapOutput(shuffleId: Int, mapId: Int, status: MapStatus): Unit

  // Reduce Task 查询时调用
  def getMapSizesByExecutorId(shuffleId: Int, reduceId: Int)
      : Seq[(BlockManagerId, Seq[(BlockId, Long)])]
}

2.2 MapStatus --- 每个 Map Task 的寻址元数据

scala 复制代码
case class MapStatus(location: BlockManagerId, partitionSizes: Array[Long])
// location         → Executor 的 host + port + executorId
// partitionSizes   → 每个 Reduce 分区的压缩后大小
//                   (Reduce Task 通过前 N-1 个分区大小累加得到第 N 个分区 offset)

2.3 MapOutputTrackerWorker --- Executor 端本地缓存

scala 复制代码
// Reduce Task 调用入口
BlockStoreShuffleReader.read():
  val statuses = mapOutputTracker.getMapSizesByExecutorId(shuffleId, reduceId)
  // → 返回 (BlockManagerId, Seq[(ShuffleBlockId, dataSize)]) 列表
  // → BlockManager.getBlockData() 逐一拉取

三、Index File → Data File 解析链

3.1 三步走:BlockId → Offset → Data

scala 复制代码
// ① 构造 BlockId
val blockId = ShuffleBlockId(shuffleId, mapId, reduceId)

// ② IndexShuffleBlockResolver.getBlockData(blockId)
val indexFile = getIndexFile(shuffleId, mapId)   // shuffle_X_Y_0.index
indexFile.seek(reduceId * 16L)                     // 定位到分区索引
val offset = indexFile.readLong()                  // 8 字节 offset
val length = indexFile.readLong()                  // 8 字节 length

// ③ 读取 data file
val dataFile = getDataFile(shuffleId, mapId)       // shuffle_X_Y_0.data
dataFile.seek(offset); dataFile.readFully(buf, 0, length)

3.2 IndexShuffleBlockResolver 源码

scala 复制代码
override def getBlockData(blockId: ShuffleBlockId): ManagedBuffer = {
  val indexFile = getIndexFile(blockId.shuffleId, blockId.mapId)
  val in = new DataInputStream(new FileInputStream(indexFile))
  in.skip(blockId.reduceId * 16L)            // 跳过前 reduceId 个分区索引
  val offset = in.readLong()                  // ← 关键
  val nextOffset = in.readLong()              // 下一个分区的 offset
  in.close()
  new FileSegmentManagedBuffer(getDataFile(...), offset, nextOffset - offset)
}
// reduceId × 16 定位 → 一次 seek + read → 返回 ManagedBuffer

四、远程拉取:Netty + BlockManager

复制代码
Reduce 端 ShuffleClient.fetchBlocks()
  → NettyBlockTransferService.fetchBlocks(host, port, execId, blockIds)
    → Netty 连接远端 BlockManager
    → 服务端 NettyBlockRpcServer.receive(OpenBlocks)
      → BlockManager.getBlockData(blockId)
        → IndexShuffleBlockResolver.getBlockData()
    → FileRegion.transferTo() 发送数据块(零拷贝)

重试: spark.shuffle.io.maxRetries=3
退避: spark.shuffle.io.retryWait=5s

五、ExternalShuffleService:Executor 退出后的守护

复制代码
正常模式(无 ESS):
  Executor 退出 → Shuffle 数据丢失 → 重算 Map Task

ExternalShuffleService(ESS):
  Executor 退出 → ESS 仍持有 shuffle 文件
  → MapOutputTracker 中的 MapStatus.location 指向 ESS
  → Reduce Task 直接从 ESS 拉取

配置: spark.shuffle.service.enabled=true
端口: spark.shuffle.service.port=7337

六、寻址公式总结

复制代码
Reduce Task 端到端寻址流程:
① getMapSizesByExecutorId(shuffleId, reduceId)
   → MapOutputTrackerWorker 向 Driver 请求 MapStatus 列表
② 遍历每个 MapStatus:
   Node = status.location
   BlockId = ShuffleBlockId(shuffleId, mapId, reduceId)
   Size = status.partitionSizes(reduceId)
③ fetchBlocks(Node, Seq(BlockId))
   → Netty → 远端 BlockManager → getBlockData(BlockId)
④ getBlockData = IndexFile.seek(reduceId × 16) → offset+length → DataFile
⑤ 解压 → 反序列化 → ExternalAppendOnlyMap → Iterator

七、总结

  1. MapOutputTracker:Driver 端全局注册表 + Executor 端本地缓存。Map Task 完成后注册 MapStatus → Reduce Task 获取 (节点, BlockId, 大小) 列表。

  2. Index File 精确定位 :每分区 16B 索引,reduceId × 16 一次 seek 即获 offset+length,无需全量扫描。

  3. ExternalShuffleService:YARN 下 Executor 退出后数据不丢失,减少 Map Task 重算。


作者 :starzy

博客 :blog.starzy.cn

GitHub :starzy1990.github.io

专注 AI Agent · LangGraph · RAG · 大数据架构 · 数据工程实践

相关推荐
silvamux_13 分钟前
MCP 协议如何让大模型完成工具调用
大数据·服务器·elasticsearch·fastapi·vllm
Mars-xq15 分钟前
家庭膳食营养速查表
大数据·人工智能
Earth explosion35 分钟前
企业级Agent:MCP正在成为新的控制平面
大数据·平面
benchmark_cc42 分钟前
实时行情系统如何避免无意义的重复请求?从请求去重到缓存设计
大数据·开发语言·python·pandas·量化交易·股票数据·quantdash
Data analyse4561 小时前
App埋点版本兼容的字段迁移校验
大数据·运维·服务器·前端·数据库·数据分析
蝶依斓(湖南)全案整装1 小时前
整装企业多平台内容差异化表达方法:形态适配与事实一致
大数据
Data analyse4561 小时前
Nginx日志访客识别的边界处理
大数据·运维·服务器·数据库·数据分析
Shulex1 小时前
Shopify 独立站弃购挽回与客服实时转化:Shulex 实战架构指南
大数据·人工智能·架构·智能客服·跨境电商
sunshine8851 小时前
关联交易抵消与内部往来清理:合并报表中的“排雷”策略
大数据·数据库·人工智能
Qyr991 小时前
2026-2032年全球纤维素酶行业全景深度研究报告
大数据