当前位置: 首页 > news >正文

SeaTunnel 的 Zeta 引擎采用集群模式运行作业。

其中,Master 节点负责管理作业级状态;Worker 节点负责执行 Task Group。当用户发起 Cancel 操作时,Master 需要向对应的 Worker 发送取消请求。任务结束或取消完成后,Worker 再将最终状态回传给 Master。

简化后的流程如下:

用户发起 Cancel ↓ Master 向 Worker 发送取消请求 ↓ Worker 取消或完成任务 ↓ Worker 向 Master 汇报最终状态 ↓ Master 更新 Job 状态

乍看之下,这个流程并不复杂。

但在分布式系统中,每一个环节都可能受到节点故障、集群成员变化以及 Master 恢复过程等因素的影响。而这些因素都可能引发竞态条件(Race Condition)。

问题现象

问题的表现很直接:

Job 一直停留在 CANCELING 状态

用户已经执行了取消操作,但任务始终无法进CANCELED等最终状态。

最开始,我把排查重点放在:Master → Worker这条取消请求链路上。

第一个怀疑对象:取消请求链路

在 Master 端,SeaTunnel 会向 Worker 发送CancelTaskOperation

这里有一个关键逻辑:在发送取消请求之前,系统会先检查当前任务所在的执行节点是否仍然存在于集群成员列表中。

核心逻辑如下:

while (!taskFuture.isDone() && nodeEngine .getClusterService() .getMember(executionAddress = getCurrentExecutionAddress()) != null) { try { nodeEngine .getOperationService() .createInvocationBuilder( Constant.SEATUNNEL_SERVICE_NAME, new CancelTaskOperation(taskGroupLocation), executionAddress) .invoke() .get(); return; } catch (Exception e) { Thread.sleep(2000); } }

这一点立刻引起了我的注意。

如果由于心跳异常等原因,Worker 节点暂时从集群视图中消失,那么循环可能会直接退出,甚至连 Cancel 请求都没有发送出去。

因此,我最初的猜测是:

Master 认为取消流程已经处理完毕,但 Worker 实际上从未收到 Cancel 请求。

这个方向确实值得关注。不过后来我发现,仅凭这一点还不足以解释为什么 Job 会一直停留在 CANCELING 状态。

因为即便错过了 Cancel 请求,任务最终仍有可能正常结束,并将最终状态上报给 Master。

于是,我开始把视线转向另一个方向:

当 Worker 向 Master 汇报最终任务状态时,究竟会发生什么?

更关键的链路:Worker → Master 的状态通知

当任务进入最终状态后,Worker 会调用notifyTaskStatusToMaster(),将状态上报给 Master。

该方法的设计思路是:在通知成功之前持续进行重试。

while (isRunning && !notifyStateSuccess) { InvocationFuture<Object> invoke = nodeEngine .getOperationService() .createInvocationBuilder( SeaTunnelServer.SERVICE_NAME, new NotifyTaskStatusOperation( taskGroupLocation, taskExecutionState), nodeEngine.getMasterAddress()) .invoke(); try { invoke.get(); notifyStateSuccess = true; } catch (JobNotFoundException e) { notifyStateSuccess = true; } catch (ExecutionException e) { if (e.getCause() instanceof JobNotFoundException) { notifyStateSuccess = true; } else { Thread.sleep(sleepTime); } } }

乍看之下,这套重试机制本身并没有什么问题。

但其中对JobNotFoundException的处理却至关重要。

如果 Worker 收到JobNotFoundException,它会将notifyStateSuccess设为true,并停止后续重试。

在很多情况下,这样的处理是合理的。因为如果 Master 上已经找不到对应的 Job,往往意味着该 Job 已经结束,并且已经从运行中的 Job 列表中移除。

但在 Master Failover 场景下,这种假设就可能带来问题。

JobNotFoundException 的特殊处理逻辑

在 Master 端,任务状态更新时会先检查runningJobMasterMap

public void updateTaskExecutionState(TaskExecutionState taskExecutionState) { TaskGroupLocation taskGroupLocation = taskExecutionState.getTaskGroupLocation(); JobMaster runningJobMaster = runningJobMasterMap.get(taskGroupLocation.getJobId()); if (runningJobMaster == null) { throw new JobNotFoundException( String.format("Job %s not running", taskGroupLocation.getJobId())); } runningJobMaster.updateTaskExecutionState(taskExecutionState); }

通常情况下,runningJobMaster == null表示该 Job 已经不再运行。

然而,还有另一种可能的时间窗口。

在 Master Failover 期间,新的 Master 可能尚未完全恢复runningJobMasterMap。如果 Worker 恰好在这段时间内上报最终任务状态,新 Master 就可能无法找到对应的JobMaster,并抛出JobNotFoundException

随后,Worker 会将这个异常视为成功处理,并停止重试。

问题发生的过程可能如下:

  1. 一个 Job 正在执行取消操作。
  2. Master 发生切换。
  3. Worker 完成任务,并上报最终任务状态。
  4. 新 Master 尚未完全恢复runningJobMasterMap
  5. Master 抛出JobNotFoundException
  6. Worker 将其视为成功处理,并停止重试。
  7. Master 在恢复完成后,始终没有收到最终任务状态。
  8. Job 一直停留在CANCELING状态。

这正是我发现问题的关键。

问题不仅仅在于 Cancel RPC 可能发送失败,更重要的是,Worker 的最终状态通知有可能在 Master 恢复期间丢失。

为什么我选择新增 Force Stop,而不是直接修改 Cancel

定位到问题后,我考虑过几种不同的解决方案:

  • 调整 JobNotFoundException 的处理逻辑;
  • 等待 runningJobMasterMap 完全恢复后再处理状态通知;
  • 将更多运行时状态持久化到分布式存储;
  • 重构现有的 Cancel 状态机。

但这个问题具有偶发性,而且高度依赖特定的时序条件。

与此同时,正常的 Cancel 流程又是整个执行生命周期中非常敏感的一部分。直接修改这部分逻辑,可能会引入新的行为变化,或者带来额外的性能开销。

因此,我首先选择了一种更务实的方案。

我没有改变现有 Cancel 的语义,而是新增了一套独立的 Force Stop 机制。

思路其实很简单:

如果优雅取消(Graceful Cancellation)无法继续推进,那么运维人员就需要一种明确的手段来最终确定 Job 的状态。

Cancel 与 Force Stop 的区别

让我来解释下 Cancel 与 Force Stop 的区别。

Cancel

Cancel 属于一种优雅终止(Graceful Shutdown)机制。

它会向正在运行的任务发送停止请求,并依赖正常的任务生命周期以及 Worker 的状态通知链路来完成整个过程。

Force Stop

Force Stop 则是一种面向运维恢复的机制。

它不应该依赖远端 Worker 是否仍然能够正常响应。Master 会根据自身的判断直接完成 Job 状态的终结和相关资源的清理。

简单来说:

Cancel = 尝试以优雅的方式停止 Job Force Stop = 当 Cancel 无法继续推进时,直接终结 Job

Force Stop 的目的并不是取代 Cancel。它是一条专门用于处理卡死场景的兜底路径。

我的收获

这次问题让我意识到,一个 Job 状态卡住,并不一定是负责更新该状态的那段代码出了问题。

在这个案例中,一开始最可疑的是 Cancel 请求链路。但最终发现,更关键的问题其实出在 Worker 到 Master 的状态通知链路上。

在正常情况下,JobNotFoundException看起来是一个合理的终止条件;但在 Master Failover 场景下,它也可能意味着:

新的 Master 尚未完成 Job 的恢复。

这两种含义有着本质区别。

而这样一个细微的差别,恰恰决定了 Worker 应该停止重试,还是继续重试。

这也再次提醒我,在分布式系统中,异常处理本身就是状态机的一部分,而不仅仅是错误处理逻辑。

总结

http://www.jsqmd.com/news/1139229/

相关文章:

  • 开头就是暴击:为什么我的 FastAPI 一上生产就跪?
  • 立即预约专家面对面,我们 WAIC 2026 见!
  • 利用Stable Diffusion本地化生成赛博朋克风格图像的完整实践指南
  • BPDA-GMM:面向语义SLAM的概率化数据关联框架
  • 自媒体爆款流量的底层逻辑:不是内容好,是匹配用户需求
  • Notepad--:跨平台文本编辑的国产技术突围与深度定制化实践
  • xParse文档解析Skill:5分钟上手的结构化数据提取方案
  • Ubuntu 离线部署 Nginx
  • 一个简单案例理解OpenClaw的做梦过程
  • 微服务网关选型与落地:Kong、APISIX 与 Traefik 的工程实践对比
  • 无犯罪记录公证书怎么办理?无犯罪记录公证需要带什么材料?
  • 先别急着重启,查查你的数据库连接池“池子”是不是漏了
  • 深度学习框架:PyTorch
  • SSL证书类型太多不会选?这篇帮你理清楚
  • 电商选品分析与商品搬货:应用场景、痛点及完整解决方案
  • 用 NotebookLM 高效阅读文献与撰写综述:从入门到精通的完整指南
  • Windows平台HexStrike AI渗透测试环境搭建:Docker与WSL双环境部署指南
  • 财联万业(杭州)数字科技有限公司能否给代理划定独家经营区域?
  • Android免Root抓包:Fiddler系统证书安装与HTTPS解密实战
  • DDR4 vs DDR5 PCB设计对比:从POD电平到DBI技术的3大布线调整
  • 探索指南:BedrockLauncher - Minecraft基岩版启动器功能亮点解析
  • 警惕 Codex 幻觉:AI 编程的边界实测与应对策略
  • 用Python调用全平台视频元数据解析API,提取抖音快手视频信息(附完整代码)
  • 字符串的三种定义方式、拼接、格式化——我终于搞懂了f“{name}“是什么意思
  • 从触发信号到 A/B 基准,如何把 Skill 做成可回归工程单元
  • 基于多源数据融合与GNN的代码安全漏洞实时检测系统设计与实践
  • c#关于DataGridView和DataTable
  • 【JAVA毕设源码分享】基于springboot药品商城管理系统的设计与实现(程序+文档+代码讲解+一条龙定制)
  • 恐惧魔王AI视频生成模型:暗黑奇幻主题实战指南
  • 【ROS2】 机械臂开发架构:为何“智与看”用 Docker,“真”用传统?