几百个任务同时跑也不崩:AI浏览器高并发架构实战与源码
AI浏览器 高并发解决方案|附源码
随着大模型能力的快速演进,“AI浏览器”正在成为新一代智能入口。它不再只是传统意义上的网页访问工具,而是集成了网页理解、自动检索、任务规划、表单填写、摘要生成、多标签页协同、Agent执行等能力的智能系统。
但是,当AI浏览器从个人工具走向企业级平台时,一个非常现实的问题会立刻出现:高并发场景下如何稳定运行?
例如:
- 同时有几百个用户发起网页总结任务;
- 系统需要并发打开大量页面进行信息采集;
- 多个AI Agent同时操作浏览器环境;
- 页面加载、截图、DOM解析、模型调用、任务编排都在消耗资源;
- 浏览器实例过多导致CPU、内存、文件句柄迅速耗尽;
- 大模型接口存在限流、超时、排队等问题。
本文将围绕“AI浏览器高并发解决方案”展开,介绍整体架构设计、关键技术点、并发控制策略、资源池化方案、任务队列设计,并附上一个可运行的简化源码示例,帮助你快速搭建一个具备高并发处理能力的AI浏览器服务雏形。
一、什么是AI浏览器?
AI浏览器可以简单理解为:具备网页访问能力、页面理解能力和自动执行能力的智能浏览器系统。
它通常由以下几个模块组成:
| 模块 | 作用 |
|---|---|
| 浏览器执行层 | 负责打开网页、点击、输入、截图、抓取DOM |
| 页面解析层 | 解析网页正文、链接、表格、图片、结构化数据 |
| AI理解层 | 调用大模型进行摘要、问答、抽取、分类 |
| Agent规划层 | 根据用户目标自动拆解任务并执行 |
| 任务调度层 | 管理任务队列、并发、重试、超时 |
| API服务层 | 向前端或第三方系统提供接口 |
一个典型任务可能是:
用户输入:“帮我打开某个新闻网站,搜索AI浏览器相关内容,并总结前三篇文章。”
AI浏览器需要完成:
- 启动或复用浏览器实例;
- 打开目标网站;
- 输入关键词;
- 点击搜索;
- 提取搜索结果;
- 打开文章详情;
- 获取正文内容;
- 调用大模型生成摘要;
- 返回最终结果。
如果只有一个用户使用,这并不复杂。但当同时有100个、1000个用户发起类似任务时,系统设计就会变得完全不同。
二、AI浏览器高并发的核心难点
1. 浏览器实例非常消耗资源
以 Playwright 或 Puppeteer 为例,一个 Chromium 浏览器实例通常会占用几十到数百MB内存。如果每个请求都新建一个浏览器实例,那么在高并发下很快就会导致:
- 内存飙升;
- CPU占满;
- 容器频繁重启;
- 页面打开缓慢;
- 系统整体不可用。
因此,高并发AI浏览器不能简单采用“一个请求一个浏览器”的模式,而应该使用浏览器池。
2. 页面加载存在大量不可控因素
网页访问不同于普通接口调用,它会受到很多因素影响:
- 目标网站响应慢;
- 页面包含大量JS;
- 第三方广告、统计脚本拖慢加载;
- 页面出现验证码;
- 网站限制访问频率;
- DNS或网络波动;
- 页面DOM结构变化。
所以系统必须具备:
- 超时控制;
- 重试机制;
- 异常捕获;
- 页面隔离;
- 任务失败降级。
3. 大模型接口本身也有限流
AI浏览器通常要调用大模型做网页摘要、问答、抽取等任务。而大模型服务一般都有:
- QPS限制;
- TPM限制;
- RPM限制;
- 响应延迟较高;
- 价格成本较高。
所以不能让所有浏览器任务直接无控制地调用模型,而应该增加:
- 模型请求队列;
- 并发信号量;
- Token预算控制;
- 结果缓存;
- 流式返回。
4. 多用户任务需要隔离
如果多个用户共享同一个浏览器上下文,可能会出现:
- Cookie串号;
- 登录状态污染;
- 页面互相影响;
- 隐私数据泄露。
正确做法是:
- 复用Browser;
- 隔离Context;
- 每个任务使用独立Page;
- 任务结束后关闭Page和Context;
- 必要时按用户维护独立会话。
三、总体架构设计
一个可扩展的AI浏览器高并发架构可以设计如下:
用户请求
│
▼
API网关 / HTTP服务
│
▼
任务队列 Task Queue
│
▼
调度器 Scheduler
│
├── 浏览器资源池 Browser Pool
│ ├── Browser-1
│ ├── Browser-2
│ └── Browser-N
│
├── 页面执行器 Page Worker
│
├── 页面解析器 DOM Parser
│
└── AI模型调用器 LLM Client
│
▼
结果缓存 / 数据库
│
▼
返回用户
核心思想是:
请求不直接操作浏览器,而是先进入任务队列,由调度器根据资源情况分配执行。
这样可以避免瞬时流量直接打爆浏览器和模型服务。
四、关键设计原则
1. 浏览器池化
高并发场景下,不建议频繁启动和关闭浏览器进程。更好的方式是:
- 启动固定数量的Browser实例;
- 每个Browser可创建多个Context;
- 每个Context创建Page执行任务;
- 任务完成后销毁Context和Page;
- Browser长期复用。
这样既能减少启动成本,又能保证任务隔离。
2. 队列削峰
当请求量超过处理能力时,不能无限制创建任务。应该引入队列,例如:
- BullMQ;
- RabbitMQ;
- Kafka;
- Redis Stream;
- Node.js内存队列;
- Python Celery。
队列的作用包括:
- 削峰填谷;
- 异步处理;
- 任务重试;
- 失败补偿;
- 状态追踪。
3. 并发限流
并发控制要分多个层级:
| 层级 | 控制内容 |
|---|---|
| API层 | 限制用户请求频率 |
| 队列层 | 限制任务积压数量 |
| 浏览器层 | 限制同时打开页面数量 |
| 模型层 | 限制LLM调用并发 |
| 目标网站层 | 避免对单站点过度访问 |
例如可以设定:
最大浏览器实例数:4
每个浏览器最大Context数:8
最大页面并发数:32
LLM最大并发数:10
单用户最大并发任务数:3
单域名最大并发访问数:5
4. 超时和熔断
浏览器任务很容易卡住,所以必须设置超时:
- 页面打开超时;
- 选择器等待超时;
- 脚本执行超时;
- 模型调用超时;
- 整体任务超时。
如果某个目标站点持续失败,则可以临时熔断,避免无效请求拖垮系统。
5. 缓存优先
很多AI浏览器任务存在重复访问,例如热门新闻、商品页面、文档页面等。
可以缓存:
- URL正文提取结果;
- 页面截图;
- 页面摘要;
- LLM生成结果;
- 搜索结果;
- Agent中间步骤。
缓存可以显著降低浏览器资源和模型调用成本。
五、技术选型
本文示例采用以下技术栈:
- Node.js
- TypeScript
- Express
- Playwright
- p-limit
- 内存任务队列
生产环境中可以替换为:
- Redis + BullMQ;
- PostgreSQL / MongoDB 存储任务;
- Kubernetes 自动扩缩容;
- Prometheus + Grafana 监控;
- OpenAI / Claude / Qwen / DeepSeek 等模型接口。
六、项目目录结构
ai-browser-concurrency/
├── package.json
├── tsconfig.json
├── src/
│ ├── app.ts
│ ├── browserPool.ts
│ ├── taskQueue.ts
│ ├── llmClient.ts
│ └── types.ts
七、源码实现
下面给出一个简化版源码示例。它实现了:
- 浏览器池;
- 任务队列;
- 页面访问;
- 正文提取;
- AI摘要模拟;
- 并发控制;
- API接口。
1. package.json
{
"name": "ai-browser-concurrency",
"version": "1.0.0",
"description": "AI浏览器高并发解决方案示例",
"main": "dist/app.js",
"scripts": {
"dev": "ts-node src/app.ts",
"build": "tsc",
"start": "node dist/app.js"
},
"dependencies": {
"express": "^4.18.3",
"playwright": "^1.42.0",
"p-limit": "^3.1.0",
"uuid": "^9.0.1"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.11.24",
"@types/uuid": "^9.0.8",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}
}
2. tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"rootDir": "./src",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true
}
}
3. types.ts
export type TaskStatus = "pending" | "running" | "success" | "failed";
export interface BrowserTask {
id: string;
url: string;
status: TaskStatus;
result?: string;
error?: string;
createdAt: number;
updatedAt: number;
}
4. browserPool.ts
import { chromium, Browser } from "playwright";
import pLimit from "p-limit";
export class BrowserPool {
private browsers: Browser[] = [];
private index = 0;
private pageLimit;
constructor(
private readonly browserCount: number,
private readonly maxConcurrentPages: number
) {
this.pageLimit = pLimit(maxConcurrentPages);
}
async init() {
for (let i = 0; i < this.browserCount; i++) {
const browser = await chromium.launch({
headless: true,
args: [
"--no-sandbox",
"--disable-dev-shm-usage",
"--disable-gpu"
]
});
this.browsers.push(browser);
}
console.log(`BrowserPool initialized: ${this.browserCount} browsers`);
}
private getBrowser(): Browser {
const browser = this.browsers[this.index];
this.index = (this.index + 1) % this.browsers.length;
return browser;
}
async run(handler: (page: any) => Promise): Promise {
return this.pageLimit(async () => {
const browser = this.getBrowser();
const context = await browser.newContext({
viewport: {
width: 1366,
height: 768
},
userAgent:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36"
});
const page = await context.newPage();
try {
page.setDefaultTimeout(10000);
page.setDefaultNavigationTimeout(15000);
const result = await handler(page);
return result;
} finally {
await page.close().catch(() => {});
await context.close().catch(() => {});
}
});
}
async close() {
for (const browser of this.browsers) {
await browser.close();
}
}
}
5. llmClient.ts
这里为了方便演示,使用一个模拟LLM客户端。在真实环境中,你可以替换成 OpenAI、DeepSeek、通义千问、Claude 等API。
import pLimit from "p-limit";
export class LLMClient {
private limit;
constructor(private readonly maxConcurrentLLM: number) {
this.limit = pLimit(maxConcurrentLLM);
}
async summarize(text: string): Promise {
return this.limit(async () => {
const sliced = text.slice(0, 1500);
// 模拟大模型接口延迟
await new Promise((resolve) => setTimeout(resolve, 800));
return `AI摘要:本文主要内容如下:${sliced.slice(0, 300)}...`;
});
}
}
6. taskQueue.ts
import { v4 as uuidv4 } from "uuid";
import { BrowserTask } from "./types";
import { BrowserPool } from "./browserPool";
import { LLMClient } from "./llmClient";
export class TaskQueue {
private tasks = new Map();
private queue: string[] = [];
private running = 0;
constructor(
private readonly browserPool: BrowserPool,
private readonly llmClient: LLMClient,
private readonly maxWorkers: number
) {}
addTask(url: string): BrowserTask {
const now = Date.now();
const task: BrowserTask = {
id: uuidv4(),
url,
status: "pending",
createdAt: now,
updatedAt: now
};
this.tasks.set(task.id, task);
this.queue.push(task.id);
this.schedule();
return task;
}
getTask(id: string): BrowserTask | undefined {
return this.tasks.get(id);
}
private schedule() {
while (this.running < this.maxWorkers && this.queue.length > 0) {
const taskId = this.queue.shift();
if (!taskId) return;
this.running++;
this.execute(taskId)
.catch((err) => {
console.error("Task execute error:", err);
})
.finally(() => {
this.running--;
this.schedule();
});
}
}
private async execute(taskId: string) {
const task = this.tasks.get(taskId);
if (!task) return;
task.status = "running";
task.updatedAt = Date.now();
try {
const pageText = await this.browserPool.run(async (page) => {
await page.goto(task.url, {
waitUntil: "domcontentloaded",
timeout: 15000
});
await page.waitForTimeout(1000);
const title = await page.title();
const text = await page.evaluate(() => {
const scripts = document.querySelectorAll("script,style,noscript");
scripts.forEach((el) => el.remove());
return document.body?.innerText || "";
});
return `标题:${title}\n\n正文:${text}`;
});
if (!pageText || pageText.length < 20) {
throw new Error("页面正文过短,可能提取失败");
}
const summary = await this.llmClient.summarize(pageText);
task.status = "success";
task.result = summary;
task.updatedAt = Date.now();
} catch (err: any) {
task.status = "failed";
task.error = err.message || "unknown error";
task.updatedAt = Date.now();
}
}
}
7. app.ts
import express from "express";
import { BrowserPool } from "./browserPool";
import { LLMClient } from "./llmClient";
import { TaskQueue } from "./taskQueue";
async function bootstrap() {
const app = express();
app.use(express.json());
const browserPool = new BrowserPool(
Number(process.env.BROWSER_COUNT || 2),
Number(process.env.MAX_CONCURRENT_PAGES || 8)
);
await browserPool.init();
const llmClient = new LLMClient(
Number(process.env.MAX_CONCURRENT_LLM || 4)
);
const taskQueue = new TaskQueue(
browserPool,
llmClient,
Number(process.env.MAX_WORKERS || 8)
);
app.post("/api/tasks", (req, res) => {
const { url } = req.body;
if (!url || typeof url !== "string") {
return res.status(400).json({
message: "url不能为空"
});
}
const task = taskQueue.addTask(url);
res.json({
taskId: task.id,
status: task.status
});
});
app.get("/api/tasks/:id", (req, res) => {
const task = taskQueue.getTask(req.params.id);
if (!task) {
return res.status(404).json({
message: "任务不存在"
});
}
res.json(task);
});
app.get("/health", (_, res) => {
res.json({
status: "ok"
});
});
const port = Number(process.env.PORT || 3000);
app.listen(port, () => {
console.log(`AI Browser server running at http://localhost:${port}`);
});
process.on("SIGINT", async () => {
console.log("Shutting down...");
await browserPool.close();
process.exit(0);
});
}
bootstrap();
八、运行方式
安装依赖:
npm install
安装 Playwright 浏览器:
npx playwright install chromium
启动服务:
npm run dev
提交任务:
curl -X POST http://localhost:3000/api/tasks \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com"}'
返回结果:
{
"taskId": "1b86f5aa-7e83-4f12-a893-xxxx",
"status": "pending"
}
查询任务:
curl http://localhost:3000/api/tasks/1b86f5aa-7e83-4f12-a893-xxxx
九、并发参数如何配置?
不同机器资源不同,参数不能一概而论。可以按以下方式估算。
1. 小型服务器
例如:2核4G
BROWSER_COUNT=1
MAX_CONCURRENT_PAGES=4
MAX_WORKERS=4
MAX_CONCURRENT_LLM=2
适合低频内部工具。
2. 中型服务器
例如:4核8G
BROWSER_COUNT=2
MAX_CONCURRENT_PAGES=8
MAX_WORKERS=8
MAX_CONCURRENT_LLM=4
适合小团队或轻量级SaaS。
3. 较大型服务器
例如:8核16G
BROWSER_COUNT=4
MAX_CONCURRENT_PAGES=24
MAX_WORKERS=24
MAX_CONCURRENT_LLM=8
适合中等并发业务,但仍建议配合Redis队列和容器扩容。
十、生产环境优化建议
上面的代码是一个简化实现,适合理解思路和快速验证。如果要用于生产环境,还需要做更多增强。
1. 使用Redis队列替代内存队列
内存队列的问题是:
- 服务重启任务丢失;
- 多实例无法共享任务;
- 不方便做延迟任务;
- 不方便失败重试。
生产环境建议使用 BullMQ:
API服务负责接收任务
Worker服务负责执行任务
Redis负责保存队列状态
这样可以横向扩展多个Worker节点。
2. 加入任务状态持久化
任务状态应写入数据库,例如:
- PostgreSQL;
- MySQL;
- MongoDB;
- Redis。
任务表可以设计为:
CREATE TABLE ai_browser_tasks (
id VARCHAR(64) PRIMARY KEY,
user_id VARCHAR(64),
url TEXT NOT NULL,
status VARCHAR(32) NOT NULL,
result TEXT,
error TEXT,
retry_count INT DEFAULT 0,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
3. 加入URL安全校验
AI浏览器服务如果允许用户传入任意URL,必须防止SSRF攻击。
需要禁止访问:
127.0.0.1localhost- 内网IP
- 云厂商Metadata地址
- file协议
- ftp协议
- 非HTTP协议
否则攻击者可能利用浏览器访问内部服务。
4. 加入单域名限流
如果大量任务集中访问同一个网站,可能会导致:
- 被目标站点封禁;
- 触发验证码;
- 造成对方服务压力;
- 自身任务大量失败。
可以按域名建立并发限制:
example.com 最大并发 3
news.example.com 最大并发 2
5. 页面正文提取优化
示例代码中使用 document.body.innerText,在真实场景中效果有限。可以采用:
- Readability;
- Mercury Parser;
- 自定义正文抽取算法;
- DOM密度分析;
- XPath规则;
- 站点模板适配。
对于AI摘要任务,正文质量直接影响模型输出质量。
6. LLM调用优化
大模型是AI浏览器的重要成本来源,建议优化:
- 相同URL摘要缓存;
- 长文本分块摘要;
- Map-Reduce总结;
- Token预算控制;
- 请求失败重试;
- 指数退避;
- 多模型路由;
- 低价值任务使用小模型。
7. 可观测性建设
高并发系统必须具备监控能力。建议采集:
- 当前队列长度;
- 任务成功率;
- 任务失败率;
- 平均执行耗时;
- P95/P99延迟;
- 浏览器实例数量;
- 页面并发数量;
- LLM调用耗时;
- 内存和CPU使用率;
- 单域名失败率。
可以使用:
- Prometheus;
- Grafana;
- OpenTelemetry;
- Loki;
- ELK。
十一、容器化部署建议
Playwright在容器中运行时,需要注意系统依赖。可以基于官方镜像:
FROM mcr.microsoft.com/playwright:v1.42.0-jammy
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
ENV NODE_ENV=production
ENV PORT=3000
CMD ["npm", "start"]
构建镜像:
docker build -t ai-browser-concurrency .
运行容器:
docker run -p 3000:3000 \
-e BROWSER_COUNT=2 \
-e MAX_CONCURRENT_PAGES=8 \
-e MAX_WORKERS=8 \
-e MAX_CONCURRENT_LLM=4 \
ai-browser-concurrency
十二、Kubernetes扩展思路
当单机无法支撑更高并发时,可以采用Kubernetes部署:
Ingress
│
▼
API Deployment,多副本
│
▼
Redis Queue
│
▼
Worker Deployment,多副本
│
▼
Browser Pool
扩容方式:
- API层根据QPS扩容;
- Worker层根据队列长度扩容;
- LLM调用层根据限流策略控制;
- Redis负责协调任务分发。
需要特别注意的是,浏览器Worker属于高资源消耗服务,应该设置合理的资源限制:
resources:
requests:
cpu: "1"
memory: "2Gi"
limits:
cpu: "2"
memory: "4Gi"
十三、常见问题与解决方案
问题1:浏览器运行一段时间后内存越来越高
可能原因:
- Page未关闭;
- Context未关闭;
- 页面存在内存泄漏;
- 浏览器长时间运行碎片化。
解决方案:
- 使用
finally确保关闭Page和Context; - 定期重启Browser实例;
- 设置单个Browser最大任务数;
- 监控内存超过阈值后自动替换实例。
问题2:页面经常超时
可能原因:
- 目标站点慢;
- 网络不稳定;
- 页面资源过多;
- waitUntil设置过严。
解决方案:
- 使用
domcontentloaded而非networkidle; - 拦截图片、字体、视频等资源;
- 设置合理重试;
- 对慢站点单独配置超时。
问题3:LLM调用成为瓶颈
解决方案:
- 增加模型并发额度;
- 使用队列削峰;
- 对重复URL做缓存;
- 长文本先压缩再总结;
- 使用本地小模型处理简单任务;
- 将模型调用与浏览器访问解耦。
问题4:高并发下被目标网站封禁
解决方案:
- 降低单域名并发;
- 增加访问间隔;
- 遵守robots协议;
- 使用官方API;
- 做任务缓存;
- 避免无意义重复抓取。
十四、总结
AI浏览器的高并发问题,本质上不是单一技术点,而是一个完整系统工程。它同时涉及:
- 浏览器资源管理;
- 任务调度;
- 队列削峰;
- 并发限流;
- 页面解析;
- 大模型调用;
- 缓存;
- 安全;
- 监控;
- 容器化部署。
本文给出的方案核心可以概括为一句话:
用队列承接请求,用浏览器池复用资源,用Context隔离任务,用限流保护系统,用缓存降低成本。
对于个人项目,内存队列加浏览器池已经足够完成原型验证;对于企业级应用,则建议进一步引入Redis队列、数据库持久化、Kubernetes弹性扩容、全链路监控和严格的安全策略。
只要架构设计合理,AI浏览器完全可以从一个单用户智能工具,演进为一个稳定、可扩展、可商用的高并发智能浏览器平台。