手机设备农场:从 5 台到 50 台的自动化调度

App 测试绕不开"设备矩阵":Android 高低端 × 不同 ROM × iOS,单台机器上的设备池撑不住回归规模。2025 年把设备池从 5 台扩到 50 台,调度层完全重做,这篇讲设备农场的架构和几个绕不开的坑。

一、设备池的抽象:设备 = 资源,不是"手机"

第一原则:调度中心眼里只有"设备句柄",不关心它是 ADB 序列号还是 USB 口还是云真机

1
2
3
4
5
6
7
8
@dataclass
class Device:
handle: str # adb:R5CT123AB / usb:001 / cloud:vm-7
platform: str # android / ios
spec: dict # {rom, resolution, cpu_tier: low/mid/high, ram}
location: str # rack-a / rack-b / cloud
state: DeviceState # idle / busy / install / fault / maintenance
capabilities: list # [gps_mock, multi_open, root]

capability 标签是调度的关键维度:用例声明自己需要哪些能力(多开用例要 multi_open,LBS 用例要 gps_mock),调度器做能力匹配。设备"是什么"(序列号)只出现在最底层的连接层,上层永远只见标签——这样加一批新设备,只要标签体系不变,调度零改动。

二、调度:三维匹配 + 亲和性

任务到设备的匹配规则,按优先级:

  1. 硬约束:平台 + capability 不匹配直接排除;
  2. 亲和性:同批次任务尽量落到同一设备/同一机架——减少"设备预热"和跨机架的 ADB 转发开销;
  3. 负载均衡:候选设备里选最近 1 小时利用率最低的;
  4. 故障规避:近 24h 有 fault 记录的设备降权(不是拉黑,fault 原因可能是偶发)。
1
2
3
4
5
6
7
def pick_device(task, pool):
candidates = [d for d in pool
if d.state is idle
and d.platform == task.platform
and task.need ⊆ d.capabilities]
return min(candidates, key=lambda d: (
-affinity(d, task.batch), d.load_1h, fault_score(d)))

为什么亲和性排第一:App 首次冷启动比热启动慢 3~5 秒,一批 50 个用例如果散落在 50 台设备上,光冷启动就多出 2~3 分钟,而且每台设备的 ROM 差异会把"环境问题"和"产品问题"混在一起。同批同设备(或同 ROM 设备)是失败归因的便宜保险

三、设备健康:主动巡检,不是等用例挂

设备故障是无声的——屏幕黑了、触摸漂移了、ADB 断连了,都表现为"用例随机失败"。被动等失败归因太贵,建主动巡检:

1
2
3
4
5
6
7
HEALTH_CHECKS = [
("adb_state", check_adb_online, 30), # 每 30s
("screen_on", check_screen_brightness, 60), # 每 60s
("touch", check_tap_echo, 300), # 每 5min:点一下看 UI 响应
("storage", check_storage_free, 3600), # 每 1h:磁盘 <2G 告警
("battery", check_battery_plugged, 300), # 充电断开立即标记
]

巡检结果进设备状态机:单项失败 → degraded(仍可接单但降权);连续 3 项或核心项(adb/touch)失败 → fault 自动摘除 + 工单。关键经验degraded 状态比 fault 更有价值——80% 的设备问题(触摸漂移、响应变慢)在 degraded 期就能拦住,等 fault 时设备早就"看起来正常"地污染了一批用例。

四、50 台之后才暴露的问题

问题 症状 解法
ADB 风暴 50 台并发 dump 窗口,USB 3.0 hub 全卡死 dump 请求按机架分桶限流(每桶并发 ≤8),加 100ms 抖动
屏幕镜像带宽 实时投屏 50 路,管理网 1Gbps 打满 投屏默认关,按需开;开时限制 15fps/720p
热堆积 机房设备连续跑 4h,CPU 降频,用例耗时 +40% 批次间强制冷却 10min;机房风扇按温度联动
数据残留 上一任务登录态没清,下一任务"已登录"直接跑 任务结束钩子:强制卸载重装或清数据,由调度器执行不信任用例

五、小结

设备农场的三个层次:抽象(设备 = 带标签的句柄,新设备零代码接入)、调度(能力硬约束 + 亲和性 + 负载)、健康(主动巡检 + degraded 降级)。规模从 5 到 50 台,代码量翻了 3 倍,但用例层零改动——抽象做对了,扩容就是加标签的事。