09 完成首次上线后的渐进改进

这一章不是首次上线的前置条件。先让单进程版本稳定运行,再按实际问题逐步增加能力。

1. 第一阶段:必须掌握

这些能力比立刻开启 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");
});

真实工程还要处理:

PM2 可配置退出等待时间:

{
  kill_timeout: 12_000,
}

它应略大于应用自己的优雅关闭期限。不要设置几十分钟来掩盖无法结束的任务。

3. 健康检查和就绪检查

Liveness

回答进程是否还能响应:

GET /health
{
  "status": "ok"
}

Readiness

回答当前实例是否适合接收业务请求:

GET /ready

可以检查关键数据库连接,但必须:

4. 崩溃重启保护

ecosystem 可以逐步加入:

{
  autorestart: true,
  restart_delay: 1000,
  min_uptime: "5s",
  max_restarts: 10,
}

作用是避免应用启动即崩溃时无休止高频重启。具体值应根据启动时间调整。

PM2 自动重启只能恢复进程,不能修复:

5. 内存限制

可以设置:

{
  max_memory_restart: "500M",
}

达到阈值后 PM2 会尝试重启应用。这是一道保护,不是内存泄漏解决方案。

需要先通过监控了解正常基线,再决定阈值。文件上传的 MemoryStorage、大型 findAll() 和 JSON 序列化都可能造成内存峰值。

6. HTTPS

正式公网服务应由 Nginx 提供 HTTPS:

浏览器 HTTPS
  → Nginx 证书
  → HTTP 127.0.0.1:8080

配置完成后还要考虑:

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. 集中式日志和监控

单机日志足够完成首次上线。服务器增加后可以考虑:

需要关注:

11. 安全基础

12. 推荐成长顺序

1. fork 单进程稳定上线
2. 健康检查和优雅关闭
3. HTTPS 和日志轮转
4. release 目录与回滚
5. 监控和告警
6. 后台任务
7. cluster 或多服务器
8. CI/CD 和集中式可观测性

每次只增加一个新变量,并验证它解决了什么问题。

复盘题

  1. PM2 自动重启为什么不能代替修复内存泄漏?
  2. Liveness 和 Readiness 有什么区别?
  3. 使用 cluster 前为什么要检查 Session、cron 和限流?
  4. 哪些接口更适合改成 202 + jobId
  5. 为什么应先掌握手工发布,再自动化 CI/CD?

官方参考