测试平台-05-测试平台的任务调度与执行引擎
测试平台做到一定阶段后,最容易暴露问题的模块通常不是:
- 任务列表页
- 报告展示页
- 配置页面
而是平台最底层、但最容易被写轻的一层:
任务调度与执行引擎。
早期做平台,往往会把执行链路写成下面这种样子:
- 页面点一次执行
- 后端收一个请求
- 直接起进程或者调脚本
- 跑完把结果回写数据库
刚开始任务量少时,这种做法看起来完全能用。
但只要平台开始承载真实使用,问题会很快冒出来:
- 同一时间多个任务一起跑,资源抢占开始失控
- 任务执行到一半挂掉,平台不知道它到底算失败还是还在跑
- 执行机断开、进程退出、日志没回传,状态卡在“运行中”
- 重试做得太粗,错误任务被重复放大
- 同一个环境被多个任务同时占用,结果彼此干扰
所以测试平台的执行能力,本质上不是“把脚本跑起来”这么简单,而是:
把一次任务从“申请执行”推进到“可追踪地完成”,并且在异常情况下也能被正确收敛。
这篇文章就只讲这个问题:
测试平台的任务调度与执行引擎,到底该怎么设计,才不至于后面越跑越乱。
一、调度和执行,最好先分成两层看
很多平台最容易犯的一个错误,就是把调度和执行写成同一个模块。
例如:
- 接口收到了执行请求
- 立刻判断资源
- 直接起任务
- 顺手写状态
- 顺手收日志
看起来链路短,但后面几乎一定会出问题。
更推荐一开始就把它拆成两层:
1. 调度层
调度层回答的是:
- 哪些任务可以进入执行队列
- 哪个任务先跑
- 当前资源是否允许它跑
- 是否需要排队、延迟、限流、重试
它关心的是“谁先执行、在哪里执行、什么时候执行”。
2. 执行层
执行层回答的是:
- 任务在执行机上怎么真正跑起来
- 执行日志、产物、状态怎么回传
- 失败、超时、中断怎么处理
它关心的是“任务怎么跑、怎么收尾、怎么留证据”。
这两层一旦混在一起,后面最容易出现的症状就是:
- 状态机混乱
- 排队规则写在执行器里
- 执行器开始感知页面逻辑
- 重试逻辑到处散落
二、执行链路最先要明确的,不是技术栈,而是状态机
很多平台讨论执行引擎时,最容易先聊:
- 用不用消息队列
- 用不用 Redis
- 用 goroutine 还是 worker pool
这些都重要,但不是第一步。
第一步应该先回答:
一条任务从创建到结束,中间到底会经历哪些状态。
如果状态机没想清楚,后面再好的技术选型都只是把混乱放大。
更常用的一套最小状态流转通常是:
pending:任务已创建,等待调度queued:任务已进入候选队列,等待资源dispatching:调度器已选中,准备分发到执行节点running:执行器已真正开始运行success:执行成功完成failed:执行失败完成timeout:超过最大执行窗口canceled:被主动取消lost:平台和执行现场失联,需要人工或守护程序收敛
这里最容易被漏掉的就是 dispatching 和 lost。
因为真实平台里经常会出现下面这种中间态:
- 平台已经把任务发出去了
- 执行节点还没来得及确认
- 或者节点短暂失联
如果没有中间状态,平台就很容易出现“双发”或者“假卡死”。
三、调度层真正要解决的,不只是排队,而是资源冲突
调度这件事很容易被理解成:
- 先进先出
- 优先级高的先跑
这当然是一部分,但测试平台里的调度难点往往不在“队列算法”本身,而在:
资源约束非常复杂。
测试平台里常见的资源不只是机器 CPU 和内存,还包括:
- 测试环境名额
- 真机设备
- 浏览器节点
- 共享账号
- 测试数据池
- 外部依赖配额
也就是说,一条任务能不能跑,不只是看“有没有空 worker”,还要看:
- 它需要的环境现在是否被占用
- 它需要的设备是否健康
- 同类任务是否超过并发上限
- 是否会挤占高优任务的执行窗口
所以调度层最好至少能回答 4 个问题:
1. 任务需要哪些资源
例如:
- 环境:
test-a - 设备:
android-17 - 执行节点标签:
ui-runner - 并发槽位:
1
2. 这些资源当前是否可用
不仅要看有没有,还要看:
- 是否健康
- 是否被别人租约占用
- 是否满足版本或能力要求
3. 任务是否允许排队等待
有些任务适合排队:
- 夜间回归
- 非紧急冒烟
有些任务不适合排太久:
- 提测验收
- 发布前阻塞校验
4. 资源释放后,谁先补位
如果没有明确规则,就很容易出现:
- 低优先级任务长期占满资源
- 高优任务永远插不进来
四、更推荐的一套执行骨架
如果现在从 0 到 1 做一版测试平台执行链路,更推荐先把骨架收成下面几层。
1. 任务定义层
负责存:
- 任务类型
- 执行入口
- 默认参数
- 资源需求模板
- 超时阈值
这里不存运行态。
2. 任务实例层
每次执行都生成一条独立实例,负责存:
- 触发来源
- 实际参数
- 当前状态
- 分配到的资源
- 开始结束时间
这是平台后面做追踪、回放、审计的基础。
3. 调度器
负责:
- 从
pending/queued任务里挑候选 - 根据优先级、资源、限流规则排序
- 为任务申请租约
- 把任务分配给合适的执行节点
4. 执行代理
部署在执行节点上,负责:
- 拉取任务
- 准备运行目录
- 拉起脚本、容器或命令
- 持续回传心跳、日志和状态
5. 收尾与回收器
负责:
- 超时任务终止
- 僵尸任务清理
- 租约释放
- 日志补采
- 产物归档
这块如果没有单独设计,后面“卡运行中”几乎一定会成为平台顽疾。
五、任务执行不要直接绑死“执行命令”,最好抽成标准协议
很多平台第一版执行器最常见的写法是:
- 后端拼一个 shell 命令
- 远端机器执行
- 拿 exit code 当最终结果
这种做法能启动很快,但上限很低。
因为测试平台真正要关心的,不只是命令返回值,还包括:
- 执行开始没
- 中间跑到哪一步
- 日志是否完整
- 中途是否重试过
- 产物在哪
- 为什么失败
所以更推荐把“执行任务”抽成一套标准协议,至少包含:
preparestartheartbeatappend_logreport_artifactfinish
这样平台和执行器之间就不会只靠“一条命令是否退出成功”来沟通。
六、最小可执行实践:一条任务怎么从入队跑到回收
如果要把这件事做成可以直接落地的第一版,更推荐下面这个顺序。
1. 建 3 张核心表
task_definitiontask_executionresource_lease
其中:
task_definition只存静态定义task_execution存一次次运行实例resource_lease存环境、设备、账号等资源占用
2. 先把执行入口收成统一格式
例如任务定义里至少明确:
1 | { |
这一步的价值不在字段本身,而在于后面调度器终于知道一条任务要消耗什么。
3. 调度器固定周期扫待执行实例
第一版完全没必要上来就做得很复杂。
可以先每 3-5 秒扫一次:
- 找出
pending/queued任务 - 判断资源是否可满足
- 给可执行任务分配租约
- 把状态切到
dispatching
4. 执行代理只做一件事:可靠回传
执行器不要一开始塞太多业务逻辑。
第一版把这几件事做好就够:
- 拉取任务
- 起执行进程
- 每
5-10秒发一次心跳 - 流式回传日志
- 结束时上传产物和退出状态
5. 单独做一个超时与失联回收任务
这一步会拖到后面做,但其实应该尽早做。
它至少要能处理:
- 超过执行阈值
- 长时间没有心跳
- 节点已经下线
- 资源租约未释放
七、现场观察点:任务跑不稳时先看哪几件事
当平台执行链路开始出问题时,排查起点通常不该先盯页面,而是先看下面这张排查顺序。
1. 任务有没有真的进入正确状态
先确认:
- 是停在
pending - 还是停在
queued - 还是卡在
dispatching - 还是
running但没有心跳
不同状态,问题根因通常完全不同。
2. 资源租约有没有异常
例如:
- 任务已经结束,但资源没释放
- 任务没开始,却先抢占了资源
- 一个资源被多任务同时占用
3. 执行节点是否真的接到了任务
这里要看:
- 节点在线状态
- 最近心跳
- 最近拉取记录
- 任务下发日志
4. 执行现场是否有证据
例如:
- 运行目录是否生成
- 标准输出是否有内容
- 日志是否持续回传
- 截图、报告、产物是否落盘
5. 回收器有没有正常工作
很多平台不是不会执行,而是执行完以后收不干净,导致:
- 页面一直显示运行中
- 下次执行拿不到资源
- 报告和状态脱节
八、真实项目里最容易踩的 6 个坑
1. 把任务定义和任务实例混写
后果通常是一次执行把“默认配置”给污染了。
2. 没有资源租约
结果就是同一环境、同一设备被并发任务互相踩。
3. 执行状态只靠前端轮询
一旦后端没有标准状态机,前端轮询只会把混乱更快暴露出来。
4. 没有心跳机制
任务死了,平台却还以为它活着。
5. 超时后只改状态,不清现场
这种最危险,因为表面上超时结束了,实际上进程和资源还在占着。
6. 失败重试没有分层
有些失败该直接重试:
- 节点短时失联
- 网络抖动
有些失败根本不该自动重试:
- 用例断言失败
- 环境数据脏
- 账号权限错误
九、真实案例型段落:一次“平台越来越卡”的问题最后并不是数据库慢
场景
某次平台接入夜间回归后,团队开始频繁反馈:
- 任务提交后长时间不开始
- 页面上很多任务一直显示运行中
- 新任务经常提示资源不足
最开始最直观的想法是:
- 数据库慢了
- 队列表太大了
执行
现场先做了 4 组检查:
- 看待执行任务分布在哪些状态
- 查资源租约表里有没有长时间未释放的记录
- 查执行节点最近心跳和任务拉取日志
- 抽 5 条“运行中”任务看现场目录和进程
现象
很快发现两个异常:
- 大量任务卡在
running,但最近心跳已经断了很久 - 多个测试环境长期被占用,租约没有回收
进一步看执行机,发现有一批历史进程已经退出了,但平台里对应任务还没结束。
排查
继续顺着状态链路排,最后确认:
- 执行代理在任务退出时会上报
finish - 但如果节点短时重连,部分
finish回传会丢 - 平台又没有独立的失联回收器
- 所以任务会永久停在
running - 租约也因为只在
finish时释放,跟着一起泄漏
这时候平台表面看起来像“越来越卡”,但根因并不是数据库吞吐,而是:
执行链路缺了一段异常收敛能力。
修复
最后做了 4 个动作:
- 增加“心跳超时自动转
lost”规则 - 为
lost/timeout任务补统一回收器 - 租约释放从“只在 finish 时释放”改成“回收器兜底释放”
- 页面把“运行中但无心跳”单独标红,避免假稳定
修完之后,平台平均排队时间明显下降,最关键的是“卡死任务”不再持续累积。
十、任务调度与执行引擎的一个判断标准
判断一个测试平台的执行链路是否成熟,通常不会先看:
- 页面做得是否完整
- 支持多少任务类型
更看下面几件事:
- 任务状态是否可解释
- 资源冲突是否可控
- 执行失败是否可收敛
- 证据链是否完整
- 异常情况下平台是否还能把现场收干净
如果这些做不到,平台看起来再丰富,本质上也只是:
一个把脚本集中放到页面上的壳。
真正的测试平台,不只是“能点执行”,而是:
能让任务可靠地被安排、执行、观察、回收,并在异常时仍然保持秩序。