08 更新、回滚与故障排查
第一次上线成功后,最频繁的工作是发布新功能。更新流程必须能够回答:
构建失败怎么办?
启动失败怎么办?
数据库已经变更怎么办?
如何回到上一个可运行版本?
1. 最简单的手工更新流程
在小型学习项目中可以先使用:
备份当前可运行版本信息
→ 获取新代码
→ npm ci
→ 类型检查
→ 构建
→ migration(如有)
→ PM2 restart
→ 健康检查和日志检查
示意命令:
cd /var/www/nloop/current
git status
git pull --ff-only
npm ci
npm run typecheck
npm run build
npm prune --omit=dev
pm2 restart ecosystem.config.js \
--only nloop-api \
--update-env
pm2 logs nloop-api --lines 100 --nostream
curl -i http://127.0.0.1:8080/health
git pull --ff-only 会在无法快进时失败,避免服务器自动生成不受控的 merge commit。生产目录不应存在临时手改源码。
这种原地更新适合学习和低风险项目,但构建过程中目录处于变化状态。掌握后应过渡到独立 release 目录。
2. 为什么构建要在 restart 前完成
错误流程:
先停止旧服务
→ 再安装依赖
→ 再编译
→ 编译失败
→ 服务长时间不可用
更好的顺序:
旧服务保持运行
→ 新代码完成依赖安装和构建
→ 构建成功后才重启
即使是手工部署,也要尽量缩短真正的停机窗口。
3. restart 与 reload
当前教程使用:
instances: 1,
exec_mode: "fork",
所以优先:
pm2 restart nloop-api
PM2 的平滑 reload 优势主要用于 cluster/多个实例。单进程 fork 模式不要因为看到“0 秒停机”宣传就假设已经获得真正的零停机发布。
4. 环境变量更新
修改 ecosystem 或 shell 环境变量后:
pm2 restart ecosystem.config.js \
--only nloop-api \
--update-env
修改 .env 后也要重启应用,让 dotenv 重新加载。
不要把同一变量同时定义在多个地方:
.env 中 PORT=8080
ecosystem 中 PORT=8081
systemd 中 PORT=8082
否则最终值由加载顺序决定,排查非常困难。
5. 数据库 migration 的发布顺序
代码可以回滚,数据库结构不一定能轻易回滚。推荐向后兼容的渐进变更:
第一次发布
新增 nullable 字段或新表
旧代码仍能运行
第二次发布
新代码开始写入和读取新字段
后续发布
数据回填完成
确认没有旧代码
再增加 NOT NULL 或删除旧字段
不要在同一次发布一开始就删除旧代码仍需要的列。
多人、多语言共同访问数据库时,migration 必须进入统一版本管理和发布流程,不能依赖 Sequelize Model 自动猜测线上结构。
6. 简单可靠的 release 目录
进阶一步可以采用:
/var/www/nloop/
├── releases/
│ ├── 20260821-100000/
│ └── 20260821-120000/
├── shared/
│ └── .env
└── current -> releases/20260821-120000
每次发布在新目录完成:
获取代码
→ npm ci
→ typecheck
→ build
→ 安装/裁剪生产依赖
→ 链接 shared/.env
→ 切换 current 软链接
→ PM2 restart
ecosystem 中保持:
cwd: "/var/www/nloop/current",
这样 PM2 不需要知道具体时间戳目录。
7. 回滚的边界
代码回滚可以把 current 指回上一版本,再重启:
current → 上一个已验证 release
pm2 restart nloop-api
健康检查
但必须先确认:
- 数据库 migration 是否向后兼容;
- 新版本是否写入旧代码不认识的数据;
.env是否兼容旧版本;- 前端和后端 API 是否需要配套回滚;
- 队列中是否存在新格式任务。
“能够切回旧 JavaScript”不等于整个系统一定能够回滚。
8. PM2 高频故障
pm2: command not found
nvm current
which node
which pm2
检查是否切换了 Node 版本,或非交互 shell 没有加载 NVM。
应用一直 restarting
pm2 show nloop-api
pm2 logs nloop-api --lines 200 --nostream
常见原因:
.env缺失;- 数据库连接失败;
- 端口占用;
dist/server.js路径错误;- 运行依赖没有安装;
- 原生模块与 Node/Linux 不匹配;
- TypeScript 编译输出目录不符合预期。
PM2 online,但 API 失败
online 只说明进程存在。继续测试:
curl -i http://127.0.0.1:8080/health
curl -i http://127.0.0.1:8080/ready
/ready 可以检查数据库等关键依赖,但要设置短超时,不能让健康检查本身拖垮服务。
端口占用
ss -lntp | grep 8080
可能是之前手工运行的 node dist/server.js 没有停止,或存在另一个 PM2 用户的进程。
9. Nginx 高频故障
502 Bad Gateway
sudo tail -n 100 /var/log/nginx/nloop-error.log
pm2 list
curl -i http://127.0.0.1:8080/health
重点检查上游地址、端口和 Express 状态。
404,但 Express 中有路由
检查:
proxy_pass尾部斜杠;- Express 是否包含
/api前缀; - Nginx
location是否匹配; - 请求是否被前端 SPA 的
location /处理。
修改配置没有生效
sudo nginx -t
sudo systemctl reload nginx
sudo nginx -T
nginx -T 会输出完整生效配置,适合发现编辑了错误文件或配置没有被 include。
权限错误
Nginx worker 用户必须能读取静态文件和进入父目录,但不要通过全局 chmod 777 解决。检查每一级目录权限和实际 worker 用户。
10. 日志排查顺序
1. 浏览器/客户端错误和状态码
2. Nginx access log:请求是否到达
3. Nginx error log:代理是否失败
4. curl 127.0.0.1:8080:绕开 Nginx
5. PM2 list/show:进程是否稳定
6. PM2 应用日志:启动和业务错误
7. MySQL/Redis/下游服务状态
这种分层排查比同时修改 Nginx、PM2 和代码更有效。
11. 更新验收表
- [ ] 知道当前运行的 Git commit 或 release 编号;
- [ ] 新版本依赖安装成功;
- [ ] 类型检查和构建成功;
- [ ] migration 已评审且向后兼容;
- [ ] PM2 restart 后没有崩溃循环;
- [ ] 健康检查成功;
- [ ] 关键业务接口完成冒烟测试;
- [ ] PM2 和 Nginx error log 没有新增异常;
- [ ] 已明确代码和数据库回滚方案;
- [ ] 如进程定义变化,已重新
pm2 save。