上一篇 下一篇 分享链接 返回 返回顶部

AI 浏览器并发扛不住?一套从部署到压测的实战方案

发布人:慈云数据-客服中心 发布时间:6小时前 阅读量:1

AI浏览器 高并发解决方案|附完整命令

在 AI Agent、RPA 自动化、网页数据采集、智能客服、自动化测试等场景中,“AI 浏览器”正在成为一个非常核心的基础设施组件。所谓 AI 浏览器,通常指的是由大模型或自动化程序驱动的浏览器环境,例如通过 Playwright、Puppeteer、Selenium、Chrome DevTools Protocol 等方式控制 Chromium 浏览器,实现网页访问、登录、点击、截图、解析页面、执行任务等能力。

但是,当业务从单任务、低频调用发展到多用户、多任务、高并发时,浏览器自动化很快会遇到性能瓶颈:CPU 飙高、内存爆炸、浏览器进程僵死、页面超时、任务堆积、IP 被封、容器频繁重启,甚至整台机器被拖垮。

本文将系统介绍一套可落地的 AI 浏览器高并发解决方案,并附带从本地部署、Docker 部署、Nginx 反向代理、任务队列、系统调优到压力测试的完整命令。


一、AI 浏览器高并发的核心问题

在普通 Web 服务中,一个接口请求可能只需要几十毫秒到几百毫秒。但 AI 浏览器任务不同,它通常涉及以下操作:

  1. 启动或连接浏览器实例;
  2. 创建浏览器上下文;
  3. 打开页面;
  4. 加载 HTML、CSS、JS、图片、字体等资源;
  5. 执行页面脚本;
  6. 模拟用户点击、输入、滚动;
  7. 等待接口返回或页面渲染;
  8. 提取内容、截图或生成结构化数据;
  9. 关闭页面或释放上下文。

这些操作对 CPU、内存、网络、磁盘 I/O 都有明显压力。

尤其在高并发情况下,常见问题包括:

  • 单机同时启动过多 Chromium 进程;
  • 每个任务都新建浏览器,启动成本过高;
  • 页面资源加载过多,浪费带宽和 CPU;
  • 没有任务队列,瞬时流量直接打爆浏览器集群;
  • 没有限流和超时机制,僵尸任务长期占用资源;
  • 浏览器上下文复用不合理,导致 Cookie、缓存、会话污染;
  • 缺少监控,无法定位 CPU、内存、页面耗时和失败率问题。

因此,高并发 AI 浏览器的设计重点不是“无限启动浏览器”,而是:

用有限、可控、可观测的浏览器资源,稳定处理大量自动化任务。


二、推荐总体架构

一套较稳定的 AI 浏览器高并发架构可以分为以下几层:

客户端 / AI Agent / 业务系统
        |
        v
API 网关 / Nginx / 负载均衡
        |
        v
任务服务 API
        |
        v
Redis / RabbitMQ / Kafka 任务队列
        |
        v
Worker 集群
        |
        v
Browserless / Playwright Server / Chromium 池
        |
        v
目标网站

核心思想

  1. 浏览器服务池化
    不建议每个请求都启动一个完整浏览器。可以通过 Browserless、Playwright Server 或自研浏览器池管理浏览器实例。

  2. 任务队列削峰填谷
    前端请求先进入队列,Worker 按机器资源能力消费任务,避免突发流量打爆系统。

  3. 并发上限控制
    每台机器根据 CPU、内存设置最大并发浏览器上下文数量。

  4. 页面资源拦截
    对不必要的图片、字体、视频、广告脚本进行拦截,大幅降低资源消耗。

  5. 超时与失败重试
    每个任务必须设置最大执行时间,失败后可按策略重试。

  6. 监控与自动扩容
    通过 Prometheus、Grafana、Docker Stats 等方式监控资源,根据队列长度进行扩容。


三、服务器基础环境准备

以下示例以 Ubuntu 22.04 为例。

1. 更新系统

sudo apt update && sudo apt upgrade -y

2. 安装基础工具

sudo apt install -y \
  curl \
  wget \
  git \
  vim \
  htop \
  unzip \
  ca-certificates \
  gnupg \
  lsb-release \
  software-properties-common

3. 查看系统资源

lscpu
free -h
df -h
ulimit -n

高并发浏览器服务对文件描述符要求较高,建议将 ulimit -n 提高。

echo "* soft nofile 1048576" | sudo tee -a /etc/security/limits.conf
echo "* hard nofile 1048576" | sudo tee -a /etc/security/limits.conf
echo "root soft nofile 1048576" | sudo tee -a /etc/security/limits.conf
echo "root hard nofile 1048576" | sudo tee -a /etc/security/limits.conf

修改 systemd 限制:

sudo mkdir -p /etc/systemd/system.conf.d
cat <

重载配置:

sudo systemctl daemon-reexec

四、安装 Docker 与 Docker Compose

1. 安装 Docker

curl -fsSL https://get.docker.com | sudo bash

2. 设置 Docker 开机自启

sudo systemctl enable docker
sudo systemctl start docker

3. 将当前用户加入 docker 组

sudo usermod -aG docker $USER

重新登录后验证:

docker version

4. 安装 Docker Compose 插件

sudo apt install -y docker-compose-plugin
docker compose version

五、部署 Browserless 浏览器服务

Browserless 是一个常用的无头 Chrome 服务,提供 WebSocket 接口,支持 Puppeteer、Playwright 等客户端连接,非常适合做浏览器池化。

1. 创建项目目录

mkdir -p ~/ai-browser-cluster
cd ~/ai-browser-cluster

2. 创建 Docker Compose 文件

cat > docker-compose.yml <<'EOF'
services:
  browserless:
    image: ghcr.io/browserless/chromium:latest
    container_name: browserless
    restart: always
    shm_size: "2gb"
    ports:
      - "3000:3000"
    environment:
      - TOKEN=your_secure_token
      - CONCURRENT=10
      - QUEUED=100
      - TIMEOUT=120000
      - CORS=true
      - HEALTH=true
      - METRICS=true
      - PREBOOT_CHROME=true
      - KEEP_ALIVE=true
    deploy:
      resources:
        limits:
          cpus: "8"
          memory: 12G
    security_opt:
      - seccomp=unconfined
    cap_add:
      - SYS_ADMIN
EOF

参数说明:

参数 说明
CONCURRENT=10 同时处理的浏览器任务数
QUEUED=100 最大排队任务数
TIMEOUT=120000 单任务最大执行时间,单位毫秒
PREBOOT_CHROME=true 预启动 Chrome,降低冷启动耗时
KEEP_ALIVE=true 保持浏览器连接,提高复用能力
shm_size=2gb 增大共享内存,减少 Chrome 崩溃

3. 启动服务

docker compose up -d

4. 查看容器状态

docker ps
docker logs -f browserless

5. 测试健康状态

curl "http://127.0.0.1:3000/health?token=your_secure_token"

如果返回正常,说明 Browserless 已经部署成功。


六、使用 Playwright 连接 Browserless

下面示例使用 Node.js + Playwright 编写 Worker,连接远程 Browserless 执行页面访问任务。

1. 安装 Node.js

建议使用 Node.js 20 LTS。

curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
node -v
npm -v

2. 创建 Worker 项目

mkdir -p ~/ai-browser-worker
cd ~/ai-browser-worker
npm init -y
npm install playwright ioredis express pino

3. 创建基础浏览器调用脚本

cat > test-browserless.js <<'EOF'
const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.connectOverCDP(
    'ws://127.0.0.1:3000?token=your_secure_token'
  );

  const context = await browser.newContext({
    viewport: { width: 1280, height: 800 },
    userAgent:
      'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36'
  });

  const page = await context.newPage();

  await page.route('**/*', route => {
    const type = route.request().resourceType();
    if (['image', 'media', 'font'].includes(type)) {
      return route.abort();
    }
    return route.continue();
  });

  await page.goto('https://example.com', {
    waitUntil: 'domcontentloaded',
    timeout: 30000
  });

  const title = await page.title();
  const text = await page.locator('body').innerText();

  console.log({
    title,
    text: text.slice(0, 200)
  });

  await context.close();
  await browser.close();
})();
EOF

运行测试:

node test-browserless.js

七、引入 Redis 队列实现削峰填谷

高并发场景下,不能让所有请求直接操作浏览器,必须使用队列。

1. 启动 Redis

可以将 Redis 加入前面的 docker-compose.yml

cd ~/ai-browser-cluster
cat > docker-compose.yml <<'EOF'
services:
  redis:
    image: redis:7-alpine
    container_name: ai-browser-redis
    restart: always
    ports:
      - "6379:6379"
    command: redis-server --appendonly yes --maxmemory 1gb --maxmemory-policy allkeys-lru
    volumes:
      - ./redis-data:/data

  browserless:
    image: ghcr.io/browserless/chromium:latest
    container_name: browserless
    restart: always
    shm_size: "2gb"
    ports:
      - "3000:3000"
    environment:
      - TOKEN=your_secure_token
      - CONCURRENT=10
      - QUEUED=100
      - TIMEOUT=120000
      - CORS=true
      - HEALTH=true
      - METRICS=true
      - PREBOOT_CHROME=true
      - KEEP_ALIVE=true
    security_opt:
      - seccomp=unconfined
    cap_add:
      - SYS_ADMIN
EOF

重新启动:

docker compose down
docker compose up -d

验证 Redis:

docker exec -it ai-browser-redis redis-cli ping

返回:

PONG

八、编写任务提交 API

这个 API 负责接收任务,并写入 Redis 队列。

1. 创建 server.js

cd ~/ai-browser-worker
cat > server.js <<'EOF'
const express = require('express');
const Redis = require('ioredis');
const crypto = require('crypto');

const app = express();
const redis = new Redis('redis://127.0.0.1:6379');

app.use(express.json({ limit: '1mb' }));

app.post('/tasks', async (req, res) => {
  const { url } = req.body;

  if (!url || !/^https?:\/\//.test(url)) {
    return res.status(400).json({ error: 'invalid url' });
  }

  const id = crypto.randomUUID();

  const task = {
    id,
    url,
    status: 'queued',
    createdAt: Date.now()
  };

  await redis.set(`task:${id}`, JSON.stringify(task), 'EX', 3600);
  await redis.lpush('browser_tasks', JSON.stringify(task));

  res.json({
    id,
    status: 'queued'
  });
});

app.get('/tasks/:id', async (req, res) => {
  const data = await redis.get(`task:${req.params.id}`);

  if (!data) {
    return res.status(404).json({ error: 'task not found' });
  }

  res.json(JSON.parse(data));
});

app.get('/health', (req, res) => {
  res.json({ status: 'ok' });
});

app.listen(8080, () => {
  console.log('API server listening on http://0.0.0.0:8080');
});
EOF

2. 启动 API

node server.js

3. 提交任务测试

curl -X POST http://127.0.0.1:8080/tasks \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com"}'

九、编写 Worker 消费任务

Worker 负责从 Redis 队列中取任务,调用浏览器服务执行任务,并将结果写回 Redis。

1. 创建 worker.js

cd ~/ai-browser-worker
cat > worker.js <<'EOF'
const { chromium } = require('playwright');
const Redis = require('ioredis');
const pino = require('pino');

const redis = new Redis('redis://127.0.0.1:6379');
const logger = pino();

const BROWSER_WS = 'ws://127.0.0.1:3000?token=your_secure_token';
const MAX_TASK_TIME = 60000;

async function runWithTimeout(promise, ms) {
  let timer;
  const timeout = new Promise((_, reject) => {
    timer = setTimeout(() => reject(new Error('task timeout')), ms);
  });

  try {
    return await Promise.race([promise, timeout]);
  } finally {
    clearTimeout(timer);
  }
}

async function handleTask(task) {
  const startedAt = Date.now();

  let browser;
  let context;

  try {
    await redis.set(
      `task:${task.id}`,
      JSON.stringify({ ...task, status: 'running', startedAt }),
      'EX',
      3600
    );

    browser = await chromium.connectOverCDP(BROWSER_WS);

    context = await browser.newContext({
      viewport: { width: 1280, height: 800 },
      ignoreHTTPSErrors: true
    });

    const page = await context.newPage();

    await page.route('**/*', route => {
      const request = route.request();
      const type = request.resourceType();
      const url = request.url();

      if (['image', 'media', 'font'].includes(type)) {
        return route.abort();
      }

      if (
        url.includes('google-analytics') ||
        url.includes('doubleclick') ||
        url.includes('facebook') ||
        url.includes('ads')
      ) {
        return route.abort();
      }

      return route.continue();
    });

    await page.goto(task.url, {
      waitUntil: 'domcontentloaded',
      timeout: 30000
    });

    await page.waitForTimeout(1000);

    const title = await page.title();
    const bodyText = await page.locator('body').innerText({ timeout: 10000 });

    const result = {
      id: task.id,
      url: task.url,
      status: 'success',
      title,
      text: bodyText.slice(0, 5000),
      costMs: Date.now() - startedAt,
      finishedAt: Date.now()
    };

    await redis.set(`task:${task.id}`, JSON.stringify(result), 'EX', 3600);

    logger.info({ id: task.id, costMs: result.costMs }, 'task success');
  } catch (err) {
    const failed = {
      ...task,
      status: 'failed',
      error: err.message,
      costMs: Date.now() - startedAt,
      finishedAt: Date.now()
    };

    await redis.set(`task:${task.id}`, JSON.stringify(failed), 'EX', 3600);

    logger.error({ id: task.id, error: err.message }, 'task failed');
  } finally {
    if (context) {
      await context.close().catch(() => {});
    }

    if (browser) {
      await browser.close().catch(() => {});
    }
  }
}

async function main() {
  logger.info('worker started');

  while (true) {
    const item = await redis.brpop('browser_tasks', 5);

    if (!item) {
      continue;
    }

    const task = JSON.parse(item[1]);

    await runWithTimeout(handleTask(task), MAX_TASK_TIME).catch(async err => {
      logger.error({ id: task.id, error: err.message }, 'task timeout or fatal error');
      await redis.set(
        `task:${task.id}`,
        JSON.stringify({
          ...task,
          status: 'failed',
          error: err.message,
          finishedAt: Date.now()
        }),
        'EX',
        3600
      );
    });
  }
}

main().catch(err => {
  logger.error(err);
  process.exit(1);
});
EOF

2. 启动 Worker

node worker.js

十、使用 PM2 管理 API 与 Worker

生产环境不建议直接用 node server.js,可以使用 PM2 管理进程。

1. 安装 PM2

sudo npm install -g pm2

2. 启动 API

cd ~/ai-browser-worker
pm2 start server.js --name ai-browser-api

3. 启动多个 Worker

假设当前机器 Browserless 并发设置为 10,可以启动 10 个左右 Worker,但实际要根据任务耗时和机器资源调整。

pm2 start worker.js --name ai-browser-worker -i 10

4. 查看状态

pm2 status
pm2 logs

5. 设置开机自启

pm2 startup
pm2 save

十一、Nginx 反向代理与限流

对外提供 API 时,建议加 Nginx 反向代理,并设置限流、超时和请求体大小。

1. 安装 Nginx

sudo apt install -y nginx

2. 创建站点配置

sudo tee /etc/nginx/sites-available/ai-browser <<'EOF'
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=20r/s;

server {
    listen 80;
    server_name your-domain.com;

    client_max_body_size 2m;

    location / {
        limit_req zone=api_limit burst=50 nodelay;

        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_connect_timeout 10s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}
EOF

3. 启用站点

sudo ln -s /etc/nginx/sites-available/ai-browser /etc/nginx/sites-enabled/ai-browser
sudo nginx -t
sudo systemctl reload nginx

十二、系统内核参数优化

浏览器高并发会产生大量网络连接和进程资源占用,可以适当调整内核参数。

sudo tee /etc/sysctl.d/99-ai-browser.conf <<'EOF'
fs.file-max = 2097152
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65535
net.ipv4.ip_local_port_range = 10000 65000
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_keepalive_time = 600
vm.swappiness = 10
EOF

应用配置:

sudo sysctl --system

查看是否生效:

sysctl fs.file-max
sysctl net.core.somaxconn
sysctl net.ipv4.ip_local_port_range

十三、浏览器启动参数优化

如果你不是使用 Browserless,而是自己启动 Chromium,可以使用以下参数降低资源消耗:

chromium-browser \
  --headless=new \
  --disable-gpu \
  --disable-dev-shm-usage \
  --disable-background-networking \
  --disable-background-timer-throttling \
  --disable-backgrounding-occluded-windows \
  --disable-breakpad \
  --disable-client-side-phishing-detection \
  --disable-component-update \
  --disable-default-apps \
  --disable-extensions \
  --disable-features=Translate,BackForwardCache,AcceptCHFrame \
  --disable-hang-monitor \
  --disable-popup-blocking \
  --disable-prompt-on-repost \
  --disable-renderer-backgrounding \
  --disable-sync \
  --metrics-recording-only \
  --mute-audio \
  --no-first-run \
  --no-default-browser-check \
  --no-sandbox \
  --password-store=basic \
  --use-mock-keychain

在 Playwright 中可这样配置:

const browser = await chromium.launch({
  headless: true,
  args: [
    '--disable-gpu',
    '--disable-dev-shm-usage',
    '--disable-background-networking',
    '--disable-extensions',
    '--disable-sync',
    '--mute-audio',
    '--no-first-run',
    '--no-default-browser-check',
    '--disable-features=Translate,BackForwardCache',
  ]
});

需要注意的是,--no-sandbox 虽然可以减少容器权限问题,但会降低安全性。生产环境如果能正确配置 sandbox,尽量不要关闭。


十四、并发容量如何估算

AI 浏览器并发不是越高越好,必须结合机器资源计算。

一个普通网页页面上下文通常消耗:

  • CPU:瞬时 0.2 到 1 核不等;
  • 内存:100MB 到 500MB 不等;
  • 复杂 SPA 页面可能超过 1GB;
  • 截图、PDF、视频页面会更重。

假设一台服务器配置为:

8 核 CPU
16GB 内存

系统和 Redis、API、Nginx 预留 3GB,剩余约 13GB。若单浏览器任务平均消耗 400MB,则理论内存可支持:

13GB / 400MB ≈ 32

但 CPU 和页面复杂度会成为瓶颈,因此建议实际并发设置为:

8 核机器:Browserless CONCURRENT=8~16
16 核机器:Browserless CONCURRENT=16~32
32 核机器:Browserless CONCURRENT=32~64

如果目标网页较重,应进一步降低并发。


十五、压测命令

可以使用 autocannon 对任务提交 API 进行压力测试。

1. 安装 autocannon

sudo npm install -g autocannon

2. 压测任务提交接口

autocannon -c 50 -d 30 -m POST \
  -H "Content-Type: application/json" \
  -b '{"url":"https://example.com"}' \
  http://127.0.0.1:8080/tasks

参数说明:

参数 说明
-c 50 50 个并发连接
-d 30 持续 30 秒
-m POST 使用 POST 方法
-b 请求体

3. 查看队列长度

docker exec -it ai-browser-redis redis-cli llen browser_tasks

4. 查看任务状态

curl http://127.0.0.1:8080/tasks/任务ID

5. 查看系统资源

htop
docker stats
pm2 monit

十六、横向扩展方案

当单机资源不足时,应进行横向扩展,而不是继续提高单机并发。

常见方式:

Nginx
  |
  |-- API Server 1
  |-- API Server 2
  |-- API Server 3
        |
        v
      Redis
        |
        |-- Worker 节点 1 + Browserless
        |-- Worker 节点 2 + Browserless
        |-- Worker 节点 3 + Browserless

多节点部署建议

  1. API 服务无状态化;
  2. Redis 单独部署或使用云 Redis;
  3. Worker 可在多台机器同时运行;
  4. 每台 Worker 机器本地部署 Browserless;
  5. Worker 连接本机 Browserless,减少网络延迟;
  6. 使用统一队列分发任务;
  7. 通过队列长度和任务耗时决定扩容数量。

Worker 中的 Browserless 地址可以改为环境变量:

export BROWSER_WS="ws://127.0.0.1:3000?token=your_secure_token"

Node.js 代码中读取:

const BROWSER_WS = process.env.BROWSER_WS;

PM2 启动时传入:

BROWSER_WS="ws://127.0.0.1:3000?token=your_secure_token" \
pm2 start worker.js --name ai-browser-worker -i 10

十七、生产环境关键优化清单

1. 必须设置任务超时

任何浏览器任务都必须有最大执行时间,例如 30 秒、60 秒或 120 秒。否则页面卡死会导致资源一直无法释放。

2. 必须释放资源

每个任务结束后必须执行:

await context.close();
await browser.close();

如果使用的是浏览器池,需要将实例归还池中,而不是直接销毁。

3. 拦截无用资源

建议默认拦截:

image
media
font
analytics
ads
tracking script

这可以显著降低页面加载成本。

4. 控制最大并发

不要让 Worker 数量大于浏览器服务可承载数量太多。否则会出现大量连接排队和超时。

5. 任务结果设置过期时间

Redis 中的任务结果应设置 TTL,避免无限增长:

EX 3600

6. 对外接口必须限流

使用 Nginx、API 网关或应用层限流,避免恶意请求拖垮系统。

7. 做好日志追踪

每个任务都应该有唯一 ID,日志中记录:

  • taskId;
  • URL;
  • 开始时间;
  • 结束时间;
  • 耗时;
  • 成功或失败;
  • 错误信息。

十八、常见故障排查命令

1. 查看 Browserless 日志

docker logs -f browserless

2. 查看 Redis 日志

docker logs -f ai-browser-redis

3. 查看容器资源占用

docker stats

4. 查看端口占用

sudo ss -lntp

5. 查看系统负载

uptime
top
htop

6. 查看内存

free -h

7. 查看磁盘

df -h
du -sh *

8. 清理 Docker 无用资源

docker system prune -af

如果 Redis 数据也可以清理:

docker compose down -v

十九、推荐的生产参数

对于一台 8 核 16GB 机器,可以从以下配置开始:

environment:
  - CONCURRENT=10
  - QUEUED=100
  - TIMEOUT=120000
  - PREBOOT_CHROME=true
  - KEEP_ALIVE=true

PM2 Worker:

pm2 start worker.js --name ai-browser-worker -i 10

Nginx 限流:

limit_req_zone $binary_remote_addr zone=api_limit:10m rate=20r/s;

任务超时:

const MAX_TASK_TIME = 60000;

如果发现 CPU 长期超过 85%,应降低并发;如果 CPU 较低但队列很长,可以适当增加 Worker 和 Browserless 并发。


二十、总结

AI 浏览器高并发的关键,不是简单地启动更多浏览器,而是构建一套稳定的资源调度体系。推荐方案是:

  1. 使用 Browserless 或 Playwright Server 做浏览器池化;
  2. 使用 Redis 队列削峰填谷;
  3. Worker 按资源能力消费任务;
  4. Nginx 做反向代理和限流;
  5. 浏览器任务设置超时、重试和资源释放;
  6. 拦截图片、字体、视频和广告资源;
  7. 通过 Docker、PM2、监控工具管理服务;
  8. 当单机不足时,通过多 Worker 节点横向扩展。

在真实生产环境中,AI 浏览器服务的稳定性往往取决于细节:并发上限是否合理、任务是否可控、失败是否可恢复、资源是否及时释放、队列是否可观测。只要按照上述架构逐步搭建,即可从单机十几个并发平稳扩展到多节点数百甚至上千级任务处理能力。

目录结构
全文