Skip to content

创建浏览器进程失败 #36

Description

@liminger233

本 issue 附带完整根因定位与已验证的修复方案,可直接参考。

一、环境

版本 XianYuSmart 2.0.7
部署方式 Docker Compose(compose.yaml,含 MySQL 服务)
运行时镜像 eclipse-temurin:21-jre-jammy
Playwright com.microsoft.playwright:playwright:1.61.0
宿主机 Ubuntu,8 GB RAM,Docker 默认配置
容器 /dev/shm 64 MB(默认,未设 shm_size

二、现象

服务启动后一切正常。运行约 1 小时以上,当 WebSocket 断开重连触发滑块验证时,界面提示:

创建浏览器进程失败:Failed to launch driver

此后所有需要浏览器的操作(滑块验证、Cookie 浏览器兜底刷新)持续失败,不再自愈。

重启容器可恢复,但约 1 小时后必然再次复现。


三、复现步骤

快速复现(约 1 分钟,推荐)

利用「driver 是静态单例、删除后不会重新解压」这一特性,跳过 1 小时等待:

# 1. 先触发一次滑块验证,让 Playwright 完成驱动解压
#    (任何需要浏览器的操作均可,例如界面点「滑块验证」)

# 2. 确认驱动目录已生成,然后手动删除它
docker exec xianyusmart-app-1 sh -c 'ls -d /tmp/playwright-java-*'
docker exec xianyusmart-app-1 sh -c 'rm -rf /tmp/playwright-java-*'

# 3. 再次触发滑块验证 → 立即复现「创建浏览器进程失败」

完整复现(约 70 分钟,还原真实时序)

  1. docker compose up -d 启动服务
  2. 触发一次需要浏览器的操作,使 Playwright 解压 node 驱动
  3. 确认目录已生成:
    docker exec xianyusmart-app-1 sh -c 'ls -d /tmp/playwright-java-*'
  4. 保持浏览器空闲(不触发滑块),等待 60 分钟以上,让定时清理任务执行一轮
  5. 再次触发滑块验证 → 复现

观察清理是否真的执行过:

docker logs xianyusmart-app-1 2>&1 | grep -i "清理Playwright临时文件"

四、根因分析

三处代码叠加导致,缺一不可。

1. Playwright Java 侧(第三方既定行为)

反编译 driver-1.61.0.jar 可得:

// com.microsoft.playwright.impl.driver.Driver
public static synchronized Driver ensureDriverInstalled(Map env, Boolean installBrowsers) {
    if (instance == null) {
        instance = createAndInstall(env, installBrowsers);
    }
    return instance;          // ← 静态单例,整个 JVM 生命周期内只解压一次
}
// com.microsoft.playwright.impl.driver.jar.DriverJar
private final Path driverTempDir;                  // 形如 /tmp/playwright-java-XXXX
void extractDriverToTempDir() { ... }              // 内含 node 二进制 + driver/package

关键:驱动目录只在首次 Playwright.create() 时解压一次,之后永久复用。目录一旦消失,Playwright 不会重新解压。

2. 项目侧清理逻辑误伤

src/main/java/com/xianyusmart/config/PlaywrightManager.javacleanTempFiles()

Files.list(tmpDir)
        .filter(path -> {
            String name = path.getFileName().toString();
            return name.startsWith("playwright") || name.contains("chromium")   // ← 问题所在
                    || name.startsWith("core.") || name.endsWith(".pipe")
                    || name.endsWith(".sock");
        })

playwright-java-XXXX 正好被 startsWith("playwright") 命中。

而清理只跳过「最后修改时间在 1 小时内」的条目(PlaywrightManager.java:171thresholdMs = TimeUnit.HOURS.toMillis(1))——驱动目录建好后不再写入,运行满 1 小时必然满足条件

3. 触发时机

src/main/java/com/xianyusmart/service/impl/TokenRefreshServiceImpl.java:586

@Scheduled(fixedDelay = 60 * ONE_MINUTE_MS, initialDelay = 5 * ONE_MINUTE_MS)
public void scheduledCleanPlaywrightTempFiles() {
    playwrightManager.cleanTempFiles();
}

每小时执行一次。而 cleanTempFiles() 开头的守卫:

if (isInitialized()) {        // 浏览器懒加载,平时绝大多数时间都是 false
    log.debug("Playwright浏览器运行中,跳过临时文件清理");
    return;
}

浏览器是懒加载的,日常不启动,所以这个守卫基本不生效,清理会照常执行。

4. 失败链路

定时清理删除 /tmp/playwright-java-XXXX
        ↓
用户触发滑块验证 → Playwright.create()
        ↓
Driver.ensureDriverInstalled() 返回已缓存实例,不重新解压
        ↓
driver.createProcessBuilder() → ProcessBuilder.start()
        ↓
IOException: Cannot run program ".../node": No such file or directory
        ↓
PlaywrightException("Failed to launch driver")      ← PlaywrightImpl.createImpl 捕获 IOException 后抛出
        ↓
PlaywrightCaptchaBrowserRunner.java:439
    failureStage = "创建浏览器进程"
        ↓
界面显示「创建浏览器进程失败」

5. 判定特征(重要)

PlaywrightCaptchaBrowserRunner.java 中相邻两行设置了不同文案:

439:    failureStage = "创建浏览器进程";
440:    playwright = Playwright.create();
441:    BrowserType browserType = chromiumAfterProcessAttached(playwright, processSession);
442:    failureStage = "启动浏览器";
443:    Browser browser = browserType.launch(...);

报错落在「创建浏览器进程」而不是「启动浏览器」,说明 Chromium 根本没被拉起,故障在 Node 驱动派生阶段。这一点可以快速把问题与Chromium 本身的配置问题(沙箱、共享内存、二进制缺失)区分开。


五、已排除的可能性

以下均在真实环境交叉验证,可省去重复排查:

排查项 实测结果 结论
/dev/shm 耗尽 Used 0 / 64M 排除
内存耗尽 available 5.2 GB,java RSS 639 MB 排除
僵尸进程堆积 PID 1 为 docker-initinit: true 生效 排除
浏览器二进制缺失 /app/ms-playwrightchromium-1228 等三个目录齐全 排除
driverProcess 反射失败 反编译确认 private java.lang.Process driverProcess 字段存在 排除
容器内进程数超限 docker-init / sh / java 三个进程 排除

六、建议修复

1. 清理过滤器排除驱动目录(必须)

.filter(path -> {
    String name = path.getFileName().toString();
    // Playwright Java 的 node 驱动解压目录,删除后 Playwright.create() 将永久失败
    if (name.startsWith("playwright-java-")) {
        return false;
    }
    return name.startsWith("playwright_chromiumdev_profile")
            || name.startsWith("playwright-artifacts-")
            || name.startsWith(".org.chromium.Chromium.")
            || name.startsWith("core.")
            || name.endsWith(".pipe")
            || name.endsWith(".sock");
})

2. 把驱动目录迁出 /tmp(推荐,双保险)

compose.yamlJAVA_OPTS 增加:

-Dplaywright.driver.tmpdir=/app/data/pw-driver

cleanTempFiles() 只扫描 java.io.tmpdir,迁移后即使过滤器被误改也不会误删。

3. 单例浏览器补齐容器启动参数

PlaywrightManager.doInit() 当前只有 setHeadless(true),建议补:

.setArgs(Arrays.asList("--no-sandbox", "--disable-dev-shm-usage"))

PlaywrightCaptchaBrowserRunner 已带 --disable-dev-shm-usage,单例这边遗漏了。容器无 user namespace 且 /dev/shm 仅 64 MB,两个参数都必要。)

4. 提升错误信息可读性(可选)

browserFailureMessage() 当前只看 exception.getMessage(),而真正的系统错误在 cause 里,导致日志只剩一句无信息量的 Failed to launch driver。建议拼接 cause 链:

private static String causeChainSummary(Throwable throwable) {
    StringBuilder summary = new StringBuilder();
    Throwable current = throwable.getCause();
    int depth = 0;
    while (current != null && depth < 5) {
        String message = current.getMessage();
        if (message != null && !message.isBlank()) {
            summary.append(" | cause: ")
                    .append(current.getClass().getSimpleName())
                    .append(": ").append(message);
        }
        current = current.getCause();
        depth++;
    }
    return summary.toString();
}

本次排障中,若日志直接输出 No such file or directory,可节省大量时间。

5. 部署模板补齐 init: true

deploy/server/compose-existing-mysql.yaml 缺少根 compose.yaml 中已有的:

init: true

根 compose 的注释已说明原因(tini 回收孤儿与僵尸进程)。该模板还设了 pids_limit: 256,缺少 tini 时风险更大。


七、相关日志片段

驱动解压成功的日志(可用于定位目录路径):

extracted driver from jar to /tmp/playwright-java-1234567890

故障发生时的原始日志:

WARN  c.x.s.impl.WebSocketTokenServiceImpl - 【账号1】检测到滑块验证,已保存待处理地址
WARN  c.x.s.impl.WebSocketServiceImpl - 启动WebSocket需要滑块验证: accountId=1, url=...
com.xianyusmart.exception.CaptchaRequiredException: 需要完成滑块验证

浏览器失败记录(修复前只输出外层 message):

浏览器滑块验证失败: type=PlaywrightException, reason=创建浏览器进程失败:Failed to launch driver

八、备注

  • 本人在 2.0.7 源码上已按上述 1–4 修复并编译通过,部署后需观察满 1 小时确认
  • 该缺陷与账号风控(RGV587_ERROR)无关,是两个独立问题
  • 复现与验证均在真实 Docker 部署环境完成

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions