Skip to content

souloss/browserpm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

English | 中文

browserpm

一个生产就绪的浏览器页面池管理器,支持自动恢复。

特性

  • 页面池化: 可配置最小/最大页面数的预热页面池
  • 轮询调度: 在页面间公平分配操作
  • 健康检查: 自动检测并替换不健康的页面
  • TTL 回收: 页面在可配置的 TTL 后被回收
  • 上下文恢复: 自动重建死亡的浏览器上下文
  • 优雅关闭: 关闭前排空活跃操作
  • 进程监控: 通过 CDP 跟踪 CPU/内存使用
  • 并发安全: 所有操作可安全并发使用

安装

  • Go 1.24+
  • playwright-go v0.5700.1+
go get github.com/souloss/browserpm

快速开始

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/playwright-community/playwright-go"
    "github.com/souloss/browserpm"
)

func main() {
    // 使用选项创建管理器
    manager, err := browserpm.New(
        browserpm.WithHeadless(true),
        browserpm.WithAutoInstall(true),
        browserpm.WithMinPages(3),
        browserpm.WithMaxPages(10),
        browserpm.WithPoolTTL(30*time.Minute),
    )
    if err != nil {
        log.Fatalf("创建管理器失败: %v", err)
    }
    defer manager.Close()

    // 定义提供者
    contextProvider := browserpm.NewContextProvider(
        playwright.BrowserNewContextOptions{
            UserAgent: playwright.String("browserpm-example/1.0"),
        },
        func(ctx context.Context, bCtx playwright.BrowserContext) error {
            return nil // 上下文设置
        },
    )

    pageProvider := browserpm.NewPageProvider(
        func(ctx context.Context, page playwright.Page) error {
            _, err := page.Goto("https://example.com")
            return err
        },
        func(ctx context.Context, page playwright.Page) bool {
            return !page.IsClosed()
        },
    )

    // 创建会话
    session, err := manager.CreateSession("example", contextProvider, pageProvider)
    if err != nil {
        log.Fatalf("创建会话失败: %v", err)
    }

    ctx := context.Background()

    // 在独占(单次使用)页面上执行操作
    err = session.Do(ctx, func(page playwright.Page) error {
        title, err := page.Title()
        if err != nil {
            return err
        }
        fmt.Printf("标题: %s\n", title)
        return nil
    })

    // 在共享(池化)页面上执行操作
    err = session.DoShare(ctx, func(page playwright.Page) error {
        result, err := page.Evaluate(`() => document.title`)
        if err != nil {
            return err
        }
        fmt.Printf("标题: %v\n", result)
        return nil
    })
}

架构

BrowserManager
├── Browser (playwright.Browser)
├── CDPSession (进程监控)
└── Sessions (map[string]*Session)
    └── Session
        ├── BrowserContext (playwright.BrowserContext)
        └── PagePool
            ├── Scheduler (轮询)
            ├── HealthChecker (后台)
            └── Reaper (基于TTL)

配置

管理器选项

manager, err := browserpm.New(
    // 浏览器选项
    browserpm.WithHeadless(true),
    browserpm.WithBrowserArgs("--no-sandbox", "--disable-gpu"),
    browserpm.WithBrowserTimeout(60*time.Second),

    // 安装选项
    browserpm.WithAutoInstall(true),
    browserpm.WithDeps(true),

    // 池选项
    browserpm.WithMinPages(1),
    browserpm.WithMaxPages(10),
    browserpm.WithPoolTTL(30*time.Minute),
    browserpm.WithGracePeriod(10*time.Second),
    browserpm.WithOperationTimeout(30*time.Second),
    browserpm.WithInitTimeout(30*time.Second),
    browserpm.WithHealthCheckInterval(30*time.Second),
    browserpm.WithScheduleStrategy("round-robin"),

    // 日志
    browserpm.WithLogger(logger),
)

会话选项(每个会话覆盖)

session, err := manager.CreateSession("my-session", cp, pp,
    browserpm.WithSessionMinPages(5),
    browserpm.WithSessionMaxPages(20),
    browserpm.WithSessionTTL(1*time.Hour),
)

默认配置

选项 默认值 描述
Headless true 无头模式运行浏览器
Browser.Timeout 60s 浏览器启动超时
Install.Auto true 启动时自动安装
Install.WithDeps true 安装系统依赖
Pool.MinPages 1 最小预热页面数
Pool.MaxPages 10 最大页面数
Pool.TTL 30m 页面存活时间
Pool.GracePeriod 10s 强制关闭前的等待期
Pool.OperationTimeout 30s 操作超时
Pool.InitTimeout 30s 页面初始化超时
Pool.HealthCheckInterval 30s 健康检查间隔
Pool.ScheduleStrategy round-robin 调度策略

提供者

ContextProvider

控制 BrowserContext 的创建和配置:

type ContextProvider interface {
    Options() playwright.BrowserNewContextOptions
    Setup(ctx context.Context, bCtx playwright.BrowserContext) error
}

PageProvider

控制页面初始化和健康检查:

type PageProvider interface {
    Init(ctx context.Context, page playwright.Page) error
    Check(ctx context.Context, page playwright.Page) bool
}

操作

Do(独占页面)

创建新页面,运行操作,然后关闭页面。在上下文/页面失败时自动重试。

err := session.Do(ctx, func(page playwright.Page) error {
    return page.Goto("https://example.com")
})

Manager.Do(一次性操作)

无需创建 Session,直接在 Manager 上执行一次性操作。每次调用都会创建临时 BrowserContext 和 Page,执行完毕后自动清理。

适用场景:

  • 需要不同的 storageState(如不同账号)
  • 一次性操作,无需复用页面
  • 需要完全隔离的上下文配置
// 最简单:无配置
err := manager.Do(ctx, playwright.BrowserNewContextOptions{
    UserAgent: playwright.String("browserpm-test/1.0"),
}, func(page playwright.Page) error {
    _, err := page.Goto("https://example.com")
    return err
})

完整配置:

err := manager.Do(ctx, playwright.BrowserNewContextOptions{
    StorageState: storageState, // storageState 从其他地方获取
    UserAgent:    playwright.String("my-agent"),
    Viewport:     &playwright.Size{Width: 1920, Height: 1080},
}, func(page playwright.Page) error {
    _, err := page.Goto("https://example.com")
    return err
})

DoShare(池化页面)

从池中获取页面,执行操作后归还。多个 goroutine 可并发使用同一页面。

err := session.DoShare(ctx, func(page playwright.Page) error {
    result, err := page.Evaluate(`() => document.title`)
    return err
})

错误处理

库使用带有错误码的结构化错误:

import "errors"

err := session.Do(ctx, op)
if errors.Is(err, browserpm.ErrClosedErr) {
    // 会话已关闭
}
if errors.Is(err, browserpm.ErrContextDeadErr) {
    // 上下文已死亡(尝试自动恢复)
}
if errors.Is(err, browserpm.ErrPoolExhaustedErr) {
    // 池已达最大容量,无可用页面
}

错误码

错误码 描述
ErrSessionNotFound 会话不存在
ErrSessionExists 会话已存在
ErrPoolExhausted 池已达最大容量
ErrContextDead 浏览器上下文已死亡
ErrPageUnavailable 创建/访问页面失败
ErrTimeout 操作超时
ErrClosed 管理器/会话已关闭
ErrInvalidState 无效的内部状态
ErrInternal 内部错误

全局单例

对于简单用例,使用全局单例:

// 配置(必须在第一次 Global() 调用之前)
browserpm.SetGlobalOptions(
    browserpm.WithHeadless(true),
    browserpm.WithMinPages(3),
)

// 使用全局函数
session, err := browserpm.GCreateSession("my-session", cp, pp)
err = browserpm.GCloseSession("my-session")
infos, _ := browserpm.GListSessions()

// 完成后关闭
browserpm.Shutdown()

进程监控

获取所有浏览器进程的 CPU/内存使用情况:

infos, err := manager.GetProcessInfos(ctx)
for _, pi := range infos {
    fmt.Printf("PID %d (%s): RSS=%dMB, CPU=%.2f\n",
        pi.ID, pi.Type, pi.RSS/1024/1024, pi.CPU)
}

会话状态

info := session.Status()
fmt.Printf("会话: %s, 状态: %s, 页面数: %d, 活跃操作: %d\n",
    info.Name, info.State, info.PageCount, info.ActiveOps)

// 列出所有会话
for _, s := range manager.ListSessions() {
    fmt.Printf("- %s (%s)\n", s.Name, s.State)
}

日志

库使用结构化日志接口。默认使用 Zap 日志。

// 自定义日志
logger := browserpm.NewZapLoggerWithConfig(true) // 调试模式
manager, _ := browserpm.New(browserpm.WithLogger(logger))

// 空操作日志(禁用日志)
manager, _ := browserpm.New(browserpm.WithLogger(browserpm.NewNopLogger()))

线程安全

所有导出方法都可安全并发使用:

  • BrowserManager 使用 sync.Map 管理会话
  • Session 使用 sync.RWMutex 保护状态
  • PagePool 在热路径上使用原子操作
  • Scheduler 使用原子计数器进行轮询

生命周期

  1. 管理器创建: New() 安装驱动、启动浏览器、建立 CDP
  2. 会话创建: CreateSession() 注册会话(上下文/池延迟创建)
  3. 首次操作: 在第一次 Do/DoShare 时创建上下文和池
  4. 健康检查: 后台 goroutine 检查页面健康状态
  5. TTL 回收: TTL 过期后页面被回收
  6. 恢复: 死亡的上下文/页面自动重建
  7. 关闭: Close() 排空活跃操作,关闭页面、上下文、浏览器

性能

针对高并发 JS 场景(如 OKX ontGet),已通过参数矩阵优化:

  • 基线: 单次 Evaluate,5 页面,50 并发 → ~77 QPS
  • 优化后: 批量 Promise.all,1 页面,1000 并发 → ~3800 QPS(约 50 倍提升)

关键:使用 page.EvaluatePromise.all 批量调用减少 CDP 往返;少页面 + 高并发优于多页面。

许可证

MIT

About

一个生产就绪的浏览器页面管理器,基于 playwright-go 构建。它管理浏览器会话,具有自动页面池化、健康检查和恢复功能。

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages