分布式测试执行机:让用例跑在一台机器的 20 倍规模上

单机跑全量回归要 4 小时,发版窗口只有 90 分钟——分布式执行机不是为了"炫技",是被发版节奏逼出来的。这篇记录一套轻量分布式测试执行架构:不引入 Kubernetes,N 台 Windows 机器 + 一个调度中心,把 4 小时压进 90 分钟。

一、架构总览

1
2
3
4
5
6
7
8
9
                 ┌─────────────┐
Web 控制台 ─────▶│ 调度中心 │◀──── 执行机心跳(10s)
│ (Flask) │────▶ 任务下发(gRPC/HTTP)
└──────┬──────┘
┌─────────────┼─────────────┐
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ 执行机 A │ │ 执行机 B │ │ 执行机 N │ (每台 4~8 个执行槽)
│ 槽1..4 │ │ 槽1..8 │ │ 槽1..4 │
└─────────┘ └─────────┘ └─────────┘

关键决策:执行机是无状态的 worker。用例代码、配置、依赖全部从调度中心拉取(git 仓库 + 配置包),执行机只负责"给我一个任务 ID,我跑完把结果吐回来"。加机器 = 装个 agent + 配个 token,零代码变更。

二、任务切分:用例 → 任务 → 槽位

切分粒度决定了并行效率和稳定性,三层结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Scheduler:
def split(self, plan: TestPlan) -> list[Task]:
"""切分策略:同平台用例聚组,组内按预估时长贪心装箱"""
groups = group_by(plan.cases, key=lambda c: c.platform)
tasks = []
for platform, cases in groups.items():
# 贪心装箱:目标 = 单任务时长 ≤ 15 分钟
for box in bin_pack(cases, target_minutes=15):
tasks.append(Task(
platform=platform,
cases=box,
est_minutes=est(box),
))
return tasks

为什么按平台聚组:同一平台(同一台设备/同一个 App 包)的用例放一个任务,避免一台机器上同时装 4 个不同平台——设备互斥、安装包冲突、代理端口打架,全来自跨平台混跑。

为什么单任务 15 分钟:经验值是"失败可定位"的上限——任务跑 15 分钟挂了,回看日志找现场是现实可行的;60 分钟的任务挂了基本只能重跑。

三、心跳与槽位:动态感知道具

执行机 agent 每 10s 上报:

1
2
3
4
5
6
7
8
9
10
{
"agent": "A-03",
"slots": [
{"id": 1, "state": "idle"},
{"id": 2, "state": "running", "task": "T-1024", "progress": 0.4},
{"id": 3, "state": "busy_install", "task": "T-1025"},
{"id": 4, "state": "fault", "reason": "device offline"}
],
"load": 0.6
}

槽位状态机:idle → busy_install → running → done/fault → idle。调度中心只往 idle 槽派任务;fault 槽自动隔离,连续 3 次 fault 的机器整机摘除并告警——一台坏机器不应该拖累整批回归。

四、结果回收与断点

每个用例结束立即上报,不等任务结束:

1
2
3
4
5
6
7
8
9
# 执行机 agent(节选)
for case in task.cases:
result = run_case(case) # 本地执行,SSE 推实时日志
post("/api/tasks/result", {
"task": task.id, "case": case.id,
"status": result.status, # passed/failed/skipped
"duration": result.duration,
"artifacts": [screenshot_path, log_path], # 上传 OSS 后传 URL
})

断点续跑:任务失败(机器宕机、App 崩溃未恢复)时,调度中心看到"已上报 passed 的用例"和"计划用例"的差集,把差集重组成新任务重新派发——不需要重跑已经通过的用例。这是分布式回归里省钱的关键:一次全量回归中途挂 2 台机器,只重跑 20% 的用例。

五、规模数据(真实压测)

规模 全量回归(约 600 用例) 对比单机
单机 8 槽 3h50m 1.0×
3 机 20 槽 1h18m 3.0×
5 机 32 槽 52m 4.4×

注意加速比不是线性的:任务切分粒度(15 分钟装箱)决定了尾部效应——最后一个任务要等它自己跑完。装箱越均匀,尾部越短,这也是贪心装箱里按预估时长排序的原因。

六、踩过的坑

  1. 时钟漂移:5 台机器的系统时间差 2s,导致"用例开始时间"排序全乱——agent 启动时强制 NTP 同步;
  2. 配置包版本:任务 A 用配置 v12 跑,任务 B 用 v13,结果对比时发现环境不一致——任务元数据里固化配置包 hash,报告头部展示;
  3. 孤儿进程:任务被 cancel 后 App 进程还活着,占着设备——cancel 语义必须包含"杀进程树 + 卸载代理",agent 本地兜底超时强杀。

七、小结

分布式执行机的设计原则就三条:worker 无状态(扩缩容零成本)、任务粒度 15 分钟(失败可定位)、结果逐例上报(断点可续跑)。不追求 K8s 那种通用编排,测试场景的"设备互斥 + 安装包互斥"用平台聚组解决,简单有效。