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

几百个任务同时跑也不崩:AI浏览器高并发架构实战与源码

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

AI浏览器 高并发解决方案|附源码

随着大模型能力的快速演进,“AI浏览器”正在成为新一代智能入口。它不再只是传统意义上的网页访问工具,而是集成了网页理解、自动检索、任务规划、表单填写、摘要生成、多标签页协同、Agent执行等能力的智能系统。

但是,当AI浏览器从个人工具走向企业级平台时,一个非常现实的问题会立刻出现:高并发场景下如何稳定运行?

例如:

  • 同时有几百个用户发起网页总结任务;
  • 系统需要并发打开大量页面进行信息采集;
  • 多个AI Agent同时操作浏览器环境;
  • 页面加载、截图、DOM解析、模型调用、任务编排都在消耗资源;
  • 浏览器实例过多导致CPU、内存、文件句柄迅速耗尽;
  • 大模型接口存在限流、超时、排队等问题。

本文将围绕“AI浏览器高并发解决方案”展开,介绍整体架构设计、关键技术点、并发控制策略、资源池化方案、任务队列设计,并附上一个可运行的简化源码示例,帮助你快速搭建一个具备高并发处理能力的AI浏览器服务雏形。


一、什么是AI浏览器?

AI浏览器可以简单理解为:具备网页访问能力、页面理解能力和自动执行能力的智能浏览器系统。

它通常由以下几个模块组成:

模块 作用
浏览器执行层 负责打开网页、点击、输入、截图、抓取DOM
页面解析层 解析网页正文、链接、表格、图片、结构化数据
AI理解层 调用大模型进行摘要、问答、抽取、分类
Agent规划层 根据用户目标自动拆解任务并执行
任务调度层 管理任务队列、并发、重试、超时
API服务层 向前端或第三方系统提供接口

一个典型任务可能是:

用户输入:“帮我打开某个新闻网站,搜索AI浏览器相关内容,并总结前三篇文章。”

AI浏览器需要完成:

  1. 启动或复用浏览器实例;
  2. 打开目标网站;
  3. 输入关键词;
  4. 点击搜索;
  5. 提取搜索结果;
  6. 打开文章详情;
  7. 获取正文内容;
  8. 调用大模型生成摘要;
  9. 返回最终结果。

如果只有一个用户使用,这并不复杂。但当同时有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.1
  • localhost
  • 内网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浏览器完全可以从一个单用户智能工具,演进为一个稳定、可扩展、可商用的高并发智能浏览器平台。

目录结构
全文