前几篇讲了环境检测方案与巡检系统,评论区有同学问到工程细节:几十个浏览器环境(每个暴露一个 CDP 调试端口),连接层怎么管理才不至于一团乱?这篇就把连接层单独拆出来讲:环境注册、连接生命周期、健康检查与故障转移——一个可以直接抄走的EnvironmentPool实现。
一、问题:为什么需要连接池
朴素的做法是"用时连接、用完关闭":
单环境单任务没问题。但规模上去之后会同时遇到四个痛点:
连接建立成本:每次connect_over_cdp都要完成 WebSocket 握手与协议初始化,批量任务频繁建连,耗时累积可观;
环境状态不可知:环境可能休眠、重启、端口变更,"用时才发现连不上"意味着任务失败后只能重试;
并发失控:20 个环境同时发起任务,瞬时连接数可能压垮宿主机;
故障无兜底:某个环境掉线,依赖它的任务全部失败,没有降级路径。
连接池要解决的就是这四件事:复用连接、感知状态、限制并发、故障转移。
二、设计:环境注册表 + 连接池
整体结构:
先看注册表——环境与端口的映射,加上健康状态:
注册表由独立脚本维护(环境增删时更新 JSON),Registry只负责读取与状态标记——职责分离的原则和巡检系统那篇一致。
三、核心:EnvironmentPool 实现
连接池本体,四个能力逐一实现:
三个设计决策需要说明:
连接不主动关闭。acquire建立的连接长期持有,release只归还并发额度——连接复用是池的核心价值。浏览器进程本来就在运行(我们接管而非创建),连接的生命周期由池统一管理,程序退出时统一断开。
失败即标记。连接失败直接写入注册表的健康状态,健康检查与故障转移都依赖这个标记,而不是事后单独探活。
超时显式设置。connect_over_cdp默认超时较长,环境休眠时会挂住整个任务队列。10 秒超时 + 失败标记,让队列继续流转。
四、健康检查与故障转移
health_check的探活动作刻意做轻(about:blank 开关一次页面)——探活本身不该对环境产生可感知的负载。acquire_with_fallback的备选顺序由调用方传入(业务知道哪些环境可互为备份),池只负责按序尝试。
五、踩坑与调优记录
坑一:Semaphore 的释放要对齐。初版acquire失败路径忘了release,并发额度被慢慢漏光,表现为"越跑越慢最后卡死"。修复原则:acquire 的每个退出路径(成功/失败/异常)都必须对应一次 release——上面代码里失败分支的self.sem.release()就是干这个的,建议用 try/finally 结构消除这类不对称。
坑二:连接失效的检测是滞后的。环境重启后,池里持有的连接对象已经失效,但只有下次操作时才会抛错。两层缓解:任务层的重试装饰器(捕获协议错误后强制重建连接再试一次);health_check定期清扫(每 30 分钟全量探活一轮,失效连接直接清出池)。
坑三:并发上限是宿主机决定的。并发 3 是我们在 16G 内存的开发机上实测的稳态值,换到 8G 的旧机器上要降到 2。这个参数没有通用答案,用健康检查的失败率做反馈信号调节即可。
六、小结
连接层的工程化,本质是把"用时连接"的随机性,换成"注册表 + 池 + 探活 + 兜底"的确定性。代码加起来不到两百行,但四个痛点(成本、状态、并发、故障)各有了明确归属。
这个池目前服务于巡检系统,接下来扩展到受控操作(自动确认订单等)时,唯一要加的是操作审计——每个连接上执行了什么,记录归档,这在从"只读"走向"写入"时是必须的。等落地后继续写。
本文代码基于 Python 3.10 + Playwright 1.40 验证(2026 年 9 月),EnvironmentPool可直接复用。工程参数(并发上限、探活周期)为团队实测值,请按自身环境调优。