External Shuffle Service(spark3_shuffle):原理、配置与排障
目录
- ESS 解决什么问题
- 架构与工作原理
- 生产故障复盘(本案)
- Hadoop 集群侧配置(NodeManager)
- Spark Client 侧配置
- 多版本共存:spark_shuffle + spark3_shuffle
- 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,不直连写端的 executor2.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.xmlyarn.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 注册:
<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 不认识)。
spark-submit\--confspark.shuffle.service.enabled=true\--confspark.shuffle.service.name=spark3_shuffle\--confspark.shuffle.service.port=7447\...优先级(高→低):代码SparkConf.set>--conf>--properties-file>spark-defaults.conf(SPARK_CONF_DIR优先于$SPARK_HOME/conf)。
落地方式:写进 Spark 3 任务提交网关的spark-defaults.conf或平台提交模板,所有任务默认继承。确认实际生效的那份文件:
grep-r"shuffle.service"$SPARK_CONF_DIR/spark-defaults.conf2>/dev/null\||grep-r"shuffle.service"$SPARK_HOME/conf/spark-defaults.conf2>/dev/null也可从运行中任务反查:YARN UI → application → Environment 页签搜shuffle.service。
版本约束:spark.shuffle.service.name3.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.0package.scala:815-826);注册链路同构(4.0BlockManager.scala:554, 601-636,重试 3 次/5s 不变);serviceData 投递主路径不变(4.0ExecutorRunnable.scala:127-140);YarnShuffleService类与默认名不变(4.0YarnShuffleService.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.0package.scala:2773;YarnShuffleService.java | 纯新增,默认行为不变 |
8. 排障手册
8.1 症状 → 根因速查
| 症状 | 根因方向 |
|---|---|
executorConnection 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 节点自行执行)
# 1. 端口监听确认(区分 aux-service 内嵌 vs 独立进程)ss-lntp|grep-E'7337|7447'ps-ef|grep-iExternalShuffleService|grep-vgrep# 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 时查)sudoiptables-L-n|grep-i7447# 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 实例未受影响。