记录日期:2026-02-12 PM2 重启 22 次 / 页面 404
rm -rf .next 才能恢复。过几天又坏。
典型错误日志:
# 编译产物丢失
Error: ENOENT: no such file or directory, open '.next/server/app-paths-manifest.json'
Error: ENOENT: no such file or directory, open '.next/server/app/_not-found/page.js'
Error: ENOENT: no such file or directory, open '.next/server/pages/_document.js'
Error: ENOENT: no such file or directory, open '.next/server/app/favicon.ico/route.js'
# Webpack 缓存文件损坏
[webpack.cache.PackFileCacheStrategy] Caching failed for pack:
Error: ENOENT: ...stat '.next/cache/webpack/client-development/3.pack.gz'
Error: ENOENT: ...stat '.next/cache/webpack/client-development/5.pack.gz'
# 幽灵配置重启
Found a change in next.config.js. Restarting the server to apply the changes...
Found a change in next.config.js. Restarting the server to apply the changes... (一小时内 3 次)
不是单一问题,而是三个独立问题形成的连锁反应:
问题配置:
// ecosystem.config.js
{
script: 'npm', // PM2 管理的是 npm
args: 'run dev', // npm 再启动 next dev
kill_timeout: 8000, // 8 秒后强制 SIGKILL
}
实际进程树:
PM2 (守护进程)
└── npm (PID 8499) ← PM2 追踪这个 PID
└── next dev (PID 8548) ← 实际的 Next.js 进程
信号传递失败机制:
sequenceDiagram
participant PM2 as PM2
participant NPM as npm (PID 8499)
participant NEXT as next dev (PID 8548)
participant CACHE as .next/cache
Note over NEXT,CACHE: Webpack 正在写 3.pack.gz ...
PM2->>NPM: SIGTERM (优雅退出信号)
Note over NPM: npm 收到 SIGTERM
Note over NPM: 但 npm 不可靠地转发信号
NPM--xNEXT: SIGTERM 可能丢失
Note over PM2: 等待 kill_timeout (8s)...
Note over NEXT,CACHE: Webpack 继续写缓存...
PM2->>NPM: SIGKILL (暴力杀死)
PM2->>NEXT: SIGKILL (杀死整个进程组)
Note over CACHE: 3.pack.gz 写到一半被截断
Note over CACHE: app-paths-manifest.json 丢失
Note over CACHE: _document.js 被删除但未重建完
npm 作为中间进程不会可靠地将 SIGTERM 转发给子进程。
这是一个 已知的 npm 行为。
PM2 只追踪 npm 的 PID,不知道真正的 next dev 进程在哪。
等 8 秒后 PM2 发 SIGKILL,所有进程被瞬间杀死——Webpack 正在写入的缓存文件就被截断损坏了。
// ecosystem.config.js - 矛盾的配置
max_memory_restart: '1G', // PM2: RSS 到 1GB 就杀进程
node_args: ['--max-old-space-size=2048'], // Node: V8 堆可以用到 2GB
| 配置项 | 值 | 含义 |
|---|---|---|
max_memory_restart | 1GB | PM2 在 RSS 到 1GB 时触发重启(SIGTERM → SIGKILL 链) |
--max-old-space-size | 2048MB | V8 引擎认为自己可以使用 2GB 内存 |
Node 认为自己有 2GB 可用,但 PM2 在 1GB 就会强杀。一个大型 Next.js 项目(596 个测试文件、大量页面和组件),dev server 编译时内存很容易超 1GB。
证据:PM2 显示进程已重启 22 次,很多次是内存触发的暴力重启。
// next.config.js
import { getCurrentEnvironment } from '../../config/environments.js'; // 引用项目根目录的文件
Next.js 会监听 next.config.js 及其依赖的变化。../../config/environments.js 位于项目根目录,任何触及该文件的操作都会让 Next.js 误判"配置变了",触发服务器自行重启。
日志中一小时内触发了 3 次:
23:11:09 Found a change in next.config.js. Restarting the server...
23:14:43 Found a change in next.config.js. Restarting the server...
23:29:24 Found a change in next.config.js. Restarting the server...
flowchart TD
A["config/environments.js 被触及\n(编辑器保存 / git 操作 / 脚本运行)"] --> B["Next.js 检测到 config 变化\n自行重启"]
B --> C["清空 .next/server/\n开始重新编译"]
C --> D["编译中大量页面\n内存快速增长"]
D --> E{"RSS > 1GB?"}
E -->|"是"| F["PM2 触发 max_memory_restart\n发送 SIGTERM → npm"]
E -->|"否"| G["编译完成\n暂时正常"]
F --> H["npm 不转发 SIGTERM\n8 秒后 SIGKILL"]
H --> I[".pack.gz 写入一半被截断\nmanifest.json 丢失"]
I --> J["PM2 自动重启\n读损坏的缓存"]
J --> K["更多编译错误\n更多 Full Reload\n更多内存消耗"]
K --> E
style A fill:#fff3cd
style F fill:#f8d7da
style H fill:#f8d7da
style I fill:#f8d7da
style K fill:#f8d7da
style G fill:#d4edda
原理:让 PM2 直接管理 next dev 进程,SIGTERM 能直接送达 Next.js,Next.js 有内置的优雅退出逻辑(完成当前编译、刷新缓存、关闭 WebSocket 连接等)。
// 修复前
{
script: 'npm',
args: 'run dev',
interpreter: 'none',
}
// 修复后
{
script: './node_modules/.bin/next',
args: 'dev --port 3001',
interpreter: 'none',
}
flowchart LR
subgraph "修复前 (信号断裂)"
PM2_OLD["PM2"] -->|"SIGTERM"| NPM["npm"]
NPM -.->|"不转发"| NEXT_OLD["next dev"]
PM2_OLD -->|"8s 后 SIGKILL"| NPM
end
subgraph "修复后 (信号直达)"
PM2_NEW["PM2"] -->|"SIGTERM"| NEXT_NEW["next dev"]
NEXT_NEW -->|"刷新缓存\n关闭连接"| EXIT["优雅退出"]
end
style NPM fill:#f8d7da
style NEXT_OLD fill:#f8d7da
style NEXT_NEW fill:#d4edda
style EXIT fill:#d4edda
原理:PM2 的 max_memory_restart 应该 >= Node 的 --max-old-space-size,否则 Node 还没认为需要 GC,PM2 就已经把它杀了。开发环境中 Next.js 内存高是正常的(热编译、模块缓存),应允许更大的内存使用。
// 修复前 (矛盾)
max_memory_restart: '1G', // PM2: 1GB 就杀
node_args: ['--max-old-space-size=2048'], // Node: 我能用 2GB
// 修复后 (对齐)
max_memory_restart: '2G', // PM2: 2GB 才杀(对齐 Node 设置)
node_args: ['--max-old-space-size=2048'], // Node: 可以用 2GB
原理:将外部配置在 import 时一次性读取并缓存,避免 Next.js 的文件监听器误判配置变更。同时对配置值做环境变量优先,减少对外部文件的运行时依赖。
// 修复前 — 直接 import 外部文件(会被 Next.js 文件监听器追踪)
import { getCurrentEnvironment } from '../../config/environments.js';
let envConfig;
try {
envConfig = getCurrentEnvironment();
} catch (error) {
envConfig = { api: { baseUrl: 'http://localhost:3000' }, web: { port: 3001 } };
}
// 修复后 — 环境变量优先,外部文件仅作 fallback
const API_BASE = process.env.NEXT_PUBLIC_API_URL || process.env.API_BASE_URL || 'http://localhost:3000';
const WEB_PORT = process.env.WEB_PORT || process.env.FRONTEND_PORT || '3001';
process.env 读取不触发文件监听import ... from '../../config/environments.js' 后,Next.js 不再追踪该文件| 验证项 | 命令 | 预期结果 |
|---|---|---|
| PM2 直接管理 next | ps -o pid,ppid,command $(pgrep -f "next dev") |
next dev 的 PPID 直接是 PM2 守护进程,不再经过 npm |
| 信号传递正常 | pm2 restart celoria-frontend && pm2 logs celoria-frontend --lines 5 |
无 ENOENT 或 Caching failed 错误 |
| 内存不再频繁触发重启 | pm2 describe celoria-frontend | grep restarts |
重启次数不再快速增长 |
| 无幽灵配置重启 | pm2 logs celoria-frontend --lines 100 | grep "change in next.config" |
无匹配结果 |
npm run 作为 PM2 的 script — npm 不转发信号,用 node_modules/.bin/xxx 直接启动目标进程max_memory_restart 必须 >= --max-old-space-size — 否则 Node 还在合理使用内存,PM2 就把它杀了next.config.js 尽量不 import 外部文件 — 因为 Next.js 的文件监听器会追踪所有依赖,任何变更都触发服务重启npm run xxx 脚本都有信号丢失风险。包括 Storybook (npx storybook dev)、Vite (npm run dev) 等。Landing Page 的 PM2 配置也应检查。