External Shuffle Service(spark3_shuffle):原理、配置与排障
目录
- [ESS 解决什么问题](#ESS 解决什么问题)
- 架构与工作原理
- 生产故障复盘(本案)
- [Hadoop 集群侧配置(NodeManager)](#Hadoop 集群侧配置(NodeManager))
- [Spark Client 侧配置](#Spark Client 侧配置)
- [多版本共存:spark_shuffle + spark3_shuffle](#多版本共存:spark_shuffle + spark3_shuffle)
- [Spark 4.0 迁移差异](#Spark 4.0 迁移差异)
- 排障手册
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 完全同构,行号见括号):
- 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 不注册)。 - 注册先于 BlockManagerMaster 注册,顺序不可反 。push-based shuffle 的 merge 目录元数据要随注册报给 ESS(SPARK-39647,
BlockManager.scala:520-525注释;4.0 同)。 - 同步注册 + 重试 。
BlockManager.scala:579-610(4.0:registerWithExternalShuffleServer()L601-636):按spark.shuffle.registration.maxAttempts(默认 3)重试,间隔硬编码 5s;全失败抛unableToRegisterWithExternalShuffleServerError→YarnCoarseGrainedExecutorBackend自杀 → container exit code 1。 - 秘钥随 container 启动按名字投递 。AM 侧
resource-managers/yarn/src/main/scala/org/apache/spark/deploy/yarn/ExecutorRunnable.scala:117-121(4.0:configureServiceDataL127-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 拉取失败。 - 注册信息持久化(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)
定位过程:
- 客户端日志锁定调用链
BlockManager.initialize → registerWithExternalShuffleServer → TransportClientFactory.createClient,Connection refused语义 = 目标端口无监听(防火墙丢包通常是 timeout)。 - 查 NodeManager 日志,发现该集群 ESS 实际状态:
YarnShuffleService: Started YARN shuffle service for Spark on port 7447,恢复目录/nm-aux-services/spark3_shuffle/------Spark 3 的 ESS 活着,但监听 7447,注册名 spark3_shuffle。 - 根因:NM 侧 Spark 3 ESS =
spark3_shuffle : 7447,客户端未配置spark.shuffle.service.name/port,按默认spark_shuffle : 7337去连 → 端口不匹配 → Connection refused。 - 日志中另一组
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」章节):
- jar 落位 :把
spark-<version>-yarn-shuffle.jar(构建产物common/network-yarn/target,发行版在yarn/目录)加入 NM classpath。多版本共存时各版本的 jar 都要落。 - 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>
- 每实例端口错开 :通过
spark-shuffle-site.xml(ESS 专用配置文件,放 Hadoop conf 目录)或 NM 环境变量为每个实例指定端口,如 Spark 2 → 7337、Spark 3 → 7447。恢复目录按 service 名自动隔离,无需额外配置。 - NM 堆放大 :
yarn-env.sh的YARN_HEAPSIZE(默认 1000MB)适当调大,避免 shuffle 高峰 GC。 - 滚动重启全部 NodeManager(aux-service 随 NM 启动,无法单独重载)。
- 可选:
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 修复后验证清单
ss -lntp确认目标端口 LISTEN;- 客户端带齐
name+port重跑任务; - executor 日志出现
Registering executor with the local external shuffle service.且无 refused; - YARN UI 确认任务正常完成;多版本共存时顺带确认另一版本的 ESS 实例未受影响。