别再迷信并发数了,Playwright 跑 50 个并发全是坑

数据分析师Leo 专家 7小时前 728 浏览 11 点赞 约 2 分钟

最近带团队在折腾自动化 Agent 的工作流,原本以为把 Playwright 的并发开到 50 个,吞吐量能直接起飞,结果现实给了我们一记响亮的耳光。

实际跑起来发现,高并发不仅没提速,反而直接把目标网站的验证码(CAPTCHA)给刷爆了。更惨的是,由于 CPU 和内存瞬间被撑爆,整个流水线陷入了“失败-重试-再失败”的死亡螺旋。这种感觉就像是你为了快点干完活,同时雇了 50 个人在一个狭小的办公室里乱撞,最后不仅活没干成,连办公室都给拆了。

其实很多时候,报错日志里显示的“目标网站反爬”或者“频率限制”,本质上是我们自己并发模型设计得太烂。

为什么并发越高反而越慢?

在做大规模浏览器自动化时,高并发会带来几个致命的连锁反应:

  • 资源挤兑: 每个 Playwright Context 都要占用大量的内存和 CPU 来处理 DOM 渲染和 JS 执行,50 个并发下去,系统响应速度会直线下降。
  • 行为特征太明显: 短时间内对同一个域名发起几十个请求,这种极其不自然的流量模式在反爬引擎眼里就是标准的 Bot。
  • 重试放大效应: 一个 Worker 遇到验证码失败了,立刻发起重试;结果其他几十个 Worker 也跟着撞墙,这种连锁反应会迅速拖垮整个 Agent 链路,导致下游的大模型拿到一堆重复或者错误的脏数据。

真正的解决思路:限流、队列与退避机制

我总结了一套实战经验:与其追求 50 个并发乱撞,不如把并发控制在 5 到 10 个,剩下的任务全部进队列。

我们需要建立一套“并发上限 + 溢出队列 + 指数退避”的架构。核心逻辑如下:

  • 设置并发上限: 严格限制同时活跃的浏览器会话数量。
  • 引入队列机制: 任务先进入 Redis 或内存队列,而不是直接开浏览器。
  • 域名频率控制: 同一个域名之间要有一定的请求间隔(比如 1-3 秒)。
  • 有限重试策略: 设置最大重试次数(建议不超过 3 次),并且必须使用指数退避(Exponential Backoff),别一失败就立刻重试。

下面是我在 Node.js 环境下封装的一个简单的浏览器池逻辑,大家可以参考这个思路去改写自己的工作流:

class BrowserPool {
  constructor(maxActive = 5, maxQueued = 10) {
    this.maxActive = maxActive;
    this.maxQueued = maxQueued;
    this.active = new Set();
    this.queue = [];
    this.domainLastHit = new Map();
  }

  async acquire(domain, task) {
    if (this.queue.length >= this.maxQueued) {
      throw new Error('Queue full');
    }

    // 等待可用槽位
    while (this.active.size >= this.maxActive) {
      await new Promise(resolve => setTimeout(resolve, 100));
    }

    // 针对域名的频率控制
    const lastHit = this.domainLastHit.get(domain) || 0;
    const elapsed = Date.now() - lastHit;
    if (elapsed < 2000) {
      await new Promise(resolve => setTimeout(resolve, 2000 - elapsed));
    }

    const session = { id: crypto.randomUUID(), domain, task };
    this.active.add(session);
    this.domainLastHit.set(domain, Date.now());
    return session;
  }

  release(session) {
    this.active.delete(session);
  }

  async execute(domain, task, retries = 3) {
    let attempt = 0;
    while (attempt < retries) {
      const session = await this.acquire(domain, task);
      try {
        // 这里执行具体的 Playwright 操作
        // const result = await performTask(session);
        // return result;
        return "success"; 
      } catch (err) {
        this.release(session);
        attempt++;
        if (attempt >= retries) throw err;
        // 指数退避:等待时间随重试次数增加
        await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
      }
    }
  }
}

如果你是在分布式环境或者多容器部署,建议直接用 Redis 来做这个队列管理,这样能保证多个 Agent 节点之间不会因为抢占同一个域名而互相踩坑。

总之,做自动化 Agent 别只盯着吞吐量看,稳定性才是落地后的第一命脉。

工作流AI落地playwrightRedisNode.js

全部回复 (3)

阿Sam的日常 高级 7小时前
还有个坑就是网络带宽,并发一高,网络层直接堵死,延迟高得离谱。
0 回复
全栈小李 高级 7小时前
确实,我之前直接用headless模式也救不了,最后还是得靠调低并发配合代理IP轮询。
0 回复
大Max爱学习 初级 7小时前
我之前也试过,结果全是僵尸进程,最后只能手动kill掉,还是得稳着点跑。
0 回复

发表回复

支持 Markdown 格式