一、常驻 Spark 的价值与隐患
1. 常驻 Spark 解决了什么问题
在交互式查询场景中,如果每条 SQL 都重新启动 Spark Application,资源申请、Driver 启动和 executor 分配会带来明显延迟。
Linkis 的常驻 Spark Engine 通过复用 Driver、SparkContext 和 YARN Application,减少重复启动成本,改善连续查询的交互体验。
这种设计本身没有问题,但它改变了故障的传播方式。
一次性 Spark Application 在任务结束后会被销毁,运行期间积累的异常状态也会随之清除。常驻 Engine 则会跨任务保留 SparkContext、RPC 连接、调度器、executor 状态和黑名单。
因此,常驻模式复用的不只是计算能力,也可能是一个已经受损的运行环境。
2. 真正的风险是假活 Engine
常驻模式下,判断 Engine 是否可用,不能只看 JVM 进程是否存活。
JVM 存活只能说明进程没有退出,不能证明 SparkContext、SchedulerBackend、Driver RPC 和 YARN 连接仍然正常。
如果 Linkis 只检查进程状态,就可能把一个已经失去执行能力的 Engine 重新放回资源池,并继续分配给后续任务。
这类 Engine 表面上仍然在线,实际上已经无法稳定申请 executor 或调度 task,可以称为"假活 Engine"。
常驻 Spark 的核心风险由此产生:
复用健康 Engine 可以提升效率,复用假活 Engine 则会把一次运行时故障传播给多条任务。
二、一次典型故障是如何发生的
3. 任务不是提交失败,而是运行阶段失败
从日志状态看,SQL 已经通过检查,并依次进入 Accepted、Scheduled 和 Running 状态。
这说明任务已经成功提交到 Linkis,也已经进入 Spark 执行阶段。因此,本次问题不是 Spark Application 创建失败,也不是 SQL 语法错误。
日志中确实出现了返回行数超过平台限制的告警。Linkis 随后切换为整段 SQL 执行,任务没有在这里终止。
所以,返回行数设置需要修改,却不是最终失败原因。真正的问题出现在任务运行期间。
4. 节点异常逐步演变为 executor 批量失效
任务执行一段时间后,部分 YARN 节点开始出现 container 异常,相关线程池也已经终止。
Spark Driver 随即移除异常节点上的 executor,并尝试申请替代资源。在正常情况下,这正是 Spark 的容错机制。
单个 executor 或单个 NodeManager 失效,通常不会直接导致任务失败。只要其他节点可用,新 executor 能正常注册,Spark 就可以重新执行丢失的 task。
但本次故障没有停留在单节点范围。新启动的 executor 随后无法连接常驻 Driver,并持续出现连接超时。
系统由此进入循环:
text
已有 executor 失效
↓
Spark 申请替代 executor
↓
新 executor 无法连接 Driver
↓
executor 启动失败
↓
Spark 再次申请替代资源
Spark 虽然还能获得 container,却无法将这些 container 转化为真正可工作的 executor,动态资源分配机制因此失去自愈能力。
5. 集群管理器重注册进一步放大故障
随后,日志开始批量出现:
text
Stale executor after cluster manager re-registered
这说明 Spark 与集群管理器之间发生了重新注册。背后可能是 ResourceManager 切换、控制面重启,或者 Driver、ApplicationMaster 与集群管理器之间发生了网络中断。
重新注册后,Spark 将之前的 executor 判定为过期。此时系统同时面对两类问题:
text
旧 executor
→ 因集群管理器重新注册而失效
新 executor
→ 因无法连接 Driver 而启动失败
旧资源无法继续使用,新资源又无法补充,Spark 的执行能力随之快速坍塌。
6. 黑名单最终终止任务,但不是最初根因
executor 和节点连续失败后,Spark 会将不稳定的执行位置加入黑名单,避免 task 反复被调度到已经出现问题的位置。
最终,某个计算分区找不到任何可用节点或 executor,日志出现:
text
cannot run anywhere due to node and executor blacklist
异常随后逐层向上传递:
text
没有可用执行位置
↓
Stage 失败
↓
Shuffle Query Stage 无法物化
↓
自适应执行失败
↓
结果无法转换
↓
Linkis 将任务标记为失败
黑名单只是任务停止的直接原因,不是最初的故障来源。
真正的故障链是节点异常、executor 批量丢失、新 executor 无法连接 Driver,以及集群管理器重新注册。
因此,关闭黑名单无法解决问题,只会让任务继续在不可用节点之间反复重试。
三、为什么故障可能影响后续任务
7. 任务结束不等于 Engine 恢复
常驻 Engine 会跨任务复用 SparkContext,以及与它关联的 SchedulerBackend、RPC 连接、executor 管理器和黑名单状态。
如果前一个任务结束时,Engine 已经出现以下问题:
- Driver 与 YARN 控制面连接异常;
- 大量 executor 已经丢失;
- 新 executor 无法注册;
- 集群管理器发生重新注册;
- 黑名单覆盖大量执行位置;
- Driver 线程池或事件队列异常;
那么这些状态不会因为 SQL 执行结束而自动清除。
如果 Linkis 发现 JVM 仍然存活,便直接将 Engine 放回可用池,下一条 SQL 就会进入一个已经失去调度能力的 SparkContext。
故障传播过程如下:
text
前一个任务遭遇基础设施异常
↓
SparkContext 运行状态受损
↓
任务失败,但 Engine 进程仍然存活
↓
Linkis 继续复用该 Engine
↓
新任务继承异常的调度和连接状态
↓
后续任务再次失败
所以,一个任务运行结束,并不能作为 Engine 健康的证明。
8. 单节点故障与 Engine 级故障必须区分
"一个节点坏了,后续任务是否一定失败"不能简单回答是或否。
如果只是单个 executor OOM、进程退出或单个 NodeManager 短暂故障,Spark 通常可以移除失败资源,在其他节点重新申请 executor,并重新执行丢失的 task。
只要 Spark 已经完成恢复,常驻 Engine 仍然可以继续使用,后续任务不一定受到影响。
真正危险的是故障已经影响:
- SparkContext;
- SchedulerBackend;
- Driver RPC;
- executor 注册能力;
- YARN 注册状态;
- 大范围节点可用性。
这时故障不再属于单个节点,而是 Engine 级甚至集群控制面级故障。
本次日志属于后一种情况。继续复用该 Engine,后续任务再次失败的概率很高。
四、如何治理常驻 Engine
9. 建立失败分类和 Engine 健康状态机
常驻 Engine 不能只有"存活"和"死亡"两个状态。更合理的生命周期是:
text
STARTING
→ HEALTHY
→ BUSY
→ IDLE
→ SUSPECT
→ QUARANTINED
→ TERMINATED
任务执行结束后,Engine 不能直接从 BUSY 返回 IDLE,而应先根据异常类型完成健康判断。
如果是语法、权限、字段或返回行数问题,失败只属于当前任务,Engine 可以继续复用。
如果是少量 executor 丢失,但 Spark 已经成功恢复,可以在健康检查通过后继续使用。
如果发生大量 executor 同时丢失、新 executor 无法注册、Driver RPC 不可达或集群管理器重新注册,Engine 应进入 SUSPECT。
确认无法恢复后,Engine 应转为 QUARANTINED,停止接收新任务并主动销毁。
同时,平台还需要区分两个对象:
text
Task
表示用户希望完成一次计算的业务意图。
text
ExecutionAttempt
表示该任务在某个 Engine 上的一次具体执行。
同一个 Task 可以产生多次 Attempt:
text
Task
├── Attempt 1:旧 Engine 执行,因基础设施故障失败
└── Attempt 2:新 Engine 执行
这种拆分可以避免把 SQL 错误与 Engine 故障混为一谈,也为隔离、重建和安全重试提供基础。
10. 建立健康检查、自动重建和主动退休机制
Engine 健康检查不能只确认 JVM 是否存在,还应覆盖五个层面。
进程层面要关注 Driver 是否频繁 Full GC,以及内存和线程池是否接近极限。
SparkContext 层面要确认上下文仍然活跃,SchedulerBackend 可以继续调度,事件队列没有持续积压。
YARN 层面要检查 Application 是否正常运行,ApplicationMaster 是否重新注册,控制面是否发生切换。
executor 层面要关注活跃数量、申请成功率、启动失败率、短时间丢失比例,以及新 executor 是否能够注册到 Driver。
网络层面则要验证 executor 所在网络是否能够真正访问 Driver RPC 服务,而不是只检查 Driver 本地端口是否处于监听状态。
当平台确认故障属于 Engine 级异常后,可以执行一次自动恢复:
text
隔离旧 Engine
→ 创建新 Engine
→ 为原 Task 创建新的 Attempt
→ 重试一次
对于纯查询任务,这种方式通常是安全的。对于写表、覆盖分区或调用外部接口等有副作用的任务,则必须先确认外部执行状态,避免重复提交。
需要注意的是,重建 Engine 只能解决运行时污染,不能替代集群故障修复。如果 YARN 控制面或网络仍然异常,新 Engine 依然会失败。
除此之外,即使没有明显故障,Engine 也不应无限期运行。长期复用可能积累内存碎片、缓存残留、配置污染、RPC 连接老化和黑名单状态。
平台应设置最大存活时间、最大任务数、连续失败次数、executor 累计丢失数和 Driver 资源水位等退休条件。
达到阈值后,Engine 可以完成当前任务,但不再接收新任务,随后平滑退出。
结语
本次任务已经成功提交,并进入 Spark 执行阶段。它最终失败,不是因为 SQL 语法,也不只是因为返回行数设置过大,而是运行期间发生了一条连续的基础设施故障链。
部分节点首先失效,Spark 尝试申请替代 executor;新 executor 随后无法连接 Driver;集群管理器重新注册又使大量旧 executor 变成 stale。随着黑名单扩大,计算分区最终无处调度,整个 Stage 被终止。
这次故障揭示了常驻 Spark 模式最重要的治理要求:
Engine 进程存活不等于 Engine 健康。常驻模式必须配套失败分类、健康状态机、故障隔离、自动重建和主动退休机制。
只要 Linkis 能识别 Engine 级故障,并及时停止复用受损运行时,一次任务失败就不会自然扩散到后续任务。
反之,如果平台只根据 JVM 是否存活决定复用,一个已经失去调度和通信能力的 Engine,就可能连续拖垮多条原本正常的 SQL。