大数据智能运维实战(六):任务一直在等——排查调度队列与执行资源

系列:Spring AI + MCP------大数据智能运维实战

本篇目标:结合运行阶段、调度原因、执行节点槽位和计算队列记录,逐步缩小"任务一直在等"的范围。

阅读基础:沿用 lab-03-06 工程;Java 17、Spring Boot 4.1.0、Spring AI 2.0.1。

案例说明:平台、调度规则、队列、运行标识及数据均为虚构教学设定,不对应真实平台实现。

1. 小周说"还在等",小林先问"在哪一层等"

上一篇,小周已经知道,不能只看一个 CPU 当前值就判断任务为什么慢。

但新问题马上出现了。

"这个任务一直在等。是机器不够吗?"

小林打开调度记录:"先别急着加机器。这里的'等',可能是调度规则还没放行,也可能是没分到合适的执行节点,还可能已经提交给计算引擎,却在引擎里面排队。"

这几种情况都可能让用户觉得"日报迟迟不出结果",处理方向却不同。

本篇继续用青禾平台和"门店经营日报",但新增几次独立的教学运行。它们用来区分排查分支,不是第一篇那次任务后来查出的真实根因。

观察时刻仍固定在 2026-09-16 09:10 +08:00。

2. 先画清楚任务可能经过哪些环节

可以先把一次运行理解为:

text 复制代码
进入调度等待
    ↓
调度条件允许,并获得合适的执行位置
    ↓
执行器开始处理任务
    ↓
必要时提交到计算引擎
    ↓
计算引擎准入、分配资源并执行

有的任务直接在执行器内完成,不会进入独立计算引擎。这里讨论的是需要继续提交计算任务的教学场景。

本篇重点区分三种排查方向:

方向 常见表现 应该优先查什么
调度规则等待 仍未启动,依赖或并发条件未满足 本次调度决策、依赖状态、并发限制
合适的执行资源不足 仍未启动,找不到符合条件的空闲槽位 标签、执行池、槽位、节点上报时间
计算队列等待 执行器已经启动,引擎应用仍在队列中 应用标识、队列状态、准入与资源分配记录

前两项都可能发生在调度侧,不一定是两个先后独立的队列。不同平台的调度模型会有区别,不能仅凭一个通用流程图套用内部状态机。

图 1:先定位等待发生在哪一层,再解释这一层的限制。

还有一个关键提醒:调度平台里的 RUNNING,可能只表示执行器已开始处理。执行器可能还在准备参数、上传文件、等待提交结果,或者等待引擎完成。

调度侧 RUNNING,不能自动翻译成"计算引擎正在计算"。

3. 先查准确的一次运行,不要只拿任务名拼关联

小周输入"门店经营日报"。

小林先确认了三项身份:

  • taskId=daily-report:是哪项任务。
  • runId:是哪一次运行。
  • attemptId=attempt-1:这次运行里的哪次执行尝试。

同一项任务可能每天都运行,也可能在同一天重跑。只有任务名,无法保证查到的队列记录和日志属于同一次执行。

本篇所有样本只有一个执行尝试。为了让实验可以直接复现,提供以下精确标识:

运行标识 场景
run-policy-20260916 任务并发限制
run-slots-20260916 匹配节点没有空闲槽位
run-engine-20260916 已提交,计算队列等待
run-unknown-20260916 执行器已启动,但缺少引擎关联标识
run-engine-stale-20260916 引擎记录已经过期

读者在实验里可以直接复制这些标识。真实助手应先通过运行列表找到候选,再使用返回的准确标识查询,不能按名称猜一个 runId。

引擎标识也一样。本例 demo-app-42 来自该次运行的教学提交记录,并不是把任务名换个前缀拼出来的。

4. CPU 明明很低,为什么还是分不到执行节点

先看 run-slots-20260916。

这次运行 08:55 进入等待,09:10 还没有执行器启动记录。09:09 的调度记录给出原因:NO_MATCHING_SLOT。

本次任务要求执行节点带有 batch 标签。09:09 的节点快照是:

节点 标签 CPU 已用槽位 / 总槽位 能否提供匹配的空闲槽位
A batch 34% 2/2 标签匹配,但槽位已满
B realtime 22% 0/2 槽位空闲,但标签不匹配
C batch 17% 2/2 标签匹配,但槽位已满

这里的槽位是教学调度器设置的并发执行名额,不等于 CPU 核数。

一个任务占着槽位时,可能在等数据或远程响应,CPU 不一定高。因此"槽位占满"和"CPU 占满"是两个不同事实。

图 2:B 有两个空闲槽位,但这次运行要求 batch 标签,不能据此认为它能被分配过去。

全池空闲槽位是 2 个,符合本次标签条件的空闲槽位却是 0 个。

这个结果可以解释:按 09:09 的调度记录与槽位快照,这次运行尚未拿到匹配的执行槽位。

但我们只有 09:09 的这组证据,不能因此声称从 08:55 到 09:10 的每一分钟都满足同样条件。要解释完整等待过程,还需要等待期间的调度决策和槽位释放事件。

节点 C 在这个独立样本中的上报是 09:09,和前文 pressure 的 08:20 旧记录不同。场景不同,不能混用两组时间。

5. 有空闲资源,也可能被调度规则拦住

接着看 run-policy-20260916。

它同样从 08:55 开始等待,但调度记录中的原因是:

text 复制代码
schedulerReason = MAX_CONCURRENT_RUNS
activeRunsForTask = 1
maxConcurrentRuns = 1

这组教学规则规定:同一任务同时最多运行一个实例,09:09 已经有一个运行中的实例。

小周问:"那多加一台机器有用吗?"

单从这条规则看,增加机器不会自动解除同一任务的并发上限。应该先核对正在运行的实例、它是否正常推进,以及为什么要设置这个限制。

这不等于建议立即修改限制。并发上限可能用于避免重复处理或资源竞争,是否调整需要结合业务约束。

在助手报告里,应把这组证据描述为"调度器报告并发上限已达到",不要换成"集群资源不足"。后者把一个明确的调度条件,改写成了另一种没有证据的解释。

6. 调度侧已经 RUNNING,计算引擎仍可能在排队

第三次查询是 run-engine-20260916。

这组数据给出了一条更细的时间线:

时刻 记录
08:55 进入调度等待
09:00 执行器开始处理,调度状态变为 RUNNING
09:01 提交到计算引擎,记录到 demo-app-42
09:09 引擎上报该应用为 QUEUED
09:10 本次实验观察时刻

09:09 的引擎队列记录同时给出:

text 复制代码
queueName = demo-batch
queueRunning = 8
queueLimit = 8
engineReason = QUEUE_RUNNING_LIMIT

在这个简化的教学规则里,该队列同时最多准入 8 个运行中的应用,目前已经达到上限。原始原因也明确指向队列运行数量限制。

这组证据支持"应继续检查计算队列的准入条件"。它不能推出所有节点 CPU 都高,也不能推出整个集群都没有可用内存。

小周又算了一遍耗时:"调度等了 5 分钟,启动后 10 分钟,提交后 9 分钟,总共 24 分钟?"

小林摇头:"后面的 9 分钟,已经包含在启动后的 10 分钟里。"

图 3:提交后经过时间是启动后经过时间的一部分,不能重复相加。

正确的口径是:

text 复制代码
总经过时间:09:10 - 08:55 = 15 分钟
调度等待:09:00 - 08:55 = 5 分钟
执行器启动后经过:09:10 - 09:00 = 10 分钟
提交后经过:09:10 - 09:01 = 9 分钟

因此,总经过时间是 5 + 10 = 15 分钟。

这里特意使用"提交后经过",没有把 9 分钟命名为精确的"引擎排队时长"。最后一次引擎观测是 09:09,只有状态快照,没有完整的阶段事件序列;09:09 到观察时刻之间是否变化,也没有新的观测。

更不能把这 9 分钟写成实际计算时间。

7. 工具返回证据和排查方向,不替我们修改调度

本篇新增 ops_inspect_wait。

小林没有把所有输入设计成自由组合。调用者只提供一个精确 runId,工具读取这次运行相关的固定教学记录。

返回结构保留原始 run、阶段 stage、原因 reason、几个口径明确的耗时,以及 nextChecks。

下面是服务里的返回对象定义。可能不存在的启动时间、引擎关联和队列字段都明确允许 null:

java 复制代码
    public record Executor(String nodeId, String tag, int totalSlots, int usedSlots,
                           double cpuPercent, OffsetDateTime reportedAt) {}
    public record Run(String runId, String taskId, String attemptId, String requiredExecutorTag,
                      String schedulerState,
                      OffsetDateTime queuedAt, @Schema(nullable=true) OffsetDateTime executorStartedAt,
                      @Schema(nullable=true) String schedulerReason, OffsetDateTime schedulerReportedAt,
                      @Schema(nullable=true) Integer activeRunsForTask,
                      @Schema(nullable=true) Integer maxConcurrentRuns,
                      @Schema(nullable=true) String engineId, @Schema(nullable=true) OffsetDateTime submittedAt, @Schema(nullable=true) String engineState,
                      @Schema(nullable=true) OffsetDateTime engineReportedAt, @Schema(nullable=true) String engineReason,
                      @Schema(nullable=true) String queueName,
                      @Schema(nullable=true) Integer queueRunning, @Schema(nullable=true) Integer queueLimit, List<Executor> executors) {}
    public record WaitResult(OffsetDateTime observedAt, Run run, String stage,
                             long totalElapsedMinutes, long schedulerWaitMinutes,
                             @Schema(nullable=true) Long afterExecutorStartMinutes, @Schema(nullable=true) Long afterSubmitMinutes,
                             String reason, List<String> nextChecks) {}

判断过程的核心方法如下,完整的固定样本与辅助方法在配套工程的 WaitService.java 中:

java 复制代码
    public WaitResult inspect(String runId) {
        Run run = fixture(runId);
        OffsetDateTime started = run.executorStartedAt();
        long schedulerWait = minutes(run.queuedAt(), started == null ? DemoData.NOW : started);
        Long afterStart = started == null ? null : minutes(started, DemoData.NOW);
        Long afterSubmit = run.submittedAt() == null ? null : minutes(run.submittedAt(), DemoData.NOW);
        String stage;
        String reason;
        List<String> next;
        if (started == null && "FRESH".equals(DemoData.freshness(run.schedulerReportedAt()))) {
            stage = "SCHEDULER_WAIT";
            reason = run.schedulerReason();
            next = reason.equals("NO_MATCHING_SLOT")
                    ? List.of("核对 batch 标签和可用槽位", "查看等待期间槽位释放事件")
                    : List.of("核对任务并发上限与正在运行的实例", "查看调度决策事件");
        } else if (run.engineId() == null) {
            stage = "UNKNOWN";
            reason = "ENGINE_MAPPING_MISSING";
            next = List.of("按 runId + attemptId 查提交日志", "确认引擎应用标识与提交结果");
        } else if (!"FRESH".equals(DemoData.freshness(run.engineReportedAt()))) {
            stage = "UNKNOWN";
            reason = "ENGINE_REPORT_STALE";
            next = List.of("补查该 engineId 的最新状态", "保留旧记录时间,不宣称当前仍在排队");
        } else if ("QUEUED".equals(run.engineState())) {
            stage = "ENGINE_QUEUE";
            reason = run.engineReason();
            next = List.of("查看该计算队列的准入与资源分配记录", "核对请求资源与队列限制");
        } else {
            stage = "UNKNOWN";
            reason = "UNSUPPORTED_ENGINE_STATE";
            next = List.of("补充引擎执行阶段记录");
        }
        return new WaitResult(DemoData.NOW, run, stage, minutes(run.queuedAt(), DemoData.NOW),
                schedulerWait, afterStart, afterSubmit, reason, next);
    }

这里没有用 CPU 大小决定等待阶段,而是先看执行器是否启动、是否有引擎关联、引擎记录是否足够新,再读取该层报告的原因。

nextChecks 只是下一步只读排查建议。工具不会取消任务、调整并发、切换队列或重启组件。

固定样本已经保证关联和时间顺序合法。真实接入时,数据适配层还要校验运行身份、时区和时间先后关系;如果出现"先启动、后入队"之类冲突,应该保留数据异常,而不是继续算出负耗时。

注册入口仍然很薄:

java 复制代码
package example.qinghe;

import org.springframework.ai.mcp.annotation.McpTool;
import org.springframework.ai.mcp.annotation.McpToolParam;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

/** 第 06 篇工具入口;业务校验与计算由服务完成。 */
@Component
@Profile({"ch06"})
public class WaitTools {
    private final WaitService service;
    public WaitTools(WaitService service) { this.service = service; }

    @McpTool(name="ops_inspect_wait", description="按准确运行标识查询教学等待证据,区分调度侧等待和引擎队列;不重试、不取消、不修改资源。",
            generateOutputSchema=true,
            annotations=@McpTool.McpAnnotations(readOnlyHint=true, destructiveHint=false))
    public WaitService.WaitResult inspect(
            @McpToolParam(description="精确运行标识:run-slots-20260916、run-policy-20260916、run-engine-20260916、run-unknown-20260916 或 run-engine-stale-20260916", required=true) String runId) {
        return service.inspect(runId);
    }
}

8. 把五个分支各跑一次

切换前先结束自己启动的旧章节服务。在 lab-03-06 目录执行:

bash 复制代码
mvn test package
java -jar target/qinghe-ops-lab-1.0.0.jar --spring.profiles.active=ch06

新开终端,先查执行槽位:

bash 复制代码
python3 mcp_call.py ops_inspect_wait \
  '{"runId":"run-slots-20260916"}'

再查计算队列:

bash 复制代码
python3 mcp_call.py ops_inspect_wait \
  '{"runId":"run-engine-20260916"}'

第二次调用的实际结构化结果中,以下是顶层判断与耗时摘录。run 原始记录另行保留,包含前文时间线、引擎应用和队列数字:

json 复制代码
{
  "observedAt": "2026-09-16T09:10:00+08:00",
  "stage": "ENGINE_QUEUE",
  "totalElapsedMinutes": 15,
  "schedulerWaitMinutes": 5,
  "afterExecutorStartMinutes": 10,
  "afterSubmitMinutes": 9,
  "reason": "QUEUE_RUNNING_LIMIT",
  "nextChecks": [
    "查看该计算队列的准入与资源分配记录",
    "核对请求资源与队列限制"
  ]
}

五组样本应分别得到:

运行 阶段 原因
policy SCHEDULER_WAIT MAX_CONCURRENT_RUNS
slots SCHEDULER_WAIT NO_MATCHING_SLOT
engine ENGINE_QUEUE QUEUE_RUNNING_LIMIT
unknown UNKNOWN ENGINE_MAPPING_MISSING
engine-stale UNKNOWN ENGINE_REPORT_STALE

最后两个分支不是为了凑测试数量。

缺少引擎标识时,我们只知道执行器已经开始处理,不能断言计算已经提交成功,更不能随便找一条名称相近的引擎应用来解释。

引擎记录过期时,也不能照搬它最后一次 QUEUED 的状态,宣称当前仍在排队。engine-stale 的记录停在 09:02,距观察时刻 8 分钟,超过本例 5 分钟规则,因此保留 UNKNOWN。

配套测试验证了五个分支、非法运行标识、匹配槽位计算,以及耗时字段的包含关系;MCP 调用验证使用真实教学服务,没有连接任何真实调度平台或计算引擎。

9. 一份等待分析,应该能带着人继续往下查

对于 run-engine-20260916,可以组织成下面的预期回答。它依据教学数据编写,未经过真实模型调用:

本次查询针对 daily-report 的运行 run-engine-20260916、尝试 attempt-1。

该运行 08:55 入队,09:00 启动执行器,调度等待为 5 分钟。09:01 已记录向计算引擎提交,关联应用为 demo-app-42。

引擎最新记录来自 09:09,状态为 QUEUED,报告原因为 QUEUE_RUNNING_LIMIT;对应 demo-batch 队列运行数为 8,上限为 8。按本例时效规则,这条记录可用于当前排查,应优先核对计算队列的准入与资源分配记录。

到观察时刻 09:10,执行器启动后经过 10 分钟,其中提交后经过 9 分钟。提交后经过时间不等于实际计算时间,也不能直接当成精确排队时长。

如果是 slots,报告就换成匹配条件和槽位证据;如果是 policy,就说明任务并发限制;如果缺少关联或记录过期,就明确指出缺什么。

小周终于不再把所有等待都叫成"机器忙"。

"现在至少知道应该去看哪一层了。"

小林说:"这一步就很有价值。先缩小范围,再补证据,后面的查询才不会东一条、西一条。"

到这里,我们的运维工具已经能列出节点、解释组件上报、描述 CPU 趋势,并区分几种等待方向。后面继续分析失败和重试时,同样要守住这条线:围绕准确的一次执行,让每个结论都能回到对应的记录。

相关推荐
Pniubi42 分钟前
HashMap扩容全流程
java·开发语言·数据结构
电梯界知识分子1 小时前
江西赣州赣县三层设计师自建房:1700mm中空极限利用,全观光铝合金电梯的精度与防震处理
大数据·服务器·网络·人工智能·家用电梯
跨境数据猎手2 小时前
2027最新跨境电商独立站模式拆解
大数据·人工智能
wtblszn2 小时前
实验室通风设备远程监控运维管理系统方案
运维
悟天特斯2 小时前
智慧园区的“体验升级“:从管理工具到服务生态的人本运营
大数据·人工智能
考虑考虑9 小时前
JDK26中的Map.ofLazy
java·后端·java ee
鶴哥只手遮天12 小时前
Spring AI 2.0.1 深度解析-第 14 章 MCP 服务端(上):注解式实现与工具来源
java·人工智能·开源
鶴哥只手遮天13 小时前
Spring AI 2.0.1 深度解析-第 15 章 MCP 服务端(下):三种协议形态、Streamable 传输与状态管理
java·人工智能·开源
鶴哥只手遮天13 小时前
Spring AI 2.0.1 深度解析-第 16 章 向量存储(上):`VectorStore` 接口体系、`BatchingStrategy`
java·人工智能·开源