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

GitHubstarzy1990.github.io

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

相关推荐
大大大大晴天1 小时前
实时数仓怎么建:基于 StarRocks 的分层设计与实践
大数据
roman_日积跬步-终至千里3 小时前
【计算资源节省与稳定性治理】自助查询系统请求幂等性设计
大数据
小玮看世界3 小时前
当“安全“变成“教训“:《拟人化暂行办法》时代,AI护栏的过度拒答之困与破局
大数据·人工智能
FserSuN5 小时前
回顾Spark的概念与应用
大数据·分布式·spark
一知半解仙5 小时前
大国博弈与机器人纪元:我们正站在怎样的历史拐点?
大数据·人工智能·机器人
亚古数据5 小时前
亚古数据:新西兰公司工商报告Company Extract全解析
大数据·人工智能
baopixiaoz6 小时前
AI量化策略师|Web3 量化交易研究员
大数据·人工智能·python·区块链
IT古董6 小时前
【WMS学习笔记系列】01-需求分析
大数据·数据库
成长之路5146 小时前
【数据集】MD&A管理层讨论与分析文本(2001-2025年)
大数据
浅念-8 小时前
MySQL 索引底层完整详解|磁盘Page|B+树推导|聚簇非聚簇索引|索引SQL操作
大数据·数据库·b树·sql·mysql·面试·职场和发展