AI浏览器并发扛不住?从浏览器池到K8s扩容的完整落地方案
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
该架构的关键点是:
- API 服务不直接执行浏览器任务,而是把任务放入队列;
- Browser Worker 从队列中消费任务;
- Worker 内部维护浏览器池,复用 Browser 实例;
- 每个任务使用独立 BrowserContext,保证隔离;
- 通过 Redis 或数据库存储任务状态;
- 通过容器或 Kubernetes 横向扩容 Worker;
- 使用 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,可以适当提高并发。
核心原则:
- CPU 使用率长期不要超过 80%;
- 内存使用率长期不要超过 75%;
- Redis 队列长度不能持续增长;
- 任务超时率应低于可接受阈值;
- 浏览器崩溃后必须自动恢复;
- 单任务必须设置超时时间;
- 每个任务结束后必须关闭 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 浏览器高并发的核心不是盲目增加浏览器数量,而是要建立一套可控、可观测、可扩展的任务执行体系。推荐的生产方案是:
- 使用 API 服务接收请求;
- 使用 Redis/BullMQ 做任务队列;
- 使用 Browser Worker 消费任务;
- Worker 内部复用 Browser,任务级别使用独立 Context;
- 使用 Docker 或 Kubernetes 横向扩容 Worker;
- 使用 Nginx 做限流和反向代理;
- 对所有任务设置超时、重试、资源释放和监控;
- 严格防范 SSRF、内存泄漏和队列堆积。
对于大多数场景来说,“浏览器池 + 任务队列 + Worker 横向扩容”就是 AI 浏览器高并发的最佳实践。它既能保证隔离性,又能提升吞吐量,还能在流量高峰时通过队列削峰,避免系统瞬间被压垮。只要合理设置并发、做好监控和限流,这套方案可以从单机几十并发平滑演进到集群上百甚至更高并发。