任务一直显示 Pending?一次数据迁移遗留脏数据导致的“僵尸任务“排查实录
写在前面:本文基于真实生产问题整理,前半部分讲清调度链路原理与通用排查思路,后半部分复盘一个容易被忽略的根因——数据迁移时跳过了部分字段,导致任务记录"缺胳膊少腿",被调度器误判为常驻任务,一直占用资源,最终形成僵尸进程。如果你也遇到过"单个任务永远 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才代表脚本运行异常。
三、调度器完整工作流程
关键结论:只要任务处于 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 定位过程
按标准排查顺序走了一遍:
- 看批量现象:不是大批量一起 pending,而是零星几条,排除资源耗尽与限流;
- 核对标签:卡住的任务标签与在线 Agent 标签一致,排除匹配规则问题;
- 查 Agent 进程与心跳:Agent 在线、心跳正常、网络通畅,排除消费链路异常;
- 查调度/消费日志:调度器对这些任务的匹配日志显示"跳过",没有报错堆栈,线索中断。
转折点在于:把卡住的任务记录和一条正常任务记录逐字段对比,发现卡住记录的某些字段是"空/缺省"的——而这些字段恰好是调度器匹配资源时依赖的字段。
进一步排查迁移脚本,找到根因:数据迁移时字段映射漏配,跳过了某几个关键字段,迁移后记录里这些字段为 null/默认值。调度器匹配逻辑遇到这类"残缺"记录时:
- 无法正确解析调度条件,匹配过程被跳过;
- 由于记录已入库、状态是 pending,调度器又认为它"还在队列里",不会主动清理或重新入队;
- 这条记录就长期挂在队列中,持续占用调度资源与队列位,形成典型的僵尸任务/僵尸进程。
5.3 修复方案
具体动作:
- 存量数据修复:编写修复脚本,为缺失字段的记录补齐正确值(或按规则回填默认值),让调度器能重新识别;
- 清理僵尸任务:对无法自动恢复的记录,人工取消后重新提交;
- 迁移脚本加固:增加字段完整性校验,迁移前后做记录数、关键字段非空率、枚举值分布的对账;
- 调度器兜底:为 pending 任务增加超时回收/告警机制,超过阈值仍未启动的任务自动告警并标记,避免"静默僵尸"长期占用资源。
六、标准化排查步骤(可落地实操顺序)
- 先看批量现象:大批量一起 pending → 优先查资源耗尽、消费异常、限流;单条卡住 → 优先查标签与调度规则,再看数据完整性;
- 核对任务标签与执行机标签:确认标签、环境分组存在对应在线 Agent,注意大小写与字符一致性;
- 查看并发配置与当前 running 任务数:是否达到上限,达到上限属于正常限流,等待即可;
- 检查 Worker 与 Agent 状态:进程是否存活、CPU/内存负载、与调度服务网络心跳;
- 持续观测任务状态快照:隔几分钟重查任务 ID,
started_at有值说明只是排队久;长时间无变化则判定卡死; - 查调度与消费服务日志:确认是否有队列阻塞、消息消费异常;
- 逐字段对比:把卡住记录与正常记录逐字段 diff,重点看
null、空串、默认值字段; - 怀疑数据本身:结合近期是否有数据迁移、批量导入、脚本升级,排查脏数据/字段缺失。
七、避坑小结
- Pending ≠ 报错失败,它只是任务生命周期的【待分配阶段】,此阶段没有执行日志,别一上来就翻脚本日志;
- 接口提交成功 ≠ 任务马上执行,异步任务天生有排队机制,正常排队与异常卡死要区分:前者资源释放后自动拉起,后者需人工干预;
- 资源空闲却长期 pending,要往数据本身上想:字段缺失、脏数据、迁移遗漏都会让任务"看起来在队列里、实际无人认领";
- 僵尸任务的危害是静默的:它不报错、不超时,却持续占用队列位与调度资源,拖慢整体吞吐,必须靠监控+超时回收兜底;
- 数据迁移一定要做字段对账:迁移前后记录数、关键字段非空率、枚举值分布三重校验,比事后救火成本低得多。
八、写在最后
异步调度是自动化测试平台的核心能力,pending 是日常高频问题。理解 Scheduler、Worker、Agent 的分工,理清链路,再遇到排队中的任务就不会一头雾水。
而本文真正的价值在于:当标准排查路径全部走通却仍无解时,要把视角从"调度逻辑"拉回到"数据本身"。一次不起眼的迁移字段遗漏,就足以让任务变成长期占用资源的僵尸进程。排查问题时,逐字段对比永远是最朴素也最有效的手段。
更多推荐
所有评论(0)