技术方案报告

Playwright 并发资源调度方案

全局浏览器锁 + pw-schedule 统一调度:2 核 3.6G VPS 上的多会话资源治理
日期:2026-08-04 面向:车贷平台 AI 技术部 环境:Hermes 网关 · 2 核 CPU / 3.6GB 内存

一、背景与现象

Hermes 网关运行在 2 核 CPU / 3.6GB 内存的 VPS 上。当多个会话/多个项目同时运行时,各会话独立启动浏览器与本地服务,资源互相打架,出现「交通拥堵」——所有并发项目都无法按时完成任务。

典型现象(用户 2026-08-04 反馈)
  • Playwright 测试大量失败或卡死
  • 浏览器操作 30 秒超时
  • 本地页面服务访问到「旧项目」的页面(端口串台)
  • 用户原话:「应该是被抢占用来做其他任务了」

二、根因分析(日志实锤)

2.1 硬件天花板

资源数值含义
CPU2 核单个 Chromium 即可占满一个核
内存3.6GB(swap 1.9GB)每个 Chromium 实例 300-800MB
浏览器实例每会话独立_active_sessions 按 task_id 隔离

2.2 三层根因

根因 1:调度层无全局闸门
  • tools/browser_tool.py:每个 task_id 独立创建 agent-browser daemon + Chromium 会话(_create_local_session
  • 只有进程内线程锁 _cleanup_lock没有跨会话的全局并发限制
  • 8/4 18:21-18:24 三个 swarm 子会话(365b5c/7705a7/cc878b)同时跑 browser → cc878b 在 18:22:47 出现 browser 'eval' timed out after 30s
根因 2:操作层端口无治理
  • 8/2 充电宝调研会话:8099 端口被 4 个残留 http.server 进程轮番占用,Playwright 测试全部访问到旧项目「微信小程序」页面而非新报告
  • 各项目端口随意(8080/8099/8100/8101),无分配规范、无冲突检测
根因 3:清理层残留累积
  • /tmp 下堆积十几个 .org.chromium.Chromium.* 残留目录
  • errors.log 记录 15+ 次 browser timed out
  • 进程崩溃/会话异常退出后,Chromium 与 http.server 无人回收

2.3 一句话结论

有限资源(2 核 3.6G)被无界并发请求争抢——三套独立体系(agent-browser 会话、npx playwright test、http.server)互不感知、无排队、无端口分配、无残留清理。

现状:Playwright 资源无调度 → 交通拥堵(问题图)会话 A 启动 agent-browser + Chromium会话 B 启动 agent-browser + Chromium会话 C 启动 npx playwright test2 核 CPU / 3.6GB 内存能同时扛住?不能 - 内存吃紧 / CPU 抢占Chromium 相互抢占 CPU内存触及 swap 边界browser eval 30s 超时playwright 测试卡死/失败http.server 端口串台 (8099 被 4 个残留进程轮番占用)所有会话都无法完成任务 ← 交通拥堵正常完成
图 1 · 现状:Playwright 资源无调度,多会话并发导致「交通拥堵」

三、方案设计(A+B 组合)

3.1 方案 A:全局浏览器闸门(源码层)

目标:保证同一时刻只有一个 Chromium 实例运行。

实现要点

  1. browser_tool.py 增加跨进程文件锁:flock /tmp/hermes-browser.lock
  2. _get_session_info() 创建本地会话前先尝试获取锁
  3. 获取失败 → 排队等待(默认上限 300s),期间轮询
  4. 会话关闭/空闲超时(120s)→ 释放锁
  5. 排队超时 → 返回明确错误「浏览器繁忙,另一会话正在使用(已等待 300s)」

关键设计

  • 锁粒度 = 全局互斥(2 核物理上限,任何两个 Chromium 同跑都会抢 CPU)
  • 排队而非直接失败(并发时等一等而不是任务中断)
  • 非浏览器操作(终端/文件/网络)不受锁影响

3.2 方案 B:pw-schedule 统一调度(包装命令)

目标:统一治理 Playwright 测试与本地 HTTP 服务,端口永不串台,残留自动清理。

命令设计

pw-schedule test [dir]       # 包装 npx playwright test:排队 → 跑 → 释放
pw-schedule server [dir]     # 端口池(8080-8105)分配 → 启动 http.server → 登记 → 退出清理
pw-schedule status           # 查看当前占用
pw-schedule clean            # 清理残留(孤儿 chromium / 残留 http.server / 过期注册项)

注册表~/.hermes/state/playwright-registry.json

{
  "entries": [
    {"pid": 12345, "port": 8080, "dir": "/home/ubuntu/proj",  "started_at": "...", "owner": "session-xxx", "type": "http.server"},
    {"pid": 12346, "port": null, "dir": "/home/ubuntu/proj2", "started_at": "...", "owner": "session-yyy", "type": "playwright"}
  ]
}

端口分配:从 8080 起扫描,lsof 检测空闲即占用;冲突自动换下一个端口,不报错。

残留清理:孤儿判定 = 注册表有记录但进程不存在 → 删;socket 目录无 owner_pid 或 owner 进程已死 → 删。

方案:全局浏览器闸门 + 统一调度(解决图)会话A会话A会话B会话B全局浏览器锁(flock)全局浏览器锁(flock)agent-browserChromiumagent-browserChromiumpw-schedule注册表pw-schedule注册表http.server(端口池 8080-8105)http.server(端口池 8080-8105)浏览器会话(方案 A:串行排队)browser_navigate 请求锁获取成功创建 Chromium 会话执行命令 / 截图空闲 120s 后释放锁browser_navigate 请求锁⏳ 排队等待(上限 300s)释放锁获取成功(A 已释放)创建 Chromium 会话执行命令释放锁本地服务与测试(方案 B:统一调度)pw-schedule server /dir分配端口 8080(空闲)服务启动 127.0.0.1:8080pw-schedule server /other-dir8080 被占 → 分配 8081服务启动 127.0.0.1:8081pw-schedule test排队 → npx playwright test → 释放维护(自动清理)进程退出即删登记项cron 每日 pw-schedule clean孤儿 chromium / 残留 http.server自动清理
图 2 · 方案时序:方案 A 串行排队 + 方案 B 统一调度与自动清理

3.3 实施步骤

阶段 1(立即,0 代码):运维纪律
  • Playwright 测试统一 workers=1 串行
  • 每项目固定端口 + 启动前 lsof 检查
  • 每日清理残留(手动或简单 cron)
阶段 2(源码层,1-2 小时):方案 A
  • 改 browser_tool.py 加文件锁
  • 配置项 browser.max_concurrent_browsers(默认 1)
  • 回归测试:并发调用验证排队
阶段 3(包装命令,2-3 小时):方案 B
  • pw-schedule Python CLI
  • 全局注册表 + 端口池 + 清理器
  • 接入各项目测试命令(建议在 CLAUDE.md 中声明)

3.4 验收标准

#验收用例方案
T1两个会话同时调 browser_navigate,第二个排队而非立刻失败A
T2排队超 300s 返回「浏览器繁忙」明确错误A
T3会话结束后锁释放,下一个会话立即可用A
T4两个项目同时起 http.server,端口自动错开不串台B
T5进程崩溃后残留 chromium 可被 clean 清理B
T6status 命令实时反映端口/进程占用B
T7非浏览器操作(终端命令)不受锁影响A

四、风险与对策

风险对策
全局互斥导致浏览器任务串行变慢2 核机器上串行是唯一正确选择;单会话多页面不受影响
文件锁在进程崩溃时未释放flock 由内核自动释放(进程退出即解锁),天然安全
排队 300s 太久可配置;默认值兼顾「等一会 vs 别无限等」
历史项目不记得用 pw-schedule提供 wrapper 兼容(pw-schedule test 内直接透传原参数);CLAUDE.md 声明

五、附录:关键日志证据

# 8/4 18:22:47 三子会话并发,浏览器 eval 超时
2026-08-04 18:22:47,278 WARNING [20260804_182137_cc878b] tools.browser_tool: browser 'eval' timed out after 30s (task=sa-1-01455396, socket_dir=/tmp/agent-browser-h_22f115714e)

# 8/2 充电宝会话:8099 端口被 4 个残留 http.server 轮番占用
"8099 端口被 4 个残留的旧 http.server 进程轮番占用,导致 Playwright 测试大量失败(访问到的是旧项目的「微信小程序」页面而不是我们的报告)"

# 用户现场描述
2026-08-04 18:42:32 [20260804_005024_310376fe] msg='[grootwu] 应该是被抢占用来做其他任务了'

# 并发窗口:8/4 18:21-18:24 三个会话同时使用 browser
2026-08-04 18:21 20260804_182137_365b5c
2026-08-04 18:21 20260804_182137_7705a7
2026-08-04 18:21 20260804_182137_cc878b

六、结论

Playwright 并发打架的根因是资源无调度:2 核 3.6G 的机器上,多个会话独立启动 Chromium/测试/本地服务,互不感知、无排队、无端口分配、无残留清理。方案 A(源码层全局浏览器锁)+ 方案 B(pw-schedule 统一调度)双管齐下:A 让浏览器会话串行排队不互相饿死,B 让端口统一分配不串台、残留自动清理。实施分三阶段,从零代码运维纪律到源码改造,每阶段有明确验收标准,可逐步落地。