External Shuffle Service(spark3_shuffle):原理、配置与排障

External Shuffle Service(spark3_shuffle):原理、配置与排障

目录

  1. [ESS 解决什么问题](#ESS 解决什么问题)
  2. 架构与工作原理
  3. 生产故障复盘(本案)
  4. [Hadoop 集群侧配置(NodeManager)](#Hadoop 集群侧配置(NodeManager))
  5. [Spark Client 侧配置](#Spark Client 侧配置)
  6. [多版本共存:spark_shuffle + spark3_shuffle](#多版本共存:spark_shuffle + spark3_shuffle)
  7. [Spark 4.0 迁移差异](#Spark 4.0 迁移差异)
  8. 排障手册

1. ESS 解决什么问题

正常路径下,map 端 executor 把 shuffle 文件写在自己容器挂载的本地磁盘上,只有该 executor 进程活着才能对外提供读取。一旦 executor 死亡(OOM 被 YARN 杀、Dynamic Allocation 回收、抢占、speculation),本节点上的 shuffle 文件就"无人认领",reduce 端 FetchFailed,只能整 stage 重算。

ESS(External Shuffle Service)的核心设计是把 shuffle 文件的"读服务"与 executor 的生命周期解耦:一个常驻在每个 NodeManager 上的服务,executor 死了它还在,继续替死掉的 executor 提供本节点 shuffle 文件的读取。

三个直接受益场景:

场景 依赖 ESS 的原因
Dynamic Allocation(classic 模式) 空闲 executor 被回收后,它的 shuffle 数据仍可读;不开 ESS 时 classic 动态分配无法工作(需改用 spark.dynamicAllocation.shuffleTracking.enabled=true 替代)
executor 被杀 / 节点故障恢复 已写完的 shuffle 数据不用重算,代价从"整 stage 重试"降为"重算丢失的分区"
Push-based shuffle(3.2+) merge 目录元数据由 executor 注册时提交给 ESS,merge 数据读取也走 ESS(见第 2 节注册顺序)

注意 :ESS 不是 Spark 强制组件。spark.shuffle.service.enabled=false(默认)+ 静态资源、不开 Dynamic Allocation 时可以完全不用;但 executor 一死 shuffle 数据即丢。启用 ESS 后它是硬依赖------本节点的 ESS 不在,executor 直接注册失败自杀(见第 3 节故障)。

2. 架构与工作原理

2.1 部署形态(两种,功能等价)

形态 运行位置 适合
YARN aux-service(推荐) NodeManager JVM 内部,类 org.apache.spark.network.yarn.YarnShuffleService YARN 集群标准做法,随 NM 生命周期管理
独立进程 每节点单独拉起 org.apache.spark.deploy.ExternalShuffleService(Spark 3.4+ 支持 standalone 模式守护) 非 YARN(Standalone/K8s)或需要与 NM 解耦运维时

aux-service 名(如 spark_shuffle/spark3_shuffle)只是 yarn-site.xml 里的注册名,不是代码里的固定值 ------4.0 中 YarnShuffleService.java:219 的 super("spark_shuffle") 仅是构造默认值,NM 实际按 yarn-site.xml 注册的名字实例化,并按这个名字隔离恢复目录、路由 serviceData。

2.2 整体拓扑

复制代码
Driver(AM) ── startContainer(serviceData=按service name投递secret) ──▶ NodeManager
                                                                        │
                                                          spark3_shuffle (ESS, :7447)
                                                          恢复目录: .../nm-aux-services/spark3_shuffle/
                                                                        ▲ 注册(BlockManager.initialize)
executor ──────────────────────────────────────────────────────────────┘
   │ MapStatus 里记录的 shuffle 地址 = ESS 的 BlockManagerId(host, servicePort)
   ▼
reduce 端 fetch shuffle 数据 ──▶ 全部经 ESS,不直连写端的 executor

2.3 注册链路(源码级)

以 3.3 为准(4.0 完全同构,行号见括号):

  1. executor 启动 → BlockManager.initialize 。core/src/main/scala/org/apache/spark/storage/BlockManager.scala:526-531(4.0: L550-556):shuffle.service.enabled=true 时先构造 shuffleServerId = BlockManagerId(executorId, hostName, externalShuffleServicePort)------端口取客户端配置的 spark.shuffle.service.port ,然后调用 registerWithExternalShuffleServer()(driver 不注册)。
  2. 注册先于 BlockManagerMaster 注册,顺序不可反 。push-based shuffle 的 merge 目录元数据要随注册报给 ESS(SPARK-39647,BlockManager.scala:520-525 注释;4.0 同)。
  3. 同步注册 + 重试 。BlockManager.scala:579-610(4.0: registerWithExternalShuffleServer() L601-636):按 spark.shuffle.registration.maxAttempts(默认 3)重试,间隔硬编码 5s;全失败抛 unableToRegisterWithExternalShuffleServerError → YarnCoarseGrainedExecutorBackend 自杀 → container exit code 1。
  4. 秘钥随 container 启动按名字投递 。AM 侧 resource-managers/yarn/src/main/scala/org/apache/spark/deploy/yarn/ExecutorRunnable.scala:117-121(4.0: configureServiceData L127-140):以 spark.shuffle.service.name 为 key 把 secret 作为 serviceData 放进 ContainerLaunchContext,NM 按名字路由 给对应 aux-service 实例。名字不匹配时秘钥送不到服务实例------yarn 侧会打 WARN("send ServiceData and read Service Meta Data may have issues unless the refer to the name in the config");未开鉴权时注册仍能靠端口连通,开了 spark.authenticate=true 后名字错直接导致 shuffle 拉取失败。
  5. 注册信息持久化(recovery) 。ESS 把 appId → executor localDirs 映射持久化到 yarn.nodemanager.recovery.dir/nm-aux-services/<service名>/(本案实测:/var/lib/hadoop-yarn/yarn-nm-recovery/nm-aux-services/spark3_shuffle/registeredExecutors.ldb)。3.3 隐式 LevelDB;3.4 起可用 spark.shuffle.service.db.backend 选 LEVELDB/ROCKSDB,4.0 默认 ROCKSDB(见第 7 节)。目录按 service 名隔离,这是多版本共存的基础之一。

2.4 一致性原则:三方对齐

ESS 能用起来的唯一硬约束是三方一致:

一致项 配置处
yarn-site.xml yarn.nodemanager.aux-services 列表中的名字 ↔ yarn.nodemanager.aux-services.<名字>.class 的前缀
客户端 spark.shuffle.service.name(3.2.0 引入) ↔ NM 注册的名字(一字不差)
客户端 spark.shuffle.service.port ↔ 该 service 实例监听端口(多实例并存时必须错开)

名字本身随意,对齐即可;不配 name 时默认 spark_shuffle、不配 port 时默认 7337(core/.../internal/config/package.scala,3.3 L716-727 / 4.0 L815-826,默认值两代相同)。

3. 生产故障复盘(本案)

现象:Spark 3.3 任务(application_1791943697415_0001)所有落在 mater01(127.0.0.1)的 container 失败,executor 日志:

复制代码
ERROR YarnCoarseGrainedExecutorBackend: Executor self-exiting due to : Unable to create
executor due to Unable to register with external shuffle server due to :
Failed to connect to mater01.catlbattery.com/127.0.0.1:7337
Caused by: java.net.ConnectException: Connection refused
  at BlockManager.registerWithExternalShuffleServer(BlockManager.scala:596)

定位过程:

  1. 客户端日志锁定调用链 BlockManager.initialize → registerWithExternalShuffleServer → TransportClientFactory.createClient,Connection refused 语义 = 目标端口无监听(防火墙丢包通常是 timeout)。
  2. 查 NodeManager 日志,发现该集群 ESS 实际状态:YarnShuffleService: Started YARN shuffle service for Spark on port 7447,恢复目录 /nm-aux-services/spark3_shuffle/------Spark 3 的 ESS 活着,但监听 7447,注册名 spark3_shuffle。
  3. 根因:NM 侧 Spark 3 ESS = spark3_shuffle : 7447,客户端未配置 spark.shuffle.service.name/port,按默认 spark_shuffle : 7337 去连 → 端口不匹配 → Connection refused。
  4. 日志中另一组 ClassNotFoundException: org.apache.spark.network.yarn.YarnShuffleService 属于 spark_shuffle(Spark 2 那套)初始化失败,与 Spark 3 任务无关。

结论表:

问题 结论 证据强度
直接报错 executor 注册 ESS 重试耗尽自杀,container exit 1 确定
根因 客户端默认 7337 与集群 spark3_shuffle 实际监听 7447 不匹配 确定(NM 日志 + 源码)
修复 客户端补 spark.shuffle.service.name=spark3_shuffle + spark.shuffle.service.port=7447 已确认方案

预防项 :spark.yarn.shuffle.stopOnFailure=true(docs/running-on-yarn.md)------ESS 初始化失败时 NM 主动不注册资源,避免 container 被调度到 ESS 坏掉的节点。

4. Hadoop 集群侧配置(NodeManager)

每台 NM 执行(官方流程见 docs/running-on-yarn.md「Configuring the External Shuffle Service」章节):

  1. jar 落位 :把 spark-<version>-yarn-shuffle.jar(构建产物 common/network-yarn/target,发行版在 yarn/ 目录)加入 NM classpath。多版本共存时各版本的 jar 都要落。
  2. yarn-site.xml 注册:
xml 复制代码
<property>
  <name>yarn.nodemanager.aux-services</name>
  <value>spark_shuffle,spark3_shuffle</value>
</property>
<property>
  <name>yarn.nodemanager.aux-services.spark_shuffle.class</name>
  <value>org.apache.spark.network.yarn.YarnShuffleService</value>
</property>
<property>
  <name>yarn.nodemanager.aux-services.spark3_shuffle.class</name>
  <value>org.apache.spark.network.yarn.YarnShuffleService</value>
</property>
  1. 每实例端口错开 :通过 spark-shuffle-site.xml(ESS 专用配置文件,放 Hadoop conf 目录)或 NM 环境变量为每个实例指定端口,如 Spark 2 → 7337、Spark 3 → 7447。恢复目录按 service 名自动隔离,无需额外配置。
  2. NM 堆放大 :yarn-env.sh 的 YARN_HEAPSIZE(默认 1000MB)适当调大,避免 shuffle 高峰 GC。
  3. 滚动重启全部 NodeManager(aux-service 随 NM 启动,无法单独重载)。
  4. 可选:spark.yarn.shuffle.stopOnFailure=true(见第 3 节)、4.0 的 spark.yarn.shuffle.server.recovery.disabled(见第 7 节)。

相关键全集(4.0 实测 core/.../internal/config/package.scala:771-826,3.3 同名子集语义一致):

键 默认 说明
spark.shuffle.service.enabled false 客户端开关,ESS 是否为硬依赖
spark.shuffle.service.port 7337 客户端连的端口;ESS 侧监听端口需另行对齐
spark.shuffle.service.name spark_shuffle(3.2.0+) 客户端 serviceData 路由名
spark.shuffle.service.removeShuffle true(3.3.0+) ESS 是否在 shuffle 不再需要时清理块;4.0 起默认真正生效(见第 7 节)
spark.shuffle.service.fetch.rdd.enabled false ESS 是否代理 RDD block 读取
spark.shuffle.service.db.enabled true 仅 standalone 形态的持久化开关
spark.shuffle.service.db.backend 4.0=ROCKSDB(3.4.0 引入;3.3 无此键,隐式 LEVELDB) 见第 7 节
spark.shuffle.registration.timeout / .maxAttempts 5000ms / 3 客户端注册重试

5. Spark Client 侧配置

spark.shuffle.service.name/port 是 SparkConf 级参数,只配在客户端,yarn-site.xml 里写无效(Hadoop 不认识)。

bash 复制代码
spark-submit \
  --conf spark.shuffle.service.enabled=true \
  --conf spark.shuffle.service.name=spark3_shuffle \
  --conf spark.shuffle.service.port=7447 \
  ...

优先级(高→低):代码 SparkConf.set > --conf > --properties-file > spark-defaults.conf(SPARK_CONF_DIR 优先于 $SPARK_HOME/conf)。

落地方式 :写进 Spark 3 任务提交网关的 spark-defaults.conf 或平台提交模板,所有任务默认继承。确认实际生效的那份文件:

bash 复制代码
grep -r "shuffle.service" $SPARK_CONF_DIR/spark-defaults.conf 2>/dev/null \
  || grep -r "shuffle.service" $SPARK_HOME/conf/spark-defaults.conf 2>/dev/null

也可从运行中任务反查:YARN UI → application → Environment 页签搜 shuffle.service。

版本约束 :spark.shuffle.service.name 3.2.0 起生效;Spark 2 任务没有也不读这个参数,天然走默认 spark_shuffle。

6. 多版本共存:spark_shuffle + spark3_shuffle

双版本并行的标准姿势(本案集群即此形态):

维度 Spark 2 Spark 3
aux-service 注册名 spark_shuffle spark3_shuffle(名字自定义,与 client 对齐即可)
监听端口 7337 7447(必须错开)
恢复目录 .../nm-aux-services/spark_shuffle/ .../nm-aux-services/spark3_shuffle/(按名隔离,自动)
yarn-shuffle jar Spark 2 发行包 Spark 3/4 发行包
客户端配置 无需 name/port(默认即中) 显式 name=spark3_shuffle + port=7447

要点:

  • 两个实例是两个独立进程内对象 ,各自持有端口、恢复目录、注册表,互不感知;spark_shuffle 实例挂掉不影响 spark3_shuffle(本案 Spark 2 实例 CNFE 起不来,Spark 3 任务照常可跑,反之亦然)。
  • client 侧 spark.shuffle.service.name 写错的服务名 = 秘钥投递错对象;未开鉴权时靠端口兜底仍能注册,开鉴权后必挂。所以 name/port 建议同时配齐,不要只修端口。
  • 两套 spark-defaults.conf 建议分开维护,避免把 4.0 专属参数混进 Spark 2 的全局配置。

7. Spark 4.0 迁移差异

总体结论:机制与 3.3 完全同源,配置方式无差异,仅默认行为有三处变化。

兼容面(已核验):enabled/port/name 三键键名、默认值、语义不变(4.0 package.scala:815-826);注册链路同构(4.0 BlockManager.scala:554, 601-636,重试 3 次/5s 不变);serviceData 投递主路径不变(4.0 ExecutorRunnable.scala:127-140);YarnShuffleService 类与默认名不变(4.0 YarnShuffleService.java:219, 338);spark.shuffle.useOldFetchProtocol 兼容键仍在。spark3_shuffle + 7447 的显式配置可直接沿用;NM 侧 yarn-shuffle.jar 整套替换为 4.0 版本即可。

三处差异:

# 差异 证据 影响
1 ESS 本地 db 默认 LEVELDB → ROCKSDB,LEVELDB deprecated,且不自动迁移旧数据 docs/core-migration-guide.md L35;package.scala:805-813 升级 ESS jar 重启后新建 RocksDB store,旧 LevelDB 保留但不读 → ESS 重启时无法恢复重启前注册的 executor → 运行中任务 fetch 失败。对策:①升级窗口避开运行中任务;②过渡期在 ESS 的 spark-shuffle-site.xml 显式设 db.backend=LEVELDB(仍支持,deprecated)
2 4.0 起默认由 ESS 清理 deallocated executor 的 shuffle 块(spark.shuffle.service.removeShuffle,回退设 false) docs/core-migration-guide.md L47 键 3.3 已存在且默认 true;从未显式配置的集群行为升级,属正向优化(清理更及时)
3 新增 spark.yarn.shuffle.server.recovery.disabled(默认 false)与 YarnShuffleServiceMetrics(metrics2 观测) 4.0 package.scala:2773;YarnShuffleService.java 纯新增,默认行为不变

8. 排障手册

8.1 症状 → 根因速查

症状 根因方向
executor Connection refused 注册 ESS(BlockManager.registerWithExternalShuffleServer)后自杀、container exit 1 目标端口无监听:ESS 未起 / 端口不匹配(最常见) / 防火墙 REJECT
注册连上了但 shuffle fetch 报 executor 找不到 / 鉴权失败 spark.shuffle.service.name 与 NM 注册名不匹配,serviceData(秘钥)投错对象
NM 日志 ClassNotFoundException: org.apache.spark.network.yarn.YarnShuffleService 对应实例的 yarn-shuffle.jar 不在 NM classpath(注意区分是哪个实例,另一实例可能正常)
ESS 重启后运行中任务大量 FetchFailed recovery 未生效(4.0 切 RocksDB 后旧 LevelDB 不迁移,见第 7 节)

8.2 标准排查命令(NM 节点自行执行)

bash 复制代码
# 1. 端口监听确认(区分 aux-service 内嵌 vs 独立进程)
ss -lntp | grep -E '7337|7447'
ps -ef | grep -i ExternalShuffleService | grep -v grep

# 2. NM 日志确认每个实例的初始化结果与端口
grep -iE "YarnShuffleService|spark3?_shuffle|AuxServices" \
  $(ls -tr /var/log/hadoop-yarn/*nodemanager* 2>/dev/null) | tail -30

# 3. 远程探测(executor 视角复现)
nc -zv <NM_IP> 7447

# 4. 排除防火墙 REJECT(有监听但仍 refused 时查)
sudo iptables -L -n | grep -i 7447

# 5. 恢复目录确认(名字隔离的证据)
ls /var/lib/hadoop-yarn/yarn-nm-recovery/nm-aux-services/

8.3 修复后验证清单

  1. ss -lntp 确认目标端口 LISTEN;
  2. 客户端带齐 name + port 重跑任务;
  3. executor 日志出现 Registering executor with the local external shuffle service. 且无 refused;
  4. YARN UI 确认任务正常完成;多版本共存时顺带确认另一版本的 ESS 实例未受影响。
相关推荐
geovindu1 小时前
rust: Template Method Pattern
开发语言·后端·设计模式·rust·行为模式·模板方法模
high一马平川1 小时前
# SystemUI Dagger2 精读 · 第 2 章 @Inject:1200 处构造注入,和 1 处例外
android·java·kotlin
小陈从不会敲代码1 小时前
JavaEE面试复习(多线程-初阶)
java·面试·java-ee
周厉1 小时前
酒店PMS系统设计:房型房价管理与动态价格优先级实现(Java/MySQL)
java·数据库·pms酒店系统
一碗白开水一2 小时前
Day 5 · 弹性 | 微观
java·前端·数据库
迪飞特科技2 小时前
大模型接入业务系统:API、本地部署、RAG、智能体四种路线怎么选
java·数据库·人工智能·知识图谱·deepseek·本地化大模型
vipxieliang2 小时前
ValidX 验证器缓存机制:如何提升验证性能
java·spring boot
未秃头的程序猿2 小时前
Spring Cloud Gateway 做统一加解密,我在请求体上栽了三天
java·后端·架构