Next.js 修复 NFS 构建目录锁:能启动,不等于可以并发写同一个 .next

Next.js 报告构建目录无法加锁时,问题未必是上一次构建留下了一个文件。可能确实有另一个进程在使用目录,也可能是网络文件系统的锁服务出了问题。把这些情况都处理成“删锁文件再重试”,会掩盖真正的并发风险。

Next.js 在 UTC 2026 年 10 月 24 日发布的 16.5.0-canary.27 中收录了一项修复,专门处理 NFS 等文件系统上有限或缺失的锁支持。上游现在把错误分类后再决定等待、报错或兼容回退。它仍是 canary 版本的变化,不能据此声称当前稳定版已包含修复,也不代表共享构建目录变得安全。

这里的锁,不是依赖锁文件

package-lock.json、pnpm-lock.yaml 等依赖锁文件描述依赖解析结果。此次修复处理的是 Next.js 输出目录的进程互斥,目的是避免多个实例同时改写同一份构建产物。

开发服务和构建过程会读写缓存、生成文件及运行状态。若两个独立进程同时操作同一个输出目录,文件本身即使没有损坏,也可能出现互相覆盖、缓存与代码不一致、读到尚未完成的产物等问题。

目录里的锁文件路径与操作系统实际持有的锁也不是同一件事。看到路径存在,不能直接推断有一个失效的进程锁;删除路径同样不能证明其他进程已经停止写入。

因此,处理这类报错应先确认谁在使用构建目录、是否有重复启动,以及工作目录和挂载点是否符合预期,而不是默认去掉互斥保护。

从一个笼统失败,拆成可判断的结果

上游合并请求 #100684 修改了 Rust 原生绑定和 TypeScript 包装层。关键变化是让加锁结果成为明确的数据,而不只是“拿到了锁”“没拿到锁”或抛出一个笼统异常。

原生绑定的结果包括 acquired、wouldBlock 和 error。错误中又带有 message、transient、unsupported,让 JavaScript 层知道失败发生在哪条路径。

情况 修复后的处理思路 不能据此推断的结论
成功加锁 返回持有真实锁的对象 其他目录或进程也已自动协调
锁被占用 返回 wouldBlock,按现有流程短暂重试 一定是陈旧文件,可以删除
暂时性加锁错误 有限重试,失败后保留具体错误 文件系统根本不支持锁
明确不支持锁 警告后返回不持有真实锁的兼容对象 多实例写同一目录已受到保护

这是此次修复最值得关注的地方。不同错误需要不同恢复策略;如果都被归成“加锁失败”,应用很难兼顾可用性与正确性。

ENOLCK 不等于永远没有锁能力

Rust 侧将锁获取被中断的错误标为可重试。代码注释说明,即使采用不等待竞争锁的模式,NFS 仍可能等待锁管理器的 RPC,而这个等待可能被信号中断。

在 Linux 和 macOS 上,修复还将 ENOLCK 归为暂时性错误。它可能来自锁服务暂时不可用或锁资源耗尽,不能单凭这个错误就宣布文件系统不支持锁。

与之对应,只有明确属于 Unsupported 的错误才设置不支持标记。打开文件、写入内容等其他 I/O 故障也没有被一律标成可重试,更没有一律变成绕过锁。

TypeScript 包装层的重试间隔仍为 10 毫秒,总体重试窗口为 1000 毫秒。这是源码中的重试控制参数,不是所有失败都能在一秒内恢复的性能保证,也不能把它当成网络文件系统操作的严格超时。

暂时性错误在窗口内没有恢复时,流程会输出相应错误并退出。权限问题、路径问题或底层 I/O 故障,应按它们自己的原因处理,重试不能代替修复。

不支持锁时,继续运行有明确代价

对于明确不支持锁的情况,包装层会打印警告,然后创建一个没有原生锁的兼容对象。这让受限环境不必仅因为缺少加锁能力就立即失败,行为与已有的部分兼容路径相似。

但这个对象没有实际提供互斥。上游警告明确要求避免多个 Next.js 进程使用同一个构建目录。程序能继续启动,表示兼容性提高了;它没有证明目录现在可以被多个构建者安全共享。

团队如果确实运行在这种环境中,应由调度和目录隔离保证单写者。不能把一个警告当成底层已经实现了分布式锁,也不应让监控只记录“进程启动成功”,却忽略保护能力已经降低。

上游讨论还提到未来希望移除 experimental.lockDistDir 这个开关。但这份补丁本身不是开关已删除的证据,不能据此给现有项目下配置迁移结论。

多个智能体或 CI 任务,先隔离输出目录

多人、多个智能体或并行 CI 共用代码时,容易把“源码能共享”误认为“所有运行状态也能共享”。构建目录是可变状态,不适合让多个独立进程任意交错写入。

如果多个参与者只是查看同一个工作副本的开发效果,可以评估共用一个开发服务。若它们分别修改不同分支、独立构建或运行不同配置,则更适合使用独立工作副本和输出目录。

通过符号链接把多个工作副本的 .next 指向同一位置,并不能因为路径看起来不同就获得隔离。真正需要区分的是最终写入位置、进程生命周期和产物归属。

CI 中也应检查容器卷、共享工作区和并行任务的实际路径。恢复一份缓存与多个运行中任务同时修改一个活动目录,是两种不同的使用方式。不能仅凭“缓存可以复用”,就让构建直接复用同一个可写目录。

这项修复针对正确性和错误处理,不提供网络文件系统上的性能承诺。Next.js 和 Turbopack 的大量文件访问仍可能受到网络往返、元数据操作及缓存一致性开销影响。

如何验证,而不是直接升级生产环境

这次新闻来源是 canary 发布记录及合并代码。本文没有安装该预发布版本,也没有在 NFS 环境运行复现测试,因此不提供未验证的吞吐量、故障率或升级收益数字。

有相关问题的团队可以先保存当前版本、文件系统类型、挂载方式、完整错误信息和并行进程情况。再在隔离环境比较原版本与包含修复的版本,确认遇到的是锁竞争、暂时性错误还是明确不支持锁。

测试应同时验证两件事:单进程能否按预期启动,以及真实支持锁的环境中,第二个进程是否仍受到互斥约束。若走到兼容回退,还要验证外围调度确实不会启动第二个写入者。

普通本地文件系统上运行正常的项目,没有必要仅凭这篇文章就切换 canary。是否升级应结合版本发布渠道、项目回归测试和稳定版后续收录情况决定。

结语

此次修复把几个表面相似、原因不同的加锁失败分开处理,让暂时性问题有机会恢复,也让受限文件系统得到明确的兼容行为。

最重要的边界仍然没有改变:继续运行与获得互斥不是一回事。面对构建目录锁问题,先识别真实进程和文件系统能力,再决定隔离、重试或升级,比把锁文件当成障碍删除掉更容易保持结果可靠。

参考来源

资料核对日期为 2026 年 10 月 26 日。文章基于上游静态代码分析,具体行为可能随后续版本调整。

文章目录
  1. 1. 这里的锁,不是依赖锁文件
  2. 2. 从一个笼统失败,拆成可判断的结果
  3. 3. ENOLCK 不等于永远没有锁能力
  4. 4. 不支持锁时,继续运行有明确代价
  5. 5. 多个智能体或 CI 任务,先隔离输出目录
  6. 6. 如何验证,而不是直接升级生产环境
  7. 7. 结语
  8. 8. 参考来源
|