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

AI浏览器并发扛不住?从浏览器池到K8s扩容的完整落地方案

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

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

在大模型应用快速落地的过程中,“AI浏览器”逐渐成为一个非常常见的基础能力。所谓 AI 浏览器,通常不是指普通用户使用的 Chrome、Edge,而是指由程序控制的浏览器运行环境,例如通过 Playwright、Puppeteer、Selenium 等工具,让浏览器自动访问网页、登录系统、抓取内容、执行网页操作、截图、生成 PDF,甚至配合大模型完成网页理解、表单填写、流程自动化等任务。

但是,一旦 AI 浏览器从单机测试进入生产环境,高并发问题就会迅速暴露出来。比如:

  • 同时有 100 个用户请求网页截图;
  • 同时有 500 个任务需要打开网页并提取正文;
  • AI Agent 需要并发操作多个 SaaS 系统;
  • 企业内部系统需要批量自动化测试或数据采集;
  • 浏览器实例频繁创建和销毁导致 CPU、内存飙升;
  • 页面加载慢、任务超时、浏览器崩溃、队列堆积。

本文将从架构设计、浏览器池、任务队列、容器化部署、反向代理、限流、监控、Kubernetes 扩容等角度,完整介绍一套 AI 浏览器高并发解决方案,并附上可直接参考的完整命令。


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

AI 浏览器的高并发,本质上不是简单地“多开几个 Chrome”就能解决。浏览器是非常重的运行时,一个 Chromium 实例可能消耗数百 MB 内存,如果每个请求都启动一个新的浏览器,很快就会出现资源耗尽。

常见问题包括:

1. 浏览器启动成本高

以 Playwright 为例,启动 Chromium 通常需要几百毫秒到数秒不等。如果每个请求都执行:

const browser = await chromium.launch()

那么在高并发场景下,服务器会不断创建浏览器进程,导致 CPU 和内存持续抖动。

2. 页面上下文隔离不足

如果多个任务共用同一个页面,可能会出现 Cookie、LocalStorage、SessionStorage、登录态、页面状态互相污染的问题。因此不能简单地让所有请求共用一个 Page。

3. 内存泄漏与进程僵死

浏览器自动化任务很容易因为页面异常、脚本超时、网络阻塞等原因导致资源没有释放。如果 Page、Context 或 Browser 没有正确关闭,系统会逐步积累僵尸进程。

4. 并发不可控

如果外部请求直接打到浏览器服务,例如同时来了 1000 个请求,服务端如果不做限流和排队,就会瞬间压垮机器。

5. 任务耗时长

AI 浏览器任务通常不是毫秒级接口,而是秒级甚至分钟级任务。例如打开网页、等待渲染、识别页面、点击按钮、下载文件、截图等,都会占用浏览器资源。


二、推荐总体架构

一套适合生产环境的 AI 浏览器高并发架构,建议采用以下模式:

用户请求
   │
   ▼
API 网关 / Nginx
   │
   ▼
业务 API 服务
   │
   ▼
Redis / Kafka / RabbitMQ 任务队列
   │
   ▼
Browser Worker 集群
   │
   ▼
浏览器池 / Context 池 / Page 池
   │
   ▼
结果存储:Redis / MySQL / PostgreSQL / S3 / MinIO

该架构的关键点是:

  1. API 服务不直接执行浏览器任务,而是把任务放入队列;
  2. Browser Worker 从队列中消费任务;
  3. Worker 内部维护浏览器池,复用 Browser 实例;
  4. 每个任务使用独立 BrowserContext,保证隔离;
  5. 通过 Redis 或数据库存储任务状态;
  6. 通过容器或 Kubernetes 横向扩容 Worker;
  7. 使用 Nginx、队列长度、Worker 数量实现削峰填谷。

三、单机高并发基础方案:Playwright + 浏览器池

如果并发量不是特别高,例如单机 10~100 并发,可以先使用浏览器池方案。

1. 安装 Node.js 环境

以 Ubuntu 为例:

sudo apt update
sudo apt install -y curl ca-certificates gnupg

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

node -v
npm -v

2. 创建项目

mkdir ai-browser-server
cd ai-browser-server

npm init -y
npm install express playwright p-limit uuid
npx playwright install --with-deps chromium

3. 编写浏览器池服务

创建文件:

mkdir src
vim src/server.js

写入以下代码:

const express = require('express')
const { chromium } = require('playwright')
const pLimit = require('p-limit')
const { v4: uuidv4 } = require('uuid')

const app = express()
app.use(express.json({ limit: '2mb' }))

const PORT = process.env.PORT || 3000
const MAX_CONCURRENCY = Number(process.env.MAX_CONCURRENCY || 10)

let browser
const limit = pLimit(MAX_CONCURRENCY)

async function initBrowser() {
  browser = await chromium.launch({
    headless: true,
    args: [
      '--disable-gpu',
      '--disable-dev-shm-usage',
      '--no-sandbox',
      '--disable-setuid-sandbox',
      '--disable-background-networking',
      '--disable-default-apps',
      '--disable-extensions',
      '--disable-sync',
      '--metrics-recording-only',
      '--mute-audio',
      '--no-first-run'
    ]
  })

  browser.on('disconnected', async () => {
    console.error('browser disconnected, restarting...')
    await initBrowser()
  })
}

async function runTask(task) {
  const context = await browser.newContext({
    viewport: {
      width: task.width || 1280,
      height: task.height || 800
    },
    userAgent: task.userAgent
  })

  const page = await context.newPage()

  try {
    page.setDefaultTimeout(task.timeout || 30000)
    await page.goto(task.url, {
      waitUntil: task.waitUntil || 'networkidle',
      timeout: task.timeout || 30000
    })

    const title = await page.title()
    const screenshot = task.screenshot
      ? await page.screenshot({ fullPage: true, type: 'png' })
      : null

    return {
      id: uuidv4(),
      url: task.url,
      title,
      screenshotBase64: screenshot ? screenshot.toString('base64') : null
    }
  } finally {
    await page.close().catch(() => {})
    await context.close().catch(() => {})
  }
}

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

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

  try {
    const result = await limit(() => runTask(req.body))
    res.json(result)
  } catch (err) {
    console.error(err)
    res.status(500).json({ error: err.message })
  }
})

app.get('/health', (req, res) => {
  res.json({
    status: 'ok',
    maxConcurrency: MAX_CONCURRENCY,
    activeCount: limit.activeCount,
    pendingCount: limit.pendingCount
  })
})

initBrowser().then(() => {
  app.listen(PORT, () => {
    console.log(`AI browser server listening on ${PORT}`)
  })
})

4. 启动服务

PORT=3000 MAX_CONCURRENCY=10 node src/server.js

5. 测试请求

curl -X POST http://127.0.0.1:3000/render \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com",
    "screenshot": false,
    "timeout": 30000
  }'

如果需要截图:

curl -X POST http://127.0.0.1:3000/render \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com",
    "screenshot": true,
    "width": 1440,
    "height": 900
  }'

四、生产级方案:Redis任务队列削峰

单机浏览器池可以解决小规模并发,但如果任务量变大,建议引入任务队列。API 接收到请求后快速返回任务 ID,真正的浏览器执行由 Worker 异步完成。

1. 安装 Redis

sudo apt update
sudo apt install -y redis-server

sudo systemctl enable redis-server
sudo systemctl start redis-server

redis-cli ping

返回:

PONG

2. 安装队列依赖

npm install bullmq ioredis

3. 编写 API 服务

创建:

vim src/api.js

写入:

const express = require('express')
const { Queue } = require('bullmq')
const { v4: uuidv4 } = require('uuid')
const IORedis = require('ioredis')

const app = express()
app.use(express.json())

const connection = new IORedis({
  host: process.env.REDIS_HOST || '127.0.0.1',
  port: Number(process.env.REDIS_PORT || 6379),
  maxRetriesPerRequest: null
})

const queue = new Queue('browser-tasks', { connection })

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

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

  const taskId = uuidv4()

  await queue.add(
    'render',
    {
      taskId,
      ...req.body
    },
    {
      jobId: taskId,
      attempts: 2,
      backoff: {
        type: 'exponential',
        delay: 3000
      },
      removeOnComplete: 1000,
      removeOnFail: 5000
    }
  )

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

app.get('/tasks/:id', async (req, res) => {
  const job = await queue.getJob(req.params.id)

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

  const state = await job.getState()
  const result = job.returnvalue || null
  const failedReason = job.failedReason || null

  res.json({
    taskId: req.params.id,
    state,
    result,
    failedReason
  })
})

app.listen(3000, () => {
  console.log('API server listening on 3000')
})

4. 编写 Worker 服务

创建:

vim src/worker.js

写入:

const { Worker } = require('bullmq')
const IORedis = require('ioredis')
const { chromium } = require('playwright')

const connection = new IORedis({
  host: process.env.REDIS_HOST || '127.0.0.1',
  port: Number(process.env.REDIS_PORT || 6379),
  maxRetriesPerRequest: null
})

const WORKER_CONCURRENCY = Number(process.env.WORKER_CONCURRENCY || 5)

let browser

async function initBrowser() {
  browser = await chromium.launch({
    headless: true,
    args: [
      '--disable-gpu',
      '--disable-dev-shm-usage',
      '--no-sandbox',
      '--disable-setuid-sandbox',
      '--disable-extensions',
      '--disable-background-networking',
      '--mute-audio',
      '--no-first-run'
    ]
  })
}

async function render(job) {
  const task = job.data

  const context = await browser.newContext({
    viewport: {
      width: task.width || 1280,
      height: task.height || 800
    }
  })

  const page = await context.newPage()

  try {
    page.setDefaultTimeout(task.timeout || 30000)

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

    const title = await page.title()

    let html = null
    if (task.returnHtml) {
      html = await page.content()
    }

    let screenshotBase64 = null
    if (task.screenshot) {
      const buf = await page.screenshot({
        fullPage: true,
        type: 'png'
      })
      screenshotBase64 = buf.toString('base64')
    }

    return {
      taskId: task.taskId,
      url: task.url,
      title,
      html,
      screenshotBase64
    }
  } finally {
    await page.close().catch(() => {})
    await context.close().catch(() => {})
  }
}

async function main() {
  await initBrowser()

  const worker = new Worker(
    'browser-tasks',
    async job => {
      return await render(job)
    },
    {
      connection,
      concurrency: WORKER_CONCURRENCY
    }
  )

  worker.on('completed', job => {
    console.log(`job completed: ${job.id}`)
  })

  worker.on('failed', (job, err) => {
    console.error(`job failed: ${job?.id}`, err)
  })

  console.log(`Browser worker started, concurrency=${WORKER_CONCURRENCY}`)
}

main()

5. 启动 API 和 Worker

node src/api.js

另开一个终端:

WORKER_CONCURRENCY=5 node src/worker.js

如果需要启动多个 Worker:

WORKER_CONCURRENCY=5 node src/worker.js
WORKER_CONCURRENCY=5 node src/worker.js
WORKER_CONCURRENCY=5 node src/worker.js

每个 Worker 并发 5 个任务,3 个 Worker 总并发约为 15。


五、使用 PM2 管理多进程

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

1. 安装 PM2

npm install -g pm2

2. 启动 API

pm2 start src/api.js --name ai-browser-api

3. 启动多个 Worker

pm2 start src/worker.js --name ai-browser-worker-1 --env production -- WORKER_CONCURRENCY=5

更推荐使用环境变量方式:

WORKER_CONCURRENCY=5 pm2 start src/worker.js --name ai-browser-worker-1
WORKER_CONCURRENCY=5 pm2 start src/worker.js --name ai-browser-worker-2
WORKER_CONCURRENCY=5 pm2 start src/worker.js --name ai-browser-worker-3

4. 查看进程

pm2 list
pm2 logs
pm2 monit

5. 设置开机自启

pm2 save
pm2 startup

根据输出执行对应命令即可。


六、Docker容器化部署

容器化可以让 AI 浏览器 Worker 更容易扩容、迁移和部署。需要注意的是,Chromium 在容器中运行时要处理依赖、沙箱、共享内存等问题。

1. 创建 Dockerfile

vim Dockerfile

写入:

FROM mcr.microsoft.com/playwright:v1.48.0-jammy

WORKDIR /app

COPY package*.json ./
RUN npm install --production

COPY . .

ENV NODE_ENV=production

EXPOSE 3000

CMD ["node", "src/api.js"]

2. 创建 docker-compose.yml

vim docker-compose.yml

写入:

version: "3.9"

services:
  redis:
    image: redis:7
    container_name: ai-browser-redis
    restart: always
    ports:
      - "6379:6379"

  api:
    build: .
    container_name: ai-browser-api
    restart: always
    environment:
      REDIS_HOST: redis
      REDIS_PORT: 6379
    ports:
      - "3000:3000"
    depends_on:
      - redis
    command: ["node", "src/api.js"]

  worker:
    build: .
    restart: always
    shm_size: "1gb"
    environment:
      REDIS_HOST: redis
      REDIS_PORT: 6379
      WORKER_CONCURRENCY: 5
    depends_on:
      - redis
    command: ["node", "src/worker.js"]

3. 构建并启动

docker compose up -d --build

4. 查看服务

docker compose ps
docker compose logs -f api
docker compose logs -f worker

5. 横向扩容 Worker

docker compose up -d --scale worker=5

如果每个 Worker 并发为 5,那么 5 个 Worker 理论并发约为 25。实际并发能力需要结合 CPU、内存、页面复杂度综合评估。

6. 停止服务

docker compose down

七、Nginx反向代理与限流

为了保护后端服务,建议在 API 前增加 Nginx,统一做反向代理、超时控制、请求体大小限制和限流。

1. 安装 Nginx

sudo apt update
sudo apt install -y nginx

2. 创建配置

sudo vim /etc/nginx/conf.d/ai-browser.conf

写入:

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

server {
    listen 80;
    server_name ai-browser.example.com;

    client_max_body_size 5m;

    location / {
        limit_req zone=ai_browser_limit burst=20 nodelay;

        proxy_pass http://127.0.0.1:3000;
        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 5s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}

3. 测试配置并重载

sudo nginx -t
sudo systemctl reload nginx

八、性能压测命令

上线前必须压测,不能只凭感觉设置并发。

1. 安装 wrk

sudo apt update
sudo apt install -y wrk

2. 编写压测脚本

vim post.lua

写入:

wrk.method = "POST"
wrk.body = '{"url":"https://example.com","screenshot":false,"timeout":30000}'
wrk.headers["Content-Type"] = "application/json"

3. 执行压测

wrk -t4 -c50 -d60s -s post.lua http://127.0.0.1:3000/tasks

参数说明:

  • -t4:4 个线程;
  • -c50:50 个连接;
  • -d60s:持续 60 秒;
  • -s post.lua:使用 POST 请求脚本。

如果压测同步渲染接口:

wrk -t4 -c20 -d60s -s post.lua http://127.0.0.1:3000/render

同步渲染接口建议并发不要过高,因为它会直接占用浏览器资源。


九、并发参数如何设置

AI 浏览器高并发不是越大越好,关键是找到机器的资源平衡点。

可以参考以下经验值:

机器配置 建议 Worker 数 单 Worker 并发 总并发
2C4G 1 2~3 2~3
4C8G 2 3~5 6~10
8C16G 3~5 5~8 15~40
16C32G 5~10 5~10 25~100

如果任务需要截图、PDF、复杂页面渲染,建议降低并发。如果只是打开简单页面获取标题或 HTML,可以适当提高并发。

核心原则:

  1. CPU 使用率长期不要超过 80%;
  2. 内存使用率长期不要超过 75%;
  3. Redis 队列长度不能持续增长;
  4. 任务超时率应低于可接受阈值;
  5. 浏览器崩溃后必须自动恢复;
  6. 单任务必须设置超时时间;
  7. 每个任务结束后必须关闭 Context。

十、Kubernetes横向扩容方案

如果业务规模较大,推荐将 API、Worker、Redis 分别部署到 Kubernetes 中。Worker 是最适合横向扩容的部分。

1. 构建镜像

docker build -t registry.example.com/ai-browser:1.0.0 .
docker push registry.example.com/ai-browser:1.0.0

2. API Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ai-browser-api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: ai-browser-api
  template:
    metadata:
      labels:
        app: ai-browser-api
    spec:
      containers:
        - name: api
          image: registry.example.com/ai-browser:1.0.0
          command: ["node", "src/api.js"]
          ports:
            - containerPort: 3000
          env:
            - name: REDIS_HOST
              value: redis
            - name: REDIS_PORT
              value: "6379"

应用:

kubectl apply -f api-deployment.yaml

3. Worker Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ai-browser-worker
spec:
  replicas: 5
  selector:
    matchLabels:
      app: ai-browser-worker
  template:
    metadata:
      labels:
        app: ai-browser-worker
    spec:
      containers:
        - name: worker
          image: registry.example.com/ai-browser:1.0.0
          command: ["node", "src/worker.js"]
          env:
            - name: REDIS_HOST
              value: redis
            - name: REDIS_PORT
              value: "6379"
            - name: WORKER_CONCURRENCY
              value: "5"
          resources:
            requests:
              cpu: "500m"
              memory: "1Gi"
            limits:
              cpu: "2"
              memory: "3Gi"

应用:

kubectl apply -f worker-deployment.yaml

4. 扩容 Worker

kubectl scale deployment ai-browser-worker --replicas=10

5. 查看状态

kubectl get pods
kubectl logs -f deployment/ai-browser-worker
kubectl top pods

十一、稳定性优化建议

1. 每个任务使用独立 Context

不要让多个任务共享同一个 Page。推荐模式是:

const context = await browser.newContext()
const page = await context.newPage()

任务结束后:

await page.close()
await context.close()

这样既能复用 Browser,又能保证 Cookie 和缓存隔离。

2. 定期重启 Browser

长时间运行的 Chromium 可能因为页面异常、资源碎片、内存泄漏而变得不稳定。可以设置 Worker 每处理一定数量任务后重启浏览器。

示例策略:

每个 Worker 处理 500~2000 个任务后重启 Browser

3. 设置任务超时

所有浏览器任务都必须设置超时,否则一个页面卡住可能长期占用资源。

page.setDefaultTimeout(30000)
await page.goto(url, { timeout: 30000 })

4. 禁止访问内网地址

如果 AI 浏览器允许用户传入 URL,一定要防止 SSRF 攻击。需要禁止访问:

127.0.0.1
localhost
0.0.0.0
10.0.0.0/8
172.16.0.0/12
192.168.0.0/16
169.254.0.0/16

否则攻击者可能通过浏览器服务访问你的内网管理系统、云厂商元数据服务等。

5. 限制返回内容大小

截图、HTML、PDF 都可能非常大,不建议全部直接返回给 API。更好的方式是把文件上传到对象存储,然后只返回 URL。

例如:

Worker 生成截图 → 上传到 MinIO/S3 → 返回 objectKey 或 signedUrl

十二、常见故障排查命令

1. 查看内存

free -h

2. 查看 CPU

top
htop

安装 htop:

sudo apt install -y htop

3. 查看 Chrome 进程

ps aux | grep chromium

4. 查看端口占用

sudo lsof -i:3000

5. 查看 Redis 队列情况

redis-cli

进入后执行:

KEYS bull:browser-tasks:*
LLEN bull:browser-tasks:wait
ZCARD bull:browser-tasks:delayed

6. Docker 查看资源

docker stats

7. Docker 查看日志

docker compose logs -f worker

8. 清理异常容器

docker compose down
docker system prune -f

十三、推荐生产配置

如果是中小规模生产环境,可以采用如下配置:

API 服务:2 个实例
Redis:1 主 1 从,或托管 Redis
Worker:5~10 个实例
单 Worker 并发:3~8
Nginx 限流:每 IP 5~20 r/s
任务超时:30~60 秒
浏览器重启周期:每 1000 个任务
日志保留:7~30 天
监控:CPU、内存、队列长度、任务成功率、平均耗时

如果业务对实时性要求很高,可以同步返回结果;如果任务耗时较长,建议异步任务模式。大多数 AI 浏览器场景,更推荐异步队列,因为它能明显提升系统稳定性。


十四、总结

AI 浏览器高并发的核心不是盲目增加浏览器数量,而是要建立一套可控、可观测、可扩展的任务执行体系。推荐的生产方案是:

  1. 使用 API 服务接收请求;
  2. 使用 Redis/BullMQ 做任务队列;
  3. 使用 Browser Worker 消费任务;
  4. Worker 内部复用 Browser,任务级别使用独立 Context;
  5. 使用 Docker 或 Kubernetes 横向扩容 Worker;
  6. 使用 Nginx 做限流和反向代理;
  7. 对所有任务设置超时、重试、资源释放和监控;
  8. 严格防范 SSRF、内存泄漏和队列堆积。

对于大多数场景来说,“浏览器池 + 任务队列 + Worker 横向扩容”就是 AI 浏览器高并发的最佳实践。它既能保证隔离性,又能提升吞吐量,还能在流量高峰时通过队列削峰,避免系统瞬间被压垮。只要合理设置并发、做好监控和限流,这套方案可以从单机几十并发平滑演进到集群上百甚至更高并发。

目录结构
全文