1. 文档目的
本文沉淀 Linkis 0.9.3 对接公司定制版 Spark 2.4.5/YARN 时的生产经验。该发行版包含上游 Spark 2.4.5 未必具备的 AQE 实现,涉及 AdaptiveSparkPlanExec、ShuffleQueryStage 的结论不能直接套用到未经确认的上游版本。本文重点回答:
- 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-services、YarnShuffleService 类和 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 定义这些键,再由 SparkSubmitProcessBuilder 从 request.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。
| 最底层信号 | 主要方向 |
|---|---|
FetchFailed、MetadataFetchFailed |
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.host、spark.driver.bindAddress、spark.driver.port 和 spark.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 全局变更,应先确认实际部署模块、实例数量、活动任务和维护窗口;多实例部署应摘流后逐台滚动,不能直接同时重启。标准步骤:
- 查询并保存修改前的数据库值;
- 在事务中更新用户配置并回查;
- 修改并编译实际部署的 Spark/xsql EngineManager,记录构建版本与校验和;
- 备份线上 JAR 和配置文件,确认目标路径、属主与权限;
- 摘流一个 EngineManager 实例,确认无正在初始化的 Engine 后替换并重启;
- 对单实例执行冒烟验证后再滚动其他实例;
- 停止测试用户旧 Spark Engine;
- 重新提交任务,创建新 Engine;
- 从启动日志核对最终
spark-submit命令; - 先运行小任务,再运行代表性大任务;
- 观察 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. 经验结论
ShuffleQueryStage是失败位置,不是根因;必须读取最底层异常。spark.executor.instances在动态分配开启时主要影响初始数量,不能限制最大 Executor 数。- 动态分配必须有与 Driver、队列容量匹配的上限,1000 不是安全的通用默认值。
- 增加内存不能修复 RPC 连接超时、Shuffle Service 注册失败或坏节点。
- client 模式下 Driver 网络可达性属于生产关键依赖,应纳入发布前检查。
- 数据库用户值、代码默认值和
spark-submit --conf必须统一核验,以最终启动命令为准。 - 所有配置变更必须通过新 Engine 验证,不能用存活的旧 Engine 判断是否生效。