01 / 设备池
设备不是插上就算,先过接入判定
多设备并行的第一个误区,是把“连上了”当成“能用了”。设备管理器里有名字、ADB 设备列表里能看到序列号,都不构成可用。判定一台设备能不能进池,只有三条硬条件,逐条过。
一、存活只认 shell 往返
设备列表是缓存,不是事实。USB 掉线、无线调试断开、adb server 里留下僵尸记录,都会让列表继续显示序列号。可靠的做法是发起一次真实的 shell 往返,拿到返回才算活着。这条判断要写进执行前的检查里,不能靠人眼看列表。
二、uiautomator2 能建会话并 dump 出界面
能连上不等于能读界面。uiautomator2 要能在该设备上建立会话、成功 dump 出当前界面结构,才说明界面读取这一层通了。dump 有重试机制,但重试两次仍失败,这台设备不该进池。dump 在 launcher 屏会失败,需要先试压缩模式再退回普通模式,这个细节放在第 06 节。
三、中文输入至少一条通道可用
三条中文输入通道分别基于 uiautomator2、adbkey 与 adb_input。其中 adb input text 打不出中文,这是实测结论,不是配置问题。所以一台设备至少要有一条第 06 节以外可用的中文输入路径,否则它能点、能读、就是写不了标题,进池没有意义。
三条都过,才进设备池。引擎自检在 3 台设备上各 13 项全过,就是这条门槛的具体形态:不是“跑起来没报错”,是 13 项逐项通过。设备进池时要落一条记录,字段固定:设备标识、ADB 序列号、安卓版本、绑定的 adb server 端口、归属服务线、最近一次自检结果与时间、当前状态(可用、隔离、退役)。这张表是后面所有判断的依据,字段缺一项,排查时就少一条线索。
判定频率要分两档。轻量的一档是每次任务前做一次 shell 往返,几秒钟,用来挡住刚刚掉线的设备;完整的一档是每天一次跑全 13 项自检,用来挡住那些能连上但界面层已经坏掉的设备。只做轻量档,会把“能连不能用”的设备放进队列;只做完整档,会在两次自检之间让掉线设备拖垮一批任务。
还有一类设备要单独说:模拟器。模拟器没有实体 USB,掉线方式不一样,往往是整个进程被杀或端口被回收。判定条件一样,但它的端口与进程绑定关系需要单独维护,不能和真机共用一张端口表。混在一张表里的后果是:模拟器重启后端口被别的进程占走,而配置里还以为它在那儿。
设备端的流程、点击与输入这几层怎么跑,在手机 APP 自动化通道里逐项写过。
02 / 端口锁
一个端口一台设备,锁必须显式
多设备并行最容易踩到的工程约束是这一条:uiautomator2 的一个进程只绑一个 adb server 端口。这句话的含义是,多个设备要真正并行,就不能共用默认端口,必须为每台设备分配独立的 adb server 端口,并且保证同一时刻只有一个进程占用某个端口。
不做这件事的表现很有欺骗性。程序不报错,任务也在跑,只是速度上不去——两台设备的操作被排成了一串,因为它们在抢同一个端口。更危险的一种表现是:进程以为自己控制了 A 设备,实际拿到的是 B 设备的界面,于是点击落在了错误的机器上。这种情况下日志看起来正常,只有对比截图才能发现,而截图往往没人看。
| 环节 | 做法 | 不做会怎样 |
|---|
| 端口分配 | 端口号与设备标识在配置里一对一绑定,不用默认端口 | 多设备退化成串行,或互相串台 |
| 申请 | 启动时先申请端口,写一份带进程号与时间戳的租约文件 | 两个进程同时认为自己拥有该端口 |
| 校验 | 拿到界面后先核对设备身份,再执行第一个动作 | 点击落到别的设备上,且不报错 |
| 释放 | 正常退出与异常退出都要释放,租约超时可被回收 | 端口永久被占,只能人工清锁 |
租约文件比起简单的“有文件就算占用”更可靠,原因是它会过期。进程崩溃时来不及删锁是常态,如果锁只是一面旗子,一次崩溃就会让这台设备永久不可用。写成租约:文件里存进程号、开始时间、设备标识;回收程序看到租约超过阈值就判定失效,回收后再重新分配。阈值不能太短,否则正常的慢任务会被误判;也不能太长,否则崩溃后要等很久。实际取值按单条任务最长耗时上浮一段来定。
端口校验这一步值得单独强调。判断“这台设备是不是我要的那台”,不能只看 adb 序列号——序列号在部分模拟器和部分真机上会重复或被改写。做法是在设备进池时把一条可读的标识写进配置(比如设备名加序列号的组合),每次建会话后读一次,与配置比对,不一致就中止这一条任务并隔离设备。这个检查只花一次界面读取,却能挡住后果最重的一类错误。
端口数量要预留。五台设备不是配五个端口,而是要留出回收与轮换的余量,否则一台设备隔离后备用机接入时会发现端口全被占着。经验做法是按设备上限的一点五倍规划端口段,并在配置里写清楚哪一段留给真机、哪一段留给模拟器,避免两类设备争同一段。
03 / 槽位归属
任务不是平均分,是算出来的
设备变多之后,最自然的想法是“五台设备,任务平分五份”。这个想法会直接撞上平台的节奏约束,因为节奏不是按设备算的。同一份发布计划里,两条通道的槽位归属是算出来的——同一个函数对象算出结果,不是人工在表里标出来的。多设备延伸也是同一条规则:槽位归属由分配函数算出,人工只在异常时干预,不做日常标注。
先摆清楚约束:每日 5 到 10 条,条间隔不小于 30 分钟并带抖动,发布窗口 09:00 到 21:59,单服务线每日 2 条,周上限 12 条。这几条是账号侧的边界。所以加设备不会提高单个账号的日上限,一台设备和五台设备对同一个账号的配额是一样的。加设备改变的是并行度、容错和不同账号之间的隔离度。
先按服务线切,再按设备落
队列的第一步切分单位是服务线,不是设备。每个服务线(对应一个账号)有独立的节奏计数器与独立的发布窗口占用,互不借用配额。切完之后才轮到设备:一条服务线固定一台主设备。固定主设备的理由很实际——同一个账号在两台设备上同时操作,行为特征会立刻变得不一致,登录态、缓存、操作习惯都会露出破绽。
备用设备只在隔离时接管
主设备被隔离,备用设备接管,但接管要带冷却:不在隔离的同一分钟内接手,先跑一次完整自检,通过再接。冷却的作用是避免把“主设备坏了”变成“两台设备一起坏”。接管只是把队列顺延,不补发——顺延掉的那一条进下一个可用槽位,不额外增加当日条数。
负载不按台数平均
五台设备不等于每台两成。分配要看三件事:该服务线当天还剩几个槽位、设备最近一次自检的耗时、设备当前的队列深度。耗时长的设备少分两条,比平均分更接近实际吞吐。平均分的后果是慢设备成为瓶颈,快设备空转,整体窗口利用率反而下降。
槽位编号要唯一且稳定
每个槽位在计划生成时就拿到编号,之后不管由哪台设备执行,编号不变。台账按槽位编号对账,设备换了、通道换了,编号都不换。这是后面能算清“谁漏了”的前提。
分配函数要能解释。上线前拿一份计划跑一次分配,把每个槽位的归属打出来,人工核一遍:同一个账号有没有被分到两台设备、同一台设备有没有同时背两个账号、某台设备是不是连续吃了当天最后几个槽位。三项都过,分配逻辑才算可用。跑不出来就别上,靠人肉排表的系统在设备数超过三台之后一定会乱。
顺延队列是这套分配的缓冲区。它记录“本该发但没发”的槽位,附原因与原始计划时间。队列深度是一个很直接的健康指标:稳定在一两条是正常波动,持续涨到一天的槽位量,说明设备池的吞吐已经跟不上计划,这时候要么减计划,要么加设备,而不是压缩间隔硬塞进去。间隔与窗口的具体定义见定时发布与节奏控制。
04 / 故障隔离
一台坏了,不能拖住整条队列
多设备的价值有一半体现在故障上:单台设备出问题,当天其他槽位照常跑。要做到这一点,得先把“坏了”定义清楚,再把“坏了之后做什么”固定下来,否则现场每次都在临时决定。
设备状态只有三级:可用、隔离、退役。三级之间只有四条转移路径,写死在程序里,不做第五种状态。状态多了,统计会变形,判断会变慢。
| 触发条件 | 判定动作 | 状态转移 |
|---|
| shell 往返连续失败 | 重试一次仍失败即判离线,不无限重试 | 可用 → 隔离 |
| 自检某项不通过 | 记录失败项编号,留一份界面快照 | 可用 → 隔离 |
| 同批任务中仅该设备集中失败 | 对比同批其他设备结果,排除平台侧原因 | 可用 → 隔离 |
| 隔离后自检全过 | 跑完整自检,13 项全过 | 隔离 → 可用 |
| 隔离后连续两轮自检不过 | 记录原因与次数,不再自动回池 | 隔离 → 退役 |
这里要区分两种健康闸,混在一起是常见错误。平台健康闸看的是平台侧状态:出现异常时暂停发布,不硬发,等状态恢复。设备健康闸看的是这台设备自己:只影响该设备上的任务,其他设备照常。把两者做成一个开关,结果是平台稍有波动就全池停摆,或者设备坏了却以为平台好了。两条闸分开,日志里也分开记,复盘时才分得清是哪一类。
隔离之后不做自动重试。这条看着反直觉,但在发布场景里是必须的:重试发布可能造出一条重复商品,重复比缺一条更难处理。正确处理是顺延——该槽位进顺延队列,由备用设备或下一个可用槽位接。重试只允许发生在“读界面失败”这类无副作用的动作上,凡是会改变平台状态的动作,一律不重试。
隔离时要留现场。至少三样:一张界面快照、一次界面结构 dump、一条带时间的失败原因枚举。原因必须是枚举值,不能写句子,否则一周之后就统计不出“这台设备主要因为什么被隔离”。三样东西的价值在于:设备是真坏了还是被某类商品卡住了,看这三次记录就能分开。曾经出现过的情况是某台设备连续被隔离,最后发现是特定类目的图片上传慢,超时被误判成设备故障——有 dump 才看得出来。
最后是恢复的节奏。隔离的设备不要立刻回池,先跑一次自检,通过了再观察一轮任务,两轮都正常才回到正常分配权重。急着回池的代价是同一台设备一天内反复隔离,队列反复顺延,节奏被打乱——这比直接少一台设备更糟。设备数少的时候尤其明显,因为反复隔离会让当天窗口末端堆满顺延任务。
05 / 并发回写
多台同时写账,槽位守恒不能破
设备一多,回写就成了并发问题。两条通道写的是同一本台账,由一个写入模块统一落盘,记录字段是 item_id、标题、描述、价格、时间戳这一套。多台设备同时回写时,如果写入不做并发控制,问题不会以报错的形式出现,只会以“对不上”的形式出现:按槽位对账时四个状态加起来不等于当日槽位数。
交错写入
两行内容在同一位置被同时写,结果是一条记录里混了另一条记录的一部分。表现为台账里出现一条字段都对但组合不合理的记录,肉眼很难发现。解决方式只有一种:整行一次性追加,或者加锁。
丢记录
两个进程读到同一个文件末尾位置,各自追加,后写的覆盖先写的。表现为当天少一条记录,且没有任何痕迹。设备数越多,撞上的概率越高。
状态覆盖
同一条商品先被写成失败,随后被改成成功,两次写入顺序颠倒,最终状态可能是错的。台账只允许追加,改状态靠追加一条新记录,当前状态取最后一条,这样顺序问题不会变成状态问题。
并发控制的具体做法是锁加追加:写入前申请一把针对台账文件的锁,拿到锁之后以追加模式写完整一行,再释放。锁的粒度对整本台账还是分段,取决于并发量;设备数在十台以内,整本加锁的开销可以忽略,简单做法更不容易出错。追求细粒度锁之前先确认并发真的到了那个量级,否则是把复杂度提前引入。
写生产数据前留一份备份,是这套并发机制的配套要求,不是可选项。备份要在改动之前生成,并且立刻读回来验证行数一致——很多所谓的备份从未被恢复过,需要的时候才发现是空的。这个要求在设备多的时候更重要,因为一次并发事故影响的是整天的记录,而不是一条。
还有两道防护跟并发无关但必须一起做:自检进程带标记,带标记的进程尝试写生产数据直接抛错;整轮自检前后对生产文件做哈希比对,不一致就报警。这两条防的是“写错地方”,和并发防的“写坏内容”是两回事。曾经出现过自检数据写进生产台账的事故,根因是路径解析早于环境变量生效,所以数据目录必须用绝对路径写在配置里,不依赖环境变量。台账的字段口径与并发细节,在发布数据台账里有完整说明。
多设备场景下还要注意一件小事:时间戳。不同设备的系统时间可能差几十秒,如果时间戳取设备本地时间,跨零点或跨窗口边界的判断就会飘。做法是统一取主控端时间,设备侧只提供事件顺序,不提供绝对时间。这一条在设备少的时候看不出来,设备一多就会有若干条记录落在窗口边界外,而规则上它们本该在窗口内。
06 / 工程坑
几处能让全池一起停摆的坑
下面这几处是实测踩过的,共同特点是:报错信息指向的地方不是真正的根因,排查时容易在错误的方向上花掉半天。
PIT 01驱动跑在哪个系统上
WSL 到 Windows 的 TCP 不通,所以 ADB 驱动必须跑在 Windows 侧。把驱动放进 WSL 里,表现出来是设备列表空、连接超时,而设备本身完全正常。
PIT 02adb server 要显式寻址
不要依赖默认地址与环境变量推断。server 地址写在配置里,连哪个地址、哪个端口都明确。依赖推断的后果是同一台机器上不同进程连到了不同的 server,各自看到一部分设备,谁都不全。
PIT 03dump 在 launcher 屏会失败
界面结构 dump 在桌面(launcher)屏有较大概率失败。做法是先试压缩模式,失败再退回普通模式。只写一条路径,就会出现“任务卡在打开应用那一步”的现象。
PIT 04中文打不出来不是编码问题
adb input text 打不出中文,这不是字符集设置能解决的。要用 uiautomator2 那条通道,已在实测中验证可输入中文。把它当成编码问题去调,会一直调不通。
PIT 05供电不足导致批量掉线
设备数量上去之后,普通 USB 口的电流不够,表现为好几台设备在同一时间段集体掉线。换带独立供电的集线器,并把设备分散到不同的控制器上,比逐台排查有效。
PIT 06端口占用清理不干净
进程被杀但端口没释放,下一次启动申请不到端口,报的却是“设备未找到”。这就是第 02 节要用租约文件的原因:锁会过期,比人工清锁可靠。
这六个坑有一个共同的处理原则:把判断写成代码里的检查,不要写进值班笔记。设备连不上时,先看 shell 往返、再看 server 地址、再看端口租约,三步走完才知道该修哪里。三步靠人记,换个班就断了。
另外补一条容易被忽略的:设备名与序列号。部分设备的序列号在重新授权后会变,部分模拟器的序列号是固定值,多台模拟器会撞号。配置里如果只用序列号做键,撞号时会出现两台设备抢同一份配置,症状是任务跑到一半行为变了。做法是键用“序列号加设备名”,并在进池时校验唯一性,撞号直接拒绝入池。
入池之后再改设备名,等于换了一个身份,配置里的键与端口绑定都要同步改。漏改的表现是任务报设备未找到,而设备明明还在列表里——这类故障最容易让人在 USB 和数据线上白折腾半天。
07 / 风控边界
设备多,不等于可以多发
多设备最容易被误用的方向,是把它当成多开刷量的工具。这个方向不但无效,还会把账号推到风险里,因为节奏约束是按账号算的,不是按设备算的。
发布侧的约束摆在这里:每日 5 到 10 条,条间隔不小于 30 分钟并带抖动,窗口 09:00 到 21:59,单服务线每日 2 条,周上限 12 条。这些数字在设备增加到五台之后一个都不变。五台设备对同一个账号发送,只会让间隔变小、时间分布变密,正好落在风控最敏感的特征上。
多设备真正的用处
- 单台故障不影响当天槽位,队列能顺延而不是丢
- 不同账号在不同设备上,登录态与操作习惯互不串
- 自检、灰度、新流程验证可以并行,不占生产设备的窗口
- 备用设备可以在隔离时接管,不需要现场换机
- 不同类目的操作节奏可以分开调,互不牵制
多设备不解决的问题
- 单个账号的日上限与周上限
- 商品同质化:指纹去重、同款链接不超过三条、单链接 3 到 9 张图这些结构规则
- 类目分散、发布时间分散、标题与主图相似度分散
- 合规闸门与到点复检的结论,不通过就是不发
- 平台侧异常时的暂停,硬发只会更糟
合规闸门和发布时刻复检要一起看。闸门在计划阶段跑一次,到点发布前用当下的基线再跑一次,不通过就不发。多设备并行时,这一条尤其重要:如果只在计划阶段跑,几台设备同时执行时基线已经变了,前面几条发出去改变了同类商品的分布,后面几条按旧基线判断就会误放。复检是 fail-closed 的,宁可少发一条。
货品层还有几条规则要用设备池一起守住:商品指纹去重,同一个指纹的链接不超过三条;单链接 3 到 9 张图,少图会被判为低质,多图上传慢会拖窗口;类目按计划分散,不集中在同一类目连续发。这些规则由闸门执行,跟哪台设备执行无关,但设备多的时候更容易被绕过——因为人会觉得“反正还有别的设备,多插一条看不出来”。看不出来的只是当天,看得到的是一周之后整体结构变差。
还有一个很实际的边界:设备与账号的对应关系要固定。今天 A 账号在 1 号机,明天换到 3 号机,这种迁移看起来只是换了台机器,实际上登录环境、机型信息、操作间隔全都变了,是风险最高的一类变化。要做迁移就一次做完,之后固定不动,不要每天换。相关规则在防封控与商品合规检查两页里有更细的口径。
08 / 扩容判断
什么时候该加设备,什么时候不该
加设备是成本决策,不是能力证明。判断依据只有几条,都能量化,不需要凭感觉。
该加的信号一:顺延队列持续变深
顺延队列稳定在一两条是正常波动。连续三天超过当日槽位量的一半,说明吞吐跟不上计划,加设备或减计划二选一。先减计划更省事,因为计划本身可能就超了账号能承受的节奏。
该加的信号二:隔离率上升但不集中
如果隔离集中在同一台设备,是那台设备的问题,换掉就行。如果隔离分散在多台设备,且原因各不相同,说明设备已经跑在极限上,需要扩容而不是修。
该加的信号三:单台任务排队跨过窗口
某台设备当天最后一条任务的开始时间已经接近窗口末端,说明这台设备的容量到顶了。这时候压缩间隔是错的,因为间隔有下限。
不该加的情况
日条数还没到上限、失败集中在合规闸门、没有值守人力、账号本身只有一个。这四种情况加设备只会增加维护面,不增加产出。
上线前把这份清单逐条过一遍,比事后排查省时间。
- 设备身份唯一序列号加设备名的组合在全池唯一,撞号的设备拒绝入池
- 端口一对一绑定每台设备有独立 adb server 端口,写在配置里,不用默认端口
- 锁带过期端口租约有进程号与时间戳,崩溃后可自动回收
- 三条入池判定shell 往返、界面 dump、中文输入通道,逐条通过才入池
- 主设备固定一条服务线只对应一台主设备,备用机只在隔离时接管
- 分配可解释拿一份计划跑出归属表,人工核对无账号串台、无设备连吃末端槽位
- 三级状态只有可用、隔离、退役,状态转移路径写死在程序里
- 两种健康闸分开平台侧异常暂停发布,设备侧异常只隔离单台
- 改状态动作不重试发布类动作失败只顺延,不重试,避免重复商品
- 并发写入有锁整行追加加锁,写生产前留备份并读回验证行数
- 绝对路径数据目录用绝对路径写在配置里,不依赖环境变量
- 时间统一时间戳取主控端时间,不用各设备本地时间
最后一句判断:设备池是运维对象,不是采购清单。买五台设备只要一天,让这五台稳定地按同一套规则跑一个月,靠的是上面这份清单里的每一条都真的在执行。设备数从一台到三台的变化,主要是端口与分配;从三台到十台的变化,主要是并发、隔离与值守。中间没有捷径,只有把每一层都写清楚。