Spark 资源配置与 Shuffle 故障排查生产手册

1. 文档目的

本文沉淀 Linkis 0.9.3 对接公司定制版 Spark 2.4.5/YARN 时的生产经验。该发行版包含上游 Spark 2.4.5 未必具备的 AQE 实现,涉及 AdaptiveSparkPlanExecShuffleQueryStage 的结论不能直接套用到未经确认的上游版本。本文重点回答:

  • Spark Engine 如何取得 Driver、Executor 资源参数;
  • 没有配置页面时,如何安全修改用户级数据库配置;
  • 固定 Executor 与动态资源分配如何选择;
  • 如何区分 Shuffle 表象、External Shuffle Service 故障和 Executor 到 Driver 的通信故障;
  • 配置变更如何发布、验证和回滚。

本文示例用户为 xxx,生产执行前应替换为实际用户并先备份原值。

2. 架构结论

Linkis 不直接执行 Shuffle。Linkis EngineManager 使用 spark-submit 启动用户级 Spark Engine,Spark Driver 划分 Stage 和调度任务,Executor 负责计算、写出和读取 Shuffle 数据;启用 External Shuffle Service 时,YARN NodeManager 中的 Shuffle Service 负责在 Executor 退出后继续提供本地 Shuffle 文件。

本项目使用:

text 复制代码
--master yarn
--deploy-mode client

因此 Driver 是 EngineManager 所在节点拉起的用户进程。所有 YARN Executor 都必须能够访问 Driver 对外发布的 RPC 地址和随机端口。生产网络策略不能只放通固定的 Linkis HTTP 端口。

3. 资源参数的生效链路

提交参数位于:

text 复制代码
params.configuration.startup

EngineManager 从启动属性读取:

text 复制代码
spark.driver.memory
spark.executor.memory
spark.executor.cores
spark.executor.instances

并生成:

text 复制代码
--driver-memory
--executor-memory
--executor-cores
--num-executors

数据库中的用户非空值优先于 default_value。内存值在表中只存数字,例如 8;代码会自动拼成 8G

资源估算:

text 复制代码
Executor 堆内存总量 = executor.instances × executor.memory
Executor CPU 总量    = executor.instances × executor.cores

YARN 实际申请内存还包含 memory overhead,因此会大于上述堆内存总量。

4. 无页面时修改用户资源配置

用户配置保存在 linkis_config_key_user

  • application_id:计算引擎应用 ID,本例为 spark
  • key_id:请求应用(creator)下的配置键 ID,本例为 IDE
  • user_name:配置所属用户;
  • value:用户覆盖值。

4.1 修改前查询

不要仅凭固定 ID 修改。先通过应用、creator、配置树和 key 联合定位:

sql 复制代码
SELECT
    ku.id,
    app.name AS engine_name,
    creator.name AS creator_name,
    k.id AS key_id,
    k.`key`,
    k.default_value,
    ku.user_name,
    ku.`value`
FROM linkis_config_key k
JOIN linkis_application creator
    ON creator.id = k.application_id
JOIN linkis_config_key_tree kt
    ON kt.key_id = k.id
JOIN linkis_config_tree t
    ON t.id = kt.tree_id
JOIN linkis_application app
    ON app.id = t.application_id
LEFT JOIN linkis_config_key_user ku
    ON ku.key_id = k.id
   AND ku.application_id = app.id
   AND ku.user_name = 'logsget'
WHERE creator.name = 'IDE'
  AND app.name = 'spark'
  AND k.`key` IN (
      'spark.executor.instances',
      'spark.executor.memory',
      'spark.executor.cores',
      'spark.driver.memory'
  );

4.2 本次采用的用户配置

text 复制代码
spark.executor.instances = 2
spark.executor.cores     = 2
spark.executor.memory    = 8
spark.driver.memory      = 8

这表示初始 Executor 为 2 个,每个 Executor 为 8G/2 cores,Driver 为 8G。

本次查询得到的映射为:

ID 配置键 新值
25 spark.executor.instances 2
27 spark.executor.cores 2
29 spark.executor.memory 8
33 spark.driver.memory 8

固定 ID 仅用于核对,不应作为跨环境更新条件。以下 SQL 按 engine、creator、key 和用户定位,并兼容用户记录尚不存在的情况:

sql 复制代码
START TRANSACTION;

SET @spark_app_id = (
    SELECT id FROM linkis_application WHERE name = 'spark' LIMIT 1
);

INSERT INTO linkis_config_key_user
    (application_id, key_id, user_name, `value`)
SELECT DISTINCT
    @spark_app_id,
    k.id,
    'logsget',
    CASE k.`key`
        WHEN 'spark.executor.instances' THEN '2'
        WHEN 'spark.executor.cores'     THEN '2'
        WHEN 'spark.executor.memory'    THEN '8'
        WHEN 'spark.driver.memory'      THEN '8'
    END
FROM linkis_config_key k
JOIN linkis_application creator
    ON creator.id = k.application_id
JOIN linkis_config_key_tree kt
    ON kt.key_id = k.id
JOIN linkis_config_tree t
    ON t.id = kt.tree_id
JOIN linkis_application app
    ON app.id = t.application_id
WHERE creator.name = 'IDE'
  AND app.name = 'spark'
  AND k.`key` IN (
      'spark.executor.instances',
      'spark.executor.cores',
      'spark.executor.memory',
      'spark.driver.memory'
  )
ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);

-- 提交前必须确认恰好返回 4 个唯一 key,且值与目标一致。
SELECT k.`key`, ku.`value`
FROM linkis_config_key_user ku
JOIN linkis_config_key k ON k.id = ku.key_id
WHERE ku.application_id = @spark_app_id
  AND ku.user_name = 'logsget'
  AND k.`key` IN (
      'spark.executor.instances',
      'spark.executor.cores',
      'spark.executor.memory',
      'spark.driver.memory'
  );

-- 此处暂停,不要自动提交。

生产执行前应先把查询结果导出为带时间戳的变更附件,记录"原记录不存在"与原值 NULL 的区别;确认连接的是主库并避开并发配置修改。若回查不是恰好 4 个唯一 key,执行 ROLLBACK;只有人工确认全部正确后,单独执行 COMMIT

4.3 默认值与用户值

不建议把修改 linkis_config_key.default_value 当作用户配置变更,因为已有的 linkis_config_key_user.value 会覆盖默认值。修改默认值还可能同时影响其他用户或同一 creator 下的其他引擎配置。

5. Executor 策略

5.1 固定 Executor

固定 Executor 的优点是资源规模可预测,不会形成扩容风暴;缺点是固定 2 个 Executor 面对大任务时并行度低,容易运行缓慢、增加 Shuffle 落盘和超时风险。

固定模式示例:

text 复制代码
--num-executors 2
--conf spark.dynamicAllocation.enabled=false

适用于故障隔离、容量验证和资源严格受控的任务,不建议在没有容量评估时作为所有大任务的长期统一配置。

5.2 有界动态分配

对于已经验证 External Shuffle Service、Driver 网络和容量的集群,可以保留动态能力,但必须设置合理上限。以下数值只是本环境的候选起始值,不是通用生产默认值:

text 复制代码
spark.dynamicAllocation.enabled=true
spark.shuffle.service.enabled=true
spark.dynamicAllocation.minExecutors=2
spark.dynamicAllocation.initialExecutors=2
spark.dynamicAllocation.maxExecutors=20

最大 20 个 Executor、每个 8G/2 cores 时:

text 复制代码
最大 Executor 堆内存约 160G
最大 Executor CPU 为 40 cores

容量评估至少应覆盖:

text 复制代码
单 Engine 最大内存 ≈ maxExecutors × (executorMemory + executorMemoryOverhead)
单 Engine 最大 CPU  = maxExecutors × executorCores
队列峰值             = 单 Engine 上限 × 同时活跃 Engine 数

还要计入 Driver/AM、队列中的其他应用以及节点装箱限制。maxExecutors 应根据 Spark UI、任务 SLA、Driver 承载能力和 YARN 队列容量逐步调整,不能直接设置为 1000。

Spark 2.4 的动态资源分配通常依赖 External Shuffle Service。启用前必须核验所有 NodeManager 的 yarn.nodemanager.aux-servicesYarnShuffleService 类和 Spark/YARN JAR 版本一致;检查 NodeManager 启动日志中服务初始化成功,并通过一次"Executor 被动态回收后,下游 Stage 仍能读取其 Shuffle 文件"的试运行验证。仅确认 NodeManager 进程存在是不够的。

6. 修正写死的动态分配参数

不要使用下面这种非标准形式:

scala 复制代码
addOpt("--spark.dynamicAllocation.enabled", Some("true"))
addOpt("--spark.dynamicAllocation.maxExecutors", Some("1000"))

标准 spark-submit 参数应通过 --conf 传递。下面两种方案必须二选一,不能同时发布。

故障隔离时可临时固定 Executor:

scala 复制代码
addOpt("--conf", Some("spark.dynamicAllocation.enabled=false"))

完成 External Shuffle Service 和容量验证后,采用有界动态分配:

scala 复制代码
addOpt("--conf", Some("spark.dynamicAllocation.enabled=true"))
addOpt("--conf", Some("spark.shuffle.service.enabled=true"))
addOpt("--conf", Some("spark.dynamicAllocation.minExecutors=2"))
addOpt("--conf", Some("spark.dynamicAllocation.initialExecutors=2"))
addOpt("--conf", Some("spark.dynamicAllocation.maxExecutors=20"))

以上硬编码修改会影响该 EngineManager 实例承载的所有新 Engine,不能作为单用户永久方案。长期方案应在 SparkResourceConfiguration 中用 CommonVars 定义这些键,再由 SparkSubmitProcessBuilderrequest.properties 或 EngineManager 配置文件读取,明确请求值、服务配置和代码默认值的优先级,避免每次调整都重新编译。

仓库同时存在 Spark 和 xsql 两份 SparkSubmitProcessBuilder。应根据生产实际部署模块修改;如果两套模块都会发布,应同步修正并通过测试防止配置漂移。特别注意检查重复添加同一个 maxExecutors 参数的情况。

7. 本次故障的证据链

7.1 表面错误

text 复制代码
Failed to materialize query stage: ShuffleQueryStage 1
Exchange hashpartitioning(..., 401)

它只能说明失败发生在 AQE 物化 Shuffle Stage 时,不能证明根因是 Shuffle 文件、内存或磁盘。

7.2 最底层错误

完整日志显示大量 Executor 无法连接:

text 复制代码
Failed to connect to /10.160.113.99:44755
Connection timed out

统计结果:

text 复制代码
连接 Driver 失败                         9881 次
Connection timed out                  19720 次
Container from a bad node              4912 次
Lost executor                          1023 次
FetchFailed                               0 次
OutOfMemoryError                          0 次

最终 Task 471 in stage 12.0 连续失败 4 次,Stage 被终止,随后被 AQE 包装为 Failed to materialize query stage

现有日志足以把本次故障归类为:

text 复制代码
Executor 到 client-mode Driver 的 RPC 连接故障

Executor 反复申请与连接超时同时大量出现,连接风暴是高概率放大因素,但仅凭当前日志尚不能证明它是最初原因。仍需结合 Driver CPU/GC、监听队列、网络连通性和事件时间线确定首因。当前证据可以排除典型的 Shuffle FetchFailed 和日志可见的 JVM OOM。

此外,节点 10.192.62.76 多次出现 Container from a bad node 和已终止的 Hadoop 线程池,应单独检查或临时隔离,但它不是唯一故障节点。

8. Shuffle 故障分类方法

排障必须沿异常链找到最底层 Caused by,不要停在 ShuffleQueryStage

最底层信号 主要方向
FetchFailedMetadataFetchFailed Shuffle 文件丢失、Executor/NodeManager 退出、网络读取失败
Unable to register with external shuffle server NodeManager External Shuffle Service、版本、端口或负载
Failed to connect to Driver Driver 地址/端口、网络、防火墙、RPC 过载、连接风暴
No space left on device NodeManager 本地盘容量或 inode
OutOfMemoryError、YARN memory limit Executor/Driver 堆或 overhead
ExecutorLostFailure: Slave lost 继续向前追 Container diagnostics 和 Executor stderr

9. 生产检查清单

9.1 Driver 节点

bash 复制代码
ss -lntp | grep <driver-port>
top -H -p <driver-pid>
jstat -gcutil <driver-pid> 1s 20
ls /proc/<driver-pid>/fd | wc -l
ulimit -n
ss -ant | grep ':<driver-port>' | awk '{print $1}' | sort | uniq -c

检查 CPU、Full GC、文件描述符、TCP backlog、conntrack、防火墙和路由。

多网卡、NAT 或严格防火墙环境还应核验 spark.driver.hostspark.driver.bindAddressspark.driver.portspark.blockManager.port。如需固定端口,必须先做端口冲突评估,并保证所有 NodeManager 到这些地址和端口双向可达。

9.2 YARN 节点

从多个 NodeManager 节点验证 Driver 连通性:

bash 复制代码
nc -vz -w 3 <driver-ip> <driver-port>

检查 NodeManager、Shuffle Service、本地磁盘和 inode:

bash 复制代码
jps | grep NodeManager
df -h
df -i

9.3 Spark 指标

通过 Spark UI 关注:

  • 同时存活和累计创建的 Executor 数;
  • Executor 添加/移除频率;
  • Driver CPU、GC 和调度延迟;
  • Shuffle Read/Write、Fetch Wait Time;
  • Spill、失败 Task 和数据倾斜;
  • 动态分配是否持续触达上限。

10. 发布、验证和回滚

数据库或代码修改不会改变已经运行的 Spark Engine。代码修改属于 EngineManager 全局变更,应先确认实际部署模块、实例数量、活动任务和维护窗口;多实例部署应摘流后逐台滚动,不能直接同时重启。标准步骤:

  1. 查询并保存修改前的数据库值;
  2. 在事务中更新用户配置并回查;
  3. 修改并编译实际部署的 Spark/xsql EngineManager,记录构建版本与校验和;
  4. 备份线上 JAR 和配置文件,确认目标路径、属主与权限;
  5. 摘流一个 EngineManager 实例,确认无正在初始化的 Engine 后替换并重启;
  6. 对单实例执行冒烟验证后再滚动其他实例;
  7. 停止测试用户旧 Spark Engine;
  8. 重新提交任务,创建新 Engine;
  9. 从启动日志核对最终 spark-submit 命令;
  10. 先运行小任务,再运行代表性大任务;
  11. 观察 Executor 数、Driver RPC 和 YARN 节点状态。

期望启动参数:

text 复制代码
--driver-memory 8G
--executor-memory 8G
--executor-cores 2
--num-executors 2
--conf spark.dynamicAllocation.enabled=true
--conf spark.shuffle.service.enabled=true
--conf spark.dynamicAllocation.minExecutors=2
--conf spark.dynamicAllocation.initialExecutors=2
--conf spark.dynamicAllocation.maxExecutors=20

验收至少要求:最终命令与方案一致;Executor 数不超过上限;代表性任务完成;观察窗口内不再出现 Driver 连接超时、Executor 创建风暴或 External Shuffle Service 注册失败。具体观察时长和错误阈值应在变更单中结合任务 SLA 明确。

触发回滚后,应停止继续滚动:恢复上一版 JAR 和配置文件并校验摘要,重启已变更实例;按变更附件恢复数据库原值(原记录不存在时删除新增记录,原值为 NULL 时恢复 NULL);重新创建测试用户 Engine 并复验。旧 Engine 与新配置混用会造成验证结论失真。

11. 经验结论

  1. ShuffleQueryStage 是失败位置,不是根因;必须读取最底层异常。
  2. spark.executor.instances 在动态分配开启时主要影响初始数量,不能限制最大 Executor 数。
  3. 动态分配必须有与 Driver、队列容量匹配的上限,1000 不是安全的通用默认值。
  4. 增加内存不能修复 RPC 连接超时、Shuffle Service 注册失败或坏节点。
  5. client 模式下 Driver 网络可达性属于生产关键依赖,应纳入发布前检查。
  6. 数据库用户值、代码默认值和 spark-submit --conf 必须统一核验,以最终启动命令为准。
  7. 所有配置变更必须通过新 Engine 验证,不能用存活的旧 Engine 判断是否生效。
相关推荐
roman_日积跬步-终至千里38 分钟前
量化投研中的缓慢变化维:从因子定义到回测可复现的底层逻辑
大数据·数据仓库·金融
roman_日积跬步-终至千里43 分钟前
常驻 Spark 引擎的稳定性风险:从一次 Linkis 任务失败说起
大数据·分布式·spark
liukuang1101 小时前
商汤2026上半年盈利6.2亿元,生成式AI业务占比近80%
大数据·人工智能
百胜软件@百胜软件1 小时前
AI是“放大器”也是“风险源”:零售企业如何驾驭这把双刃剑?
大数据·人工智能·零售
数字融合1 小时前
透明化数字孪生:未来医疗的核心平台
大数据·人工智能·virtualenv
JAVA面经实录9172 小时前
Kafka面试题标准答案(面试背诵版)
分布式·面试·kafka
一路向北finish2 小时前
《Hadoop 高可用集群与 OpenStack Mitaka 控制节点部署全流程实操排坑指南》
大数据·linux·运维·服务器·hadoop·openstack
段一凡-华北理工大学2 小时前
高炉炉况智能诊断与预警实战~系列文章14:机器学习炉况分类:样本构建、类别不平衡与模型选型
大数据·人工智能·机器学习·分类·高炉智能化·炉况诊断·炉况分类
代码里的AI星2 小时前
B2B企业AI搜索可见度诊断与GEO技术优化实践:从0%到67%的架构重构之路
大数据·人工智能