ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

pnpm报错ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL:根因是并发发布,加锁即可解决

pnpm报错ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL:根因是并发发布,加锁即可解决 先别急着怀疑 pnpm 是不是装坏了也别跑去清缓存重装。ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL 这个报错我前前后后踩了三次坑才彻底搞明白它大概率不是 pnpm 本身的问题而是你们发布方式的问题。今天这篇就好好聊聊前端发布的时候两个测试工程师同时重启为什么其中一个人会撞上这个报错以及到底怎么从根上解决。1. 先把这个报错拆开看它到底在说什么1.1 报错名字直译与真实含义这个报错全称是 ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL从命名上可以拆成三块来看RECURSIVE_EXEC表示 pnpm 在执行递归任务也就是 pnpm -r run xxx或者 workspace 里符合 pnpm run 的批量执行逻辑FIRST_FAIL表示在递归执行的过程中排在前面失败的那个子任务ERR 前缀说明这是 pnpm 自己的错误码体系不是 Node 的底层报错也就是说这个报错的意思是你在用 pnpm 执行一组递归脚本时其中某个子任务先挂了导致整个执行链终止。pnpm 不会把每个子任务都跑完再汇总结果而是遇到第一个失败就直接中断然后把这个失败对应的错误堆栈抛给你。很多人看到这种错误第一反应是依赖坏了、pnpm 版本不兼容、镜像源抽风。实际上我在两个不同项目里见过同一个报错根因完全不同一次是 node_modules 权限问题一次就是你们这种多人并发重启导致的文件竞态。1.2 为什么“两个人同时重启”会撞出这个错这个场景非常典型测试环境的前端服务挂在一台服务器上两个测试工程师各自打开终端一个执行重启脚本另一个也执行同样的重启脚本。如果你们的重启脚本里包含“删掉 node_modules → 重新 pnpm install → 重新 build → 重启服务”这种完整链路那不用等代码出问题并发本身就会制造问题。举个具体例子。A 工程师的脚本先执行 rm -rf node_modules把整个依赖目录删掉。就在这个瞬间B 工程师的构建进程正在读 node_modules/.pnpm 下的某个包文件发现文件没了直接抛 ENOENT。B 工程师的脚本处于 pnpm -r run build 的递归执行状态这个 ENOENT 就会成为“第一个失败”然后 pnpm 把这个失败包装成 ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL 抛出来。并发事件A 工程师操作B 工程师操作结果时间点 1删除 node_modules读取依赖文件ENOENT 报错时间点 2重新 install 写入硬链接构建产物写入 dist文件覆盖/写入冲突时间点 3重启服务占用端口重启服务占用同一端口端口冲突服务起不来所以这个报错本质上是“并发操作同一份文件资源”时pnpm 拿出来的一个兜底错误。它不是根因它是背锅侠。2. 这个报错最容易出现的三种“踩踏”路径2.1 路径一node_modules 删除与重建竞态这是我觉得最常见的一种。很多团队的发布脚本是这么写的rm -rf node_modules pnpm install --frozen-lockfile pnpm run build看起来每一步都没问题但一旦两个终端同时执行第一行 rm -rf 就已经注定了结局。A 把 node_modules 删了B 的构建进程正引着 node_modules 里的文件路径十有八九直接挂。pnpm 的硬链接结构对文件完整性要求很高node_modules 里一半是符号链接、一半是指向全局 store 的硬链接删到一半被读取报错就是一瞬间的事。我自己的项目里曾经用 pnpm 写过一个递归发布脚本restart: pnpm -r --parallel run build pm2 restart all这个脚本单跑没问题双跑必炸。因为两个进程都在对同一个 dist 目录和同一组 node_modules 做写操作构建到一半发现目录被对方重置整个递归链就断了。2.2 路径二全局 store 的硬链接层被破坏pnpm 的工作机制和 npm/yarn 不太一样。它采用内容寻址存储所有依赖包都缓存在全局 store 里通常是~/.pnpm-store或$PNPM_HOME下然后通过硬链接把文件链接到项目的 node_modules/.pnpm 目录下。硬链接的意思就是同一个 inode多个目录项指向它。这意味着两个进程同时访问同一个文件时如果其中一个进程把链接删掉了另一个进程手里的文件描述符就指向了一个已失效的 inode读取必然失败。如果两个 pnpm 进程同时执行 install它们都要往 node_modules/.pnpm 里注册硬链接。而注册硬链接之前pnpm 有个清理流程会把旧的链接关系移除。这时候 A 进程正在移除旧链接B 进程正在读取旧链接指向的文件文件已经被断开B 进程读取失败。底层报错可能五花八门但最终被递归执行机制捕获后统一对外展示成 ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL。2.3 路径三构建产物目录互相覆盖还有一个更隐蔽的路径两个人同时跑 build构建输出目录都是 dist构建还没结束另一个人的 webpack/vite 已经把 dist 里的旧文件清掉了。构建器在写文件时发现目录结构变了触发构建失败。这种失败如果发生在 pnpm -r run build 的递归链路里也会被包装成 FIRST_FAIL 错误。尤其是用 Vite 构建时它默认会先清空 outDir 再写入。两个 Vite 进程同时清空同一个 outDir后面的那个进程清空完前面的进程正准备把产物写进去发现目录没了直接报错。所以不管是从哪个路径踩进去问题的本质都一样你们缺了一个“同一时间只允许一个发布进程”的机制。3. 四种解法按推荐优先级排序3.1 解法一在发布脚本入口加互斥锁这是我目前最推荐的做法。不用改太多工程化结构只需要在发布脚本的最前面加一个锁判断保证同一时间内只有一个发布进程能进入关键区。如果你们的发布环境是 Linux 服务器直接用 flock 文件锁就行#!/bin/bash exec 9/tmp/frontend-deploy.lock if ! flock -n 9; then echo 已有发布任务正在执行请等待完成后再试 exit 1 fi rm -rf node_modules pnpm install --frozen-lockfile pnpm run build pm2 restart frontend # 释放锁 flock -u 9核心思路其实很简单就是用一个文件锁来代表“部署中”状态。第二个发布脚本启动时发现锁被占用直接退出而不是继续往下跑。这样从入口就杜绝了并发。如果你们的发布是在 Jenkins 或者 GitLab CI 上可以看平台自带的并发控制能力Jenkins 可以使用 Lockable Resources 插件给发布 job 加一个名为 frontend-deploy 的锁资源GitLab CI 可以在 job 上配置resource_group: frontend-deploy注意锁文件位置要选好尽量放在 /tmp 或者固定的运行目录下别放在项目目录里否则项目目录被删的时候锁文件也没了。并且锁粒度要控制好如果你有多个服务要发布别用一个全局锁把所有发布任务都串行化否则 A 服务发布的时候 B 服务也被卡住。3.2 解法二调整 pnpm 自身参数减少并发踩踏如果你的发布脚本确实需要递归执行那可以通过参数让 pnpm 不要那么“激进”。第一个参数是--workspace-concurrency。pnpm 默认会按 CPU 核心数并行执行 workspace 任务你可以显式限制为 1让所有子任务串行执行pnpm -r --workspace-concurrency1 run build第二个思路是不要用递归执行而是精确指定需要构建的包pnpm --filter your-team/web run buildfilter 只构建 web 这个子包不会把整个 workspace 都卷进来递归链路短了出现 FIRST_FAIL 的概率也就低了。还有一个无关并发但很实用的参数fetch-retries。如果你们的镜像源偶尔抽风增加重试次数能降低 install 过程中网络错误导致的失败pnpm config set fetch-retries 5 pnpm config set fetch-retry-factor 2注意这些参数只是降低失败概率不能根治并发问题。如果两个进程同时操作同一个 node_modules 目录改成串行顶多让失败时间错开但该失败还是会失败所以它更适合作为辅助手段。3.3 解法三测试环境也走 CI/CD把人工发布干掉我见过太多团队生产环境上了 Jenkins测试环境反而让测试工程师自己上服务器敲命令。你们这个场景两个测试工程师同时重启本质上就是因为发布入口完全靠人工没有任何编排系统做调度。如果条件允许最彻底的解法是把测试环境的发布也放进 CI/CD。流程变成这样测试工程师在 CI 平台上点击构建按钮或者推送代码触发流水线CI 在独立的 runner 上执行 pnpm install、build、部署runner 每次都是全新的环境不存在多个人同时操作同一台服务器的问题CI 平台的 job 并发控制天然解决冲突有人会说测试环境不值得搞那么重。其实不然测试环境的稳定性直接影响研发迭代效率。与其让开发、测试在服务器上踩踏不如花半天时间把发布流程固化下来。GitLab CI 里可以这样配置deploy-test: stage: deploy resource_group: frontend-deploy-test script: - pnpm install --frozen-lockfile - pnpm run build - scp -r dist/ usertest-server:/opt/frontend/ only: - testresource_group会保证同一时间同一个 group 下只有一个 job 在跑后触发的 job 会等待前一个完成。3.4 解法四临时绕过用于“现场救火”如果生产环境已经报错了服务起不来需要快速恢复有几个临时的绕过手段可以先用检查是否有其他人正在跑发布脚本等对方跑完再执行如果 node_modules 状态已经混乱先直接rm -rf node_modules再重新 install删除 pnpm 的临时锁文件rm -rf node_modules/.pnpm/lock.yaml以及 store 目录下的临时文件使用pnpm install --force强制重新关联硬链接这些手段只适合救火。如果每次报错都靠人工排队解决说明你们的发布机制已经到了必须改的时候了。4. 排查实录我是怎么一步步定位到并发问题的4.1 第一次看到报错时的错误判断我第一次遇到 ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL 的时候项目还是 pnpm 6 时代。当时第一反应是 pnpm 版本问题因为报错信息里带了一长串堆栈还有“recursive exec”这种字样总觉得是 pnpm 递归执行某个包的时候崩了。然后我跑了一堆操作升级 pnpm 版本、换镜像源、删掉整个 node_modules 重新 install。结果呢单跑不报错两个人同时跑还是报错。那个下午我盯着两行相似的日志看了很久才反应过来一个事两次报错的堆栈位置一模一样但报错真正的底层原因却不一样。第一次是ENOENT: no such file or directory, open xxx/node_modules/.pnpm/vue-router4.0.13/node_modules/vue-router/dist/vue-router.runtime.esm-bundler.js第二次是EPERM: operation not permitted, unlink xxx/node_modules/.pnpm/vite2.9.9/node_modules/vite/dist/node/chunks/dep-xxxx.js。一个是文件没了一个是文件删不掉但都被 pnpm 包装成了同一个上层错误。4.2 锁定并发因素的三个证据后来我梳理了一下确认是并发问题靠的是三个证据证据一单人执行时无论执行多少次都不报错。这个用排除法排掉了依赖缺失和代码问题。证据二报错出现的时间点非常固定集中在 build 和 install 的前半段。如果只是代码问题报错应该出现在 build 中后段才对。install 阶段报错 并发操作指向性非常明确。证据三用ps aux | grep node看进程列表能看到两个 node 进程同时在跑。一台服务器上同时有两个构建进程这就是最直接的实锤。4.3 给测试同学一份排查提问模板后来我在团队内部整理了一个报错登记模板遇到这类错误不用再猜按格式填信息就行操作时间几点几分执行的是否有人在同时操作提前确认过没有还是没注意完整报错堆栈贴关键几行报错前做了什么删了 node_modules改了 .npmrc单跑一次能不能复现不能复现的话十有八九是并发问题经过这么一轮规范测试工程师再遇到 ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL 时先查有没有其他人也在操作基本能秒定位。5. 这个报错给团队带来的工程化反思5.1 测试环境也要有发布纪律这次的报错表面上是 pnpm 的错误码问题实际上是测试环境发布流程缺少“互斥”意识。很多团队觉得测试环境怎么快怎么来结果是所有人共用一个服务器、一套 node_modules、一个 dist 目录谁先手谁赢后手的人一脸懵。我在见过很多团队踩了类似的坑之后形成了一个习惯凡是多人在同一台机器上操作的项目发布脚本里一律要带进程互斥或文件锁不管是前端还是后端。这个习惯能规避掉一整个类别的诡异问题不只是 pnpm 的 FIRST_FAIL。5.2 发布脚本里可以加上身份标识和日志落盘即使有了锁最好还是让每个发布动作都能被追踪。我在部分项目的发布脚本里加了这样的逻辑echo [$(date %Y-%m-%d %H:%M:%S)] deploy started by $(whoami) from $(hostname) /var/log/frontend-deploy.log这样如果出现问题能直接查是哪个人、哪台机器、什么时间执行过发布而不是靠群里问话考古。尤其在测试环境人多手杂的情况下日志落盘的价值很大能帮你快速定位是不是有人违规并行操作。你也可以在发布脚本里弹出确认提示让执行者输入姓名或工号作为这次发布的身份标识。别觉得繁琐这种“不便利”恰恰是推动流程规范化的一种办法。5.3 从这次报错反推 pnpm 的使用注意点最后说几个 pnpm 相关的避坑心得。pnpm 的硬链接机制决定了它对文件完整性非常敏感任何“意外中途中止”都可能导致依赖目录处于僵尸状态。所以我自己的习惯是不要在 install/build 进行中强杀进程如果非杀不可杀完之后一定完整删掉 node_modules 再重装不要手工去动 node_modules/.pnpm 里的文件pnpm 的目录结构是有语义的乱动一次可能引发连锁错误workspace 根目录的 package.json 里尽量少配置递归执行整个仓库的命令非要用的话加锁机制如果团队有人习惯用 npm 或 yarn在 pnpm 项目里混用包管理器也会导致 node_modules 结构错乱建议在 package.json 里加packageManager: pnpm8.15.0来约束这些点看着小实际遇到问题的时候排查成本真的很高。我在实际项目里逐步完善这套流程之后ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL 就再也没有出现在团队的聊天记录里。如果你现在正卡在这个报错上我建议你别急着换包管理器或者删缓存先回头看看你们的发布入口有没有并发保护。把锁加上让发布排队这个报错基本就不会再出现了。
返回列表