09 完成首次上线后的渐进改进
这一章不是首次上线的前置条件。先让单进程版本稳定运行,再按实际问题逐步增加能力。
1. 第一阶段:必须掌握
- TypeScript 能稳定编译;
node dist/server.js可以独立运行;- PM2 fork 单进程启动;
- Nginx 反向代理;
- PM2 和 Nginx 日志可查;
- PM2 systemd 开机启动;
/health健康检查;.env不进入 Git;- 8080 不直接暴露公网;
- 更新失败时知道如何回到上一个版本。
这些能力比立刻开启 cluster 更重要。
2. 优雅关闭
PM2 restart、systemd stop 和发布更新都会要求 Node.js 退出。应用应该停止接收新连接,等待正在处理的请求,并关闭数据库资源。
示意代码:
import type { Server } from "node:http";
const server: Server = app.listen(port, host, () => {
logger.info({ host, port }, "server started");
});
let shuttingDown = false;
async function shutdown(signal: string): Promise<void> {
if (shuttingDown) {
return;
}
shuttingDown = true;
logger.info({ signal }, "server is shutting down");
const forceExitTimer = setTimeout(() => {
logger.error("graceful shutdown timed out");
process.exit(1);
}, 10_000);
forceExitTimer.unref();
server.close(async (error?: Error) => {
try {
await sequelize.close();
if (error !== undefined) {
logger.error({ error }, "HTTP server close failed");
process.exitCode = 1;
}
} finally {
clearTimeout(forceExitTimer);
process.exit();
}
});
}
process.once("SIGINT", () => {
void shutdown("SIGINT");
});
process.once("SIGTERM", () => {
void shutdown("SIGTERM");
});
真实工程还要处理:
- Redis 连接;
- BullMQ worker;
- 正在执行的事务;
- 长连接和 WebSocket;
- 未完成的文件写入。
PM2 可配置退出等待时间:
{
kill_timeout: 12_000,
}
它应略大于应用自己的优雅关闭期限。不要设置几十分钟来掩盖无法结束的任务。
3. 健康检查和就绪检查
Liveness
回答进程是否还能响应:
GET /health
{
"status": "ok"
}
Readiness
回答当前实例是否适合接收业务请求:
GET /ready
可以检查关键数据库连接,但必须:
- 使用很短的 timeout;
- 不执行昂贵 SQL;
- 不泄露数据库地址和错误详情;
- 不因一个非关键依赖短暂异常制造雪崩。
4. 崩溃重启保护
ecosystem 可以逐步加入:
{
autorestart: true,
restart_delay: 1000,
min_uptime: "5s",
max_restarts: 10,
}
作用是避免应用启动即崩溃时无休止高频重启。具体值应根据启动时间调整。
PM2 自动重启只能恢复进程,不能修复:
- 错误 migration;
- 数据库不可用;
- 内存泄漏根因;
- 逻辑错误;
- 磁盘写满。
5. 内存限制
可以设置:
{
max_memory_restart: "500M",
}
达到阈值后 PM2 会尝试重启应用。这是一道保护,不是内存泄漏解决方案。
需要先通过监控了解正常基线,再决定阈值。文件上传的 MemoryStorage、大型 findAll() 和 JSON 序列化都可能造成内存峰值。
6. HTTPS
正式公网服务应由 Nginx 提供 HTTPS:
浏览器 HTTPS
→ Nginx 证书
→ HTTP 127.0.0.1:8080
配置完成后还要考虑:
- HTTP 自动跳转 HTTPS;
- 证书自动续期;
- Secure Cookie;
- Express
trust proxy; - HSTS 应在完全确认 HTTPS 后再开启。
7. PM2 cluster 何时再使用
当单个 Node.js 进程无法利用多核,且应用已经满足无状态或共享状态要求时,再评估:
{
instances: "max",
exec_mode: "cluster",
}
启用前检查:
Session
进程 A 内存 Session
≠ 进程 B 内存 Session
需要 Redis 等共享存储。
定时任务
每个进程都执行一次 cron,可能导致重复任务。应使用单独 worker、分布式锁或队列调度。
文件
多个进程不能无协调地写同一文件。用户文件更适合对象存储或明确的文件服务。
限流
进程内计数器不共享。多进程/多服务器应使用 Redis store。
WebSocket
需要评估连接分配、粘性会话和共享广播状态。
8. 后台任务
以下工作不适合长期占用普通 HTTP 请求:
- 大型报表;
- 批量邮件;
- 图片或视频处理;
- 大量数据导入;
- 跨系统长流程。
可以改为:
HTTP 请求
→ 写入任务队列
→ 返回 202 + jobId
→ BullMQ worker 处理
→ 客户端查询状态或接收通知
API 和 worker 可以作为 ecosystem 中两个独立应用,但要先解决任务幂等、重试和失败状态。
9. 发布目录与自动化
当手工发布步骤已经稳定,可以自动化:
CI 执行测试和构建
→ 生成带版本号的 release
→ 上传到服务器
→ 安装生产依赖
→ 切换 current
→ PM2 restart/reload
→ 健康检查
→ 失败自动切回旧 release
不要在手工流程尚不明确时直接写复杂 CI/CD;自动化只会更快地执行已有流程,包括错误流程。
10. 集中式日志和监控
单机日志足够完成首次上线。服务器增加后可以考虑:
- Loki + Grafana;
- Elastic Stack;
- 云厂商日志服务;
- OpenTelemetry;
- 错误追踪平台。
需要关注:
- 请求 P50/P95/P99;
- 5xx 和 timeout;
- PM2 restart 次数;
- CPU、RSS、事件循环延迟;
- MySQL 连接池等待和慢查询;
- Redis 和任务队列积压;
- Nginx 502/504;
- 磁盘和日志增长。
11. 安全基础
- Node.js 应用使用普通用户运行;
- 只开放必要端口;
- Express 监听本机地址;
- 生产使用 HTTPS;
.env限制权限并安全备份;- 定期更新 Node、PM2、Nginx 和 npm 依赖;
- 上传目录不能执行程序;
- 日志不得包含密码和 Token;
- MySQL 账号使用最小权限;
- 数据库和用户文件需要独立备份与恢复演练。
12. 推荐成长顺序
1. fork 单进程稳定上线
2. 健康检查和优雅关闭
3. HTTPS 和日志轮转
4. release 目录与回滚
5. 监控和告警
6. 后台任务
7. cluster 或多服务器
8. CI/CD 和集中式可观测性
每次只增加一个新变量,并验证它解决了什么问题。
复盘题
- PM2 自动重启为什么不能代替修复内存泄漏?
- Liveness 和 Readiness 有什么区别?
- 使用 cluster 前为什么要检查 Session、cron 和限流?
- 哪些接口更适合改成
202 + jobId? - 为什么应先掌握手工发布,再自动化 CI/CD?