写在前面:本文基于真实生产问题整理,前半部分讲清调度链路原理与通用排查思路,后半部分复盘一个容易被忽略的根因——数据迁移时跳过了部分字段,导致任务记录"缺胳膊少腿",被调度器误判为常驻任务,一直占用资源,最终形成僵尸进程。如果你也遇到过"单个任务永远 pending、资源明明空闲却没被拉起"的情况,这篇值得收藏。


一、先纠正一个高频误区

在自动化测试平台、自研任务调度平台里,调用提交接口后页面显示【排队中】、status 为 pending、message 为空、没有任何报错——很多同学第一反应是"接口是不是调失败了?脚本报错了?",然后跑去翻执行日志,结果什么都查不到。

先把结论说清楚:

pending 不是任务执行失败,也不是接口请求失败。 接口已经成功把任务写入数据库,只是这个异步任务还没被 Agent 领取、还没启动运行。这个阶段本来就不会产生脚本执行日志。

先看看一条典型的 pending 记录长什么样:

{
  "id": 521,
  "status": "pending",
  "trigger": "scheduler",
  "acting_user_name": "scheduler",
  "message": "",
  "created": 0,
  "created_at": "2026-11-02T21:03:36+08:00",
  "deleted": 0,
  "finished_at": null,
  "progress_json": null,
  "skipped": 0,
  "started_at": null,
  "updated": 0
}

字段解读:

字段含义当前值说明
created_at任务创建时间已生成记录
started_at任务启动时间null,从未启动
finished_at任务结束时间null,从未结束
progress_json执行进度null,还没进入执行阶段
message报错信息空,不代表正常,只是还没到执行阶段
trigger / acting_user_name触发来源scheduler,调度器自动触发,非人工点击

注意:单次查询看到 pending,只能说明"此刻在排队"。只有间隔一段时间再次查询,任务依旧 pending、started_at 始终为 null,才能判定为卡死。


二、搞懂调度链路:几个核心名词

用简单语言解释,看懂流程的基础。

1. Scheduler(调度器)
整个系统的"派单中心"。负责接收提交请求、生成任务记录、写入任务队列,按规则匹配可用执行资源,把任务下发出去。记录里 trigger: "scheduler" 就表示这是调度服务自动触发生成的任务。

2. Worker(执行节点)
一台台跑任务的机器(虚拟机/容器),是承载任务的硬件资源,相当于"工位"。

3. Agent(执行代理)
部署在 Worker 上的常驻程序,相当于"工位上的工人"。它持续和调度服务通信,主动认领分配给自己的任务、拉起脚本、收集日志、回传结果与进度。

4. 任务状态机

  • pending:已创建入库,进入待执行队列,未被 Agent 领取 —— 相当于"取了号,还没叫到你";
  • running:Agent 领取任务,脚本开始执行;
  • success / failed:执行完毕的最终结果。只有 failed 才代表脚本运行异常。

三、调度器完整工作流程

Worker 执行节点Agent 执行代理任务队列Scheduler 调度器用户/定时逻辑Worker 执行节点Agent 执行代理任务队列Scheduler 调度器用户/定时逻辑任务停留在 pending 排队等待资源释放或规则满足alt[匹配到可用 Agent][无可用 Agent / 规则不匹配 / 限流]调用提交接口,发起任务1数据库新增任务记录,status=pending2任务入队3按标签/资源规则匹配空闲 Worker/Agent4下发任务指令5更新 status=running,填充 started_at6拉起自动化脚本执行7脚本执行结果8回传结果,更新 status=success/failed,填充 finished_at9前端展示最终状态10

关键结论:只要任务处于 pending,started_at、progress_json 一般为 null,此时不会产生脚本执行日志,去查执行日志必然是空的。


四、任务长期 Pending 的 4 大类常见根因

根因 1:Worker/Agent 资源耗尽,没有空闲执行节点

大量任务同时占满所有 Worker,新任务只能在队列排队。特征:同一时间段大量任务一起 pending,而非单条卡住。

根因 2:调度器下发正常,但下游消费链路异常

Scheduler 成功生成任务并入队,但 Agent 与调度服务之间网络断开、消费进程卡死,没有 Agent 来领取。特征:有空闲 Worker、Agent 进程看似在线,但长时间不消费队列任务。

根因 3:系统并发上限限流保护

平台配置了全局/单分组最大并发数,运行任务数达到阈值后,后续任务直接进入 pending,等名额释放。特征:并发指标达到配置上限,前面任务结束后 pending 任务会被依次拉起。

根因 4:调度匹配规则不匹配,找不到符合要求的 Agent

任务携带标签、环境、时间窗口等调度条件,若集群里所有在线 Agent 都不满足标签要求(大小写、字符不一致也会匹配失败),或定时任务还没到允许执行时间,任务就一直 pending。特征:普通任务正常跑,只有带特定标签的任务卡住。

根因 5(本文重点):数据迁移遗留脏数据,字段缺失导致"僵尸任务"

这是我在真实项目中踩到的坑,常规四类根因都排查完仍无解时,一定要往数据本身上想。详见下一节。


五、实战复盘:数据迁移跳字段,任务变"僵尸进程"

5.1 现象

某次平台升级,做了一次数据库迁移。上线后陆续出现一些诡异现象:

  • 个别任务提交后永远 pending,started_at 始终为 null;
  • 资源监控显示 Worker/Agent 明明有空闲,队列里也有任务,但没有 Agent 认领;
  • 这些卡住的任务不报错、不超时释放,像是被系统"遗忘"了;
  • 随着时间推移,卡住的任务越积越多,可用执行资源被持续占用,正常任务的排队时间也被拉长。

这里要特别注意一个反直觉点:我最初以为"资源被占满所以任务排队",但监控显示资源是空闲的。真正被占用的不是执行资源,而是任务记录本身失去了正常的生命周期管理——它们变成了"僵尸"。

5.2 定位过程

按标准排查顺序走了一遍:

  1. 看批量现象:不是大批量一起 pending,而是零星几条,排除资源耗尽与限流;
  2. 核对标签:卡住的任务标签与在线 Agent 标签一致,排除匹配规则问题;
  3. 查 Agent 进程与心跳:Agent 在线、心跳正常、网络通畅,排除消费链路异常;
  4. 查调度/消费日志:调度器对这些任务的匹配日志显示"跳过",没有报错堆栈,线索中断。

转折点在于:把卡住的任务记录和一条正常任务记录逐字段对比,发现卡住记录的某些字段是"空/缺省"的——而这些字段恰好是调度器匹配资源时依赖的字段。

进一步排查迁移脚本,找到根因:数据迁移时字段映射漏配,跳过了某几个关键字段,迁移后记录里这些字段为 null/默认值。调度器匹配逻辑遇到这类"残缺"记录时:

  • 无法正确解析调度条件,匹配过程被跳过;
  • 由于记录已入库、状态是 pending,调度器又认为它"还在队列里",不会主动清理或重新入队;
  • 这条记录就长期挂在队列中,持续占用调度资源与队列位,形成典型的僵尸任务/僵尸进程。

5.3 修复方案

批量

单条

是

否

发现个别任务长期 pending

批量还是单条?

查资源/限流/消费链路

逐字段对比正常任务与异常任务

发现字段缺失/null?

排查数据迁移脚本字段映射

继续查标签/匹配规则

确认根因: 迁移跳字段

修复方案

数据修复: 补齐缺失字段

清理僵尸任务: 取消并重新提交

迁移脚本增加字段完整性校验

调度器增加僵尸任务超时回收机制

具体动作:

  1. 存量数据修复:编写修复脚本,为缺失字段的记录补齐正确值(或按规则回填默认值),让调度器能重新识别;
  2. 清理僵尸任务:对无法自动恢复的记录,人工取消后重新提交;
  3. 迁移脚本加固:增加字段完整性校验,迁移前后做记录数、关键字段非空率、枚举值分布的对账;
  4. 调度器兜底:为 pending 任务增加超时回收/告警机制,超过阈值仍未启动的任务自动告警并标记,避免"静默僵尸"长期占用资源。

六、标准化排查步骤(可落地实操顺序)

1 看批量现象

2 核对任务与执行机标签

3 查并发配置与运行任务数

4 查Worker机器与Agent进程

5 持续观测任务状态快照

6 查调度/消费服务日志

7 逐字段对比正常与异常记录

8 怀疑数据本身: 迁移/脏数据/字段缺失

  1. 先看批量现象:大批量一起 pending → 优先查资源耗尽、消费异常、限流;单条卡住 → 优先查标签与调度规则,再看数据完整性;
  2. 核对任务标签与执行机标签:确认标签、环境分组存在对应在线 Agent,注意大小写与字符一致性;
  3. 查看并发配置与当前 running 任务数:是否达到上限,达到上限属于正常限流,等待即可;
  4. 检查 Worker 与 Agent 状态:进程是否存活、CPU/内存负载、与调度服务网络心跳;
  5. 持续观测任务状态快照:隔几分钟重查任务 ID,started_at 有值说明只是排队久;长时间无变化则判定卡死;
  6. 查调度与消费服务日志:确认是否有队列阻塞、消息消费异常;
  7. 逐字段对比:把卡住记录与正常记录逐字段 diff,重点看 null、空串、默认值字段;
  8. 怀疑数据本身:结合近期是否有数据迁移、批量导入、脚本升级,排查脏数据/字段缺失。

七、避坑小结

  1. Pending ≠ 报错失败,它只是任务生命周期的【待分配阶段】,此阶段没有执行日志,别一上来就翻脚本日志;
  2. 接口提交成功 ≠ 任务马上执行,异步任务天生有排队机制,正常排队与异常卡死要区分:前者资源释放后自动拉起,后者需人工干预;
  3. 资源空闲却长期 pending,要往数据本身上想:字段缺失、脏数据、迁移遗漏都会让任务"看起来在队列里、实际无人认领";
  4. 僵尸任务的危害是静默的:它不报错、不超时,却持续占用队列位与调度资源,拖慢整体吞吐,必须靠监控+超时回收兜底;
  5. 数据迁移一定要做字段对账:迁移前后记录数、关键字段非空率、枚举值分布三重校验,比事后救火成本低得多。

八、写在最后

异步调度是自动化测试平台的核心能力,pending 是日常高频问题。理解 Scheduler、Worker、Agent 的分工,理清链路,再遇到排队中的任务就不会一头雾水。

而本文真正的价值在于:当标准排查路径全部走通却仍无解时,要把视角从"调度逻辑"拉回到"数据本身"。一次不起眼的迁移字段遗漏,就足以让任务变成长期占用资源的僵尸进程。排查问题时,逐字段对比永远是最朴素也最有效的手段。

Logo

更多推荐