Node.js: 部署与优化

最后更新:2026-08-26

Charlie 用 node app.js 把 API 部署到生产服务器后,经常因为未捕获异常导致进程崩溃,而无人值守时服务长时间中断。他引入 PM2 实现自动重启与集群模式,搭配 Docker 容器化和 Nginx 反向代理,服务可用性从 95% 提升到 99.9%,终于睡上了安稳觉。

你将学到:


1. PM2 进程管理

(1) 为什么需要进程管理器

生产环境中 Node.js 进程可能因未捕获异常、内存泄漏或系统资源不足而崩溃。直接用 node app.js 启动,进程崩溃后无人重启,导致服务中断。PM2 是 Node.js 最流行的进程管理器,提供自动重启、日志管理、负载均衡和监控功能。

▶ 示例:(2) 安装与基本命令

BASH
npm install -g pm2
BASH
pm2 start app.js --name "my-api"
pm2 restart my-api
pm2 stop my-api
pm2 delete my-api
pm2 logs my-api
pm2 monit

(3) PM2 常用命令速查

命令 作用 常用场景
pm2 start app.js 启动应用 首次部署
pm2 restart <name> 重启应用 代码更新后
pm2 reload <name> 零停机重启 生产环境更新
pm2 stop <name> 停止应用 维护期间
pm2 delete <name> 删除进程 彻底移除
pm2 logs [name] 查看日志 排查问题
pm2 monit 实时监控面板 观察资源占用
pm2 list 进程列表 查看运行状态
pm2 describe <name> 进程详情 深入诊断
pm2 save 保存进程列表 开机自启配置
pm2 startup 生成开机启动脚本 服务器重启后自动恢复

▶ 示例:PM2 启动与监控

BASH
pm2 start app.js --name "charlie-api" -i max
pm2 monit
pm2 save
pm2 startup
TEXT 📖 仅展示
┌─────┬──────────────┬─────────────┬─────────┬─────────┬──────────┐
│ id  │ name         │ mode        │ ↺       │ status  │ cpu      │
├─────┼──────────────┼─────────────┼─────────┼─────────┼──────────┤
│ 0   │ charlie-api  │ cluster     │ 15      │ online  │ 12%      │
│ 1   │ charlie-api  │ cluster     │ 2       │ online  │ 8%       │
│ 2   │ charlie-api  │ cluster     │ 0       │ online  │ 5%       │
│ 3   │ charlie-api  │ cluster     │ 1       │ online  │ 3%       │
└─────┴──────────────┴─────────────┴─────────┴─────────┴──────────┘


2. Docker 基础

(1) 为什么使用 Docker

Docker 将应用及其依赖打包为容器镜像,确保开发、测试、生产环境一致。Charlie 之前在本地能跑通的代码,部署到服务器后因为 Node.js 版本差异而报错,Docker 彻底解决了这个问题。

▶ 示例:(2) Dockerfile 编写

DOCKERFILE
FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=3s \
  CMD node healthcheck.js

USER node

CMD ["node", "app.js"]

▶ 示例:(3) docker-compose 多服务编排

YAML
version: "3.8"

services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - MONGO_URI=mongodb://mongo:27017/myapp
      - REDIS_URL=redis://redis:6379
    depends_on:
      - mongo
      - redis
    restart: always
    healthcheck:
      test: ["CMD", "node", "healthcheck.js"]
      interval: 30s
      timeout: 3s
      retries: 3

  mongo:
    image: mongo:7
    volumes:
      - mongo-data:/data/db
    restart: always

  redis:
    image: redis:7-alpine
    restart: always

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - api
    restart: always

volumes:
  mongo-data:

(4) 部署方式对比

维度 裸机部署 Docker 部署 云服务部署
环境一致性
部署速度 最快
运维复杂度
资源利用率 按需
扩展灵活性
成本
适用场景 小型项目 中大型项目 企业级项目
学习门槛

▶ 示例:构建与运行 Docker 镜像

BASH
docker build -t charlie-api:1.0 .
docker run -d -p 3000:3000 --env-file .env charlie-api:1.0
docker-compose up -d
docker-compose logs -f api


3. 环境变量管理

(1) 为什么需要环境变量

不同环境(开发、测试、生产)的数据库地址、端口号、密钥等配置各不相同。硬编码配置会导致安全隐患和环境切换困难。环境变量让配置与代码分离,符合十二因素应用方法论。

▶ 示例:(2) dotenv 与 .env 文件

JAVASCRIPT
const dotenv = require('dotenv');

dotenv.config({ path: `.env.${process.env.NODE_ENV || 'development'}` });

const config = {
  port: parseInt(process.env.PORT, 10) || 3000,
  mongoUri: process.env.MONGO_URI,
  jwtSecret: process.env.JWT_SECRET,
  redisUrl: process.env.REDIS_URL,
  logLevel: process.env.LOG_LEVEL || 'info',
};

module.exports = config;
▶ 试一试

▶ 示例:(3) 生产环境配置策略

TEXT 📖 仅展示
.env                # 默认配置(不提交 Git)
.env.development    # 开发环境
.env.test           # 测试环境
.env.production     # 生产环境(通过 CI/CD 注入,不存代码仓库)

(4) 环境变量管理方式对比

方式 安全性 灵活性 团队协作 适用场景
.env 文件 通用项目
系统环境变量 简单部署
Docker secrets Docker 环境
K8s Secrets Kubernetes
云平台配置中心 云原生项目

▶ 示例:多环境配置加载

JAVASCRIPT
const path = require('path');
const dotenv = require('dotenv');

const env = process.env.NODE_ENV || 'development';

const envFile = path.resolve(process.cwd(), `.env.${env}`);
dotenv.config({ path: envFile });

if (env === 'development') {
  dotenv.config({ path: path.resolve(process.cwd(), '.env') });
}

const required = ['MONGO_URI', 'JWT_SECRET'];
const missing = required.filter(key => !process.env[key]);

if (missing.length) {
  throw new Error(`Missing required env vars: ${missing.join(', ')}`);
}

module.exports = {
  env,
  port: parseInt(process.env.PORT, 10) || 3000,
  mongoUri: process.env.MONGO_URI,
  jwtSecret: process.env.JWT_SECRET,
  redisUrl: process.env.REDIS_URL,
  logLevel: process.env.LOG_LEVEL || 'info',
};
▶ 试一试

4. 集群模式

(1) 为什么需要集群

Node.js 是单线程的,一个 CPU 核心只能运行一个实例。集群模式利用 cluster 模块创建多个工作进程,充分利用多核 CPU,显著提升吞吐量和可用性。

(2) cluster 模块原理

主进程(Master)负责监听端口并分发请求,工作进程(Worker)处理实际业务。多个 Worker 共享同一个端口,实现负载均衡。

▶ 示例:(3) 手动实现集群

JAVASCRIPT
const cluster = require('cluster');
const os = require('os');
const http = require('http');

if (cluster.isPrimary) {
  const numCPUs = os.cpus().length;
  console.log(`Master ${process.pid} is running`);
  console.log(`Forking ${numCPUs} workers...`);

  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} died. Restarting...`);
    cluster.fork();
  });
} else {
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end(`Handled by worker ${process.pid}\n`);
  }).listen(3000);

  console.log(`Worker ${process.pid} started`);
}
▶ 试一试

(4) PM2 集群模式

PM2 内置集群支持,无需修改代码即可启用:

BASH
pm2 start app.js -i max
pm2 start app.js -i 4

▶ 示例:集群模式性能对比

BASH
node single.js &
ab -n 10000 -c 100 http://localhost:3000/
TEXT 📖 仅展示
Single process:
  Requests per second:    3254.21 [#/sec]

Cluster (4 workers):
  Requests per second:    11280.67 [#/sec]


5. 性能分析

(1) console.time 基础计时

最简单的性能测量方式,适合快速定位慢操作:

JAVASCRIPT
app.get('/api/users', async (req, res) => {
  console.time('fetch-users');
  const users = await User.find().lean();
  console.timeEnd('fetch-users');
  res.json(users);
});

(2) Performance Hooks 精确测量

Node.js 内置的 perf_hooks 模块提供高精度计时:

JAVASCRIPT
const { performance, PerformanceObserver } = require('perf_hooks');

const obs = new PerformanceObserver((list) => {
  const entries = list.getEntries();
  entries.forEach((entry) => {
    console.log(`${entry.name}: ${entry.duration.toFixed(2)}ms`);
  });
});
obs.observe({ type: 'measure', buffered: true });

function measureAsync(label, fn) {
  return async (...args) => {
    performance.mark(`${label}-start`);
    const result = await fn(...args);
    performance.mark(`${label}-end`);
    performance.measure(label, `${label}-start`, `${label}-end`);
    return result;
  };
}

const fastQuery = measureAsync('db-query', async () => {
  return await User.find().lean();
});

(3) Clinic.js 专业诊断

Clinic.js 是 Node.js 官方推荐的性能诊断工具套件:

BASH
npm install -g clinic
clinic doctor -- node app.js
clinic flame -- node app.js
clinic bubbleprof -- node app.js

(4) 性能优化策略对比

策略 工具/方法 适用场景 难度 效果
计时分析 console.time 快速定位
精确测量 perf_hooks 关键路径
CPU 分析 Clinic Flame CPU 密集型
事件循环 Clinic Doctor I/O 阻塞
内存泄漏 heapdump / memwatch 内存问题
负载测试 autocannon / ab 容量规划
APM 监控 New Relic / Datadog 持续监控

▶ 示例:使用 autocannon 负载测试

BASH
npm install -g autocannon
autocannon -c 100 -d 10 http://localhost:3000/api/health
TEXT 📖 仅展示
┌─────────┬──────┬──────┬───────┬──────┬───────┬───────┬───────┐
│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%  │ Avg   │ Stdev │ Max   │
├─────────┼──────┼──────┼───────┼──────┼───────┼───────┼───────┤
│ Latency │ 2 ms │ 4 ms │ 12 ms │ 18ms │ 5 ms  │ 3 ms  │ 45 ms │
└─────────┴──────┴──────┴───────┴──────┴───────┴───────┴───────┘

Requests/sec: 18523.6


6. 健康检查端点

(1) 为什么需要健康检查

容器编排系统(Docker、K8s)和负载均衡器需要知道服务是否正常。健康检查端点提供标准化的服务状态探测接口,实现自动故障检测和流量切换。

▶ 示例:(2) 基础健康检查

JAVASCRIPT
app.get('/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
▶ 试一试

▶ 示例:(3) 深度健康检查

JAVASCRIPT
app.get('/health', async (req, res) => {
  const checks = {
    server: 'ok',
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    memory: process.memoryUsage(),
  };

  try {
    await mongoose.connection.db.admin().ping();
    checks.database = 'ok';
  } catch (err) {
    checks.database = 'error';
  }

  try {
    await redisClient.ping();
    checks.redis = 'ok';
  } catch (err) {
    checks.redis = 'error';
  }

  const isHealthy = checks.database === 'ok' && checks.redis === 'ok';
  res.status(isHealthy ? 200 : 503).json(checks);
});
▶ 试一试

(4) readiness 与 liveness 分离

生产环境中通常将健康检查分为两种:liveness(进程是否存活)和 readiness(是否准备好接收流量):

JAVASCRIPT
app.get('/healthz', (req, res) => {
  res.json({ status: 'alive' });
});

app.get('/readyz', async (req, res) => {
  try {
    await mongoose.connection.db.admin().ping();
    await redisClient.ping();
    res.json({ status: 'ready' });
  } catch {
    res.status(503).json({ status: 'not ready' });
  }
});

▶ 示例:Docker 健康检查脚本

JAVASCRIPT
const http = require('http');

const options = {
  hostname: 'localhost',
  port: process.env.PORT || 3000,
  path: '/healthz',
  timeout: 2000,
};

const req = http.request(options, (res) => {
  if (res.statusCode === 200) {
    process.exit(0);
  } else {
    process.exit(1);
  }
});

req.on('error', () => process.exit(1));
req.on('timeout', () => { req.destroy(); process.exit(1); });
req.end();
▶ 试一试

7. Nginx 反向代理

(1) 为什么需要 Nginx

Nginx 作为反向代理,提供 SSL 终止、负载均衡、静态资源服务和请求限流等功能。Node.js 专注业务逻辑,Nginx 处理网络层优化,各司其职。

▶ 示例:(2) Node.js 生产部署架构

100%
graph LR
    Client[客户端] --> Nginx[Nginx 反向代理<br/>:80/:443]
    Nginx --> PM2[PM2 集群管理]
    PM2 --> W1[Worker 1<br/>:3000]
    PM2 --> W2[Worker 2<br/>:3000]
    PM2 --> W3[Worker 3<br/>:3000]
    PM2 --> W4[Worker 4<br/>:3000]
    W1 --> DB[(MongoDB)]
    W2 --> DB
    W3 --> Redis[(Redis)]
    W4 --> Redis

▶ 示例:(3) Nginx 配置

TEXT 📖 仅展示
upstream nodejs_backend {
    least_conn;
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
    keepalive 64;
}

server {
    listen 80;
    server_name api.example.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name api.example.com;

    ssl_certificate     /etc/ssl/certs/api.example.com.crt;
    ssl_certificate_key /etc/ssl/certs/api.example.com.key;

    location / {
        proxy_pass http://nodejs_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }

    location /health {
        proxy_pass http://nodejs_backend/health;
        access_log off;
    }

    location /static/ {
        alias /app/public/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;
    limit_req zone=api burst=50 nodelay;
}

▶ 示例:Docker 中 Nginx 反向代理配置

YAML
# docker-compose.yml 片段
nginx:
  image: nginx:alpine
  ports:
    - "80:80"
    - "443:443"
  volumes:
    - ./nginx.conf:/etc/nginx/nginx.conf:ro
    - ./ssl:/etc/ssl/certs:ro
  depends_on:
    api:
      condition: service_healthy
  restart: always


8. 综合示例:完整部署配置

Charlie 把所有部署配置整合到一起,实现了从代码到生产的全流程自动化。

▶ 示例:项目目录结构

TEXT 📖 仅展示
charlie-api/
├── app.js
├── healthcheck.js
├── config/
│   └── index.js
├── ecosystem.config.js
├── Dockerfile
├── docker-compose.yml
├── nginx.conf
├── .env.example
├── .dockerignore
└── package.json

▶ 示例:PM2 ecosystem.config.js

JAVASCRIPT 📖 仅展示
module.exports = {
  apps: [
    {
      name: 'charlie-api',
      script: 'app.js',
      instances: 'max',
      exec_mode: 'cluster',
      autorestart: true,
      watch: false,
      max_memory_restart: '512M',
      env_development: {
        NODE_ENV: 'development',
        PORT: 3000,
      },
      env_production: {
        NODE_ENV: 'production',
        PORT: 3000,
      },
      error_file: './logs/error.log',
      out_file: './logs/out.log',
      merge_logs: true,
      log_date_format: 'YYYY-MM-DD HH:mm:ss',
      max_restarts: 10,
      restart_delay: 4000,
      kill_timeout: 5000,
      listen_timeout: 10000,
    },
  ],
  deploy: {
    production: {
      user: 'deploy',
      host: 'api.example.com',
      ref: 'origin/main',
      repo: 'git@github.com:charlie/api.git',
      path: '/var/www/charlie-api',
      'pre-deploy-local': '',
      'post-deploy':
        'npm ci && pm2 reload ecosystem.config.js --env production',
      'pre-setup': '',
    },
  },
};
逻辑代码 42 行(超过 40 行限制,仅展示)

▶ 示例:Dockerfile(多阶段构建优化)

DOCKERFILE
FROM node:20-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build 2>/dev/null || true

FROM node:20-alpine AS production

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY --from=builder /app ./

RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
USER nodejs

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD node healthcheck.js

CMD ["node", "app.js"]

▶ 示例:.env.example

TEXT 📖 仅展示
NODE_ENV=production
PORT=3000
MONGO_URI=mongodb://mongo:27017/charlie-api
REDIS_URL=redis://redis:6379
JWT_SECRET=your-secret-key-here
LOG_LEVEL=info
CORS_ORIGIN=https://example.com
RATE_LIMIT_WINDOW=60000
RATE_LIMIT_MAX=100

▶ 示例:healthcheck.js

JAVASCRIPT
const http = require('http');

const req = http.request(
  {
    hostname: '127.0.0.1',
    port: parseInt(process.env.PORT, 10) || 3000,
    path: '/healthz',
    timeout: 2000,
  },
  (res) => {
    process.exit(res.statusCode === 200 ? 0 : 1);
  }
);

req.on('error', () => process.exit(1));
req.on('timeout', () => { req.destroy(); process.exit(1); });
req.end();
▶ 试一试

▶ 示例:app.js 中的健康检查端点

JAVASCRIPT 📖 仅展示
const express = require('express');
const mongoose = require('mongoose');
const config = require('./config');

const app = express();
app.use(express.json());

app.get('/healthz', (req, res) => {
  res.json({ status: 'alive', pid: process.pid });
});

app.get('/readyz', async (req, res) => {
  try {
    await mongoose.connection.db.admin().ping();
    res.json({ status: 'ready', pid: process.pid, uptime: process.uptime() });
  } catch {
    res.status(503).json({ status: 'not ready', pid: process.pid });
  }
});

app.get('/health', async (req, res) => {
  const checks = {
    status: 'ok',
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    memory: process.memoryUsage(),
    pid: process.pid,
  };

  try {
    await mongoose.connection.db.admin().ping();
    checks.database = 'ok';
  } catch {
    checks.database = 'error';
    checks.status = 'degraded';
  }

  const isHealthy = checks.database === 'ok';
  res.status(isHealthy ? 200 : 503).json(checks);
});

app.get('/api/users', async (req, res) => {
  console.time('fetch-users');
  const users = await mongoose.model('User').find().lean();
  console.timeEnd('fetch-users');
  res.json({ success: true, data: users });
});

mongoose.connect(config.mongoUri).then(() => {
  app.listen(config.port, () => {
    console.log(`Server running on port ${config.port} [${config.env}]`);
  });
});

process.on('SIGTERM', () => {
  console.log('SIGTERM received, shutting down gracefully...');
  mongoose.connection.close();
  process.exit(0);
});

process.on('SIGINT', () => {
  console.log('SIGINT received, shutting down gracefully...');
  mongoose.connection.close();
  process.exit(0);
});
逻辑代码 55 行(超过 40 行限制,仅展示)

❓ 常见问题

Q Docker 和 PM2 可以同时用吗?
A 可以但通常选其一。Docker 容器内用 node 直接运行,由 K8s/Docker Compose 管理重启;PM2 适合非容器化部署。
Q 如何选择云服务平台?
A 小项目用 Vercel/Railway(零配置),中型用 AWS/GCP(灵活但复杂),国内用阿里云/腾讯云。根据预算和团队经验选择。
Q CI/CD 有哪些常用工具?
A GitHub Actions(与 GitHub 集成好)、GitLab CI(自托管方便)、Jenkins(企业级,插件多)、CircleCI(云原生)。
Q 如何监控生产环境 Node.js 应用?
A 使用 PM2 monit、New Relic、Datadog 或自建 Prometheus + Grafana 监控 CPU、内存、响应时间和错误率。
Q 部署后如何做健康检查?
A 添加 /health 端点返回 { status: 'ok' },Docker HEALTHCHECK 或 K8s livenessProbe 定期访问该端点。

Q:PM2 和 Docker 用哪个? A:两者不冲突。Docker 解决环境一致性问题,PM2 解决进程管理和集群问题。生产推荐 Docker + PM2 组合使用,Docker 容器内运行 PM2 管理多进程。

Q:如何实现零停机部署? A:使用 pm2 reload 代替 pm2 restart,PM2 会逐个重启 Worker,始终保留部分实例处理请求。Docker 环境可用蓝绿部署或滚动更新策略。

Q:cluster 模式有什么限制? A:集群模式下进程间不共享内存,Session 需用 Redis 等外部存储共享;WebSocket 需搭配 sticky session 或 Pub/Sub 消息同步;文件系统缓存每进程独立。

Q:如何监控生产环境? A:PM2 提供 pm2 monit 实时监控和 pm2 plus 在线仪表盘;商业方案有 New Relic、Datadog、Prometheus + Grafana;关键指标包括 CPU、内存、事件循环延迟和请求响应时间。

Q:Docker 镜像怎么减小体积? A:使用 node:alpine 基础镜像、多阶段构建分离编译依赖、.dockerignore 排除非必要文件、npm ci --only=production 只安装生产依赖、清理 npm 缓存 npm cache clean --force

Q:Nginx 反向代理有什么优势? A:SSL 终止减轻 Node.js 加密开销、静态资源直接由 Nginx 处理、负载均衡分发请求、限流防止 DDoS、gzip 压缩减少传输体积、缓存加速响应。


📖 小节

(1) 本课要点

(2) 作业

  1. 用 PM2 启动你的 Express 项目,配置 ecosystem.config.js,启用集群模式并用 pm2 monit 观察运行状态
  2. 为项目编写 Dockerfile,使用多阶段构建优化镜像大小,并用 docker-compose 同时启动 Node.js 和 MongoDB
  3. 实现 /health/healthz/readyz 三个健康检查端点,并在 Docker 中配置 HEALTHCHECK
  4. 使用 autocannon 对你的 API 进行负载测试,分别测试单进程和集群模式的吞吐量差异
  5. 编写 Nginx 反向代理配置,实现 SSL 终止和负载均衡,用 abcurl 验证代理转发正常

📝 作业

  1. 完成本课所有代码示例,确保每个示例都能正确运行
  2. 修改综合示例,添加自己的扩展功能
  3. 查阅官方文档,找出本课未涉及的1-2个API并编写测试代码
  4. 思考:在实际项目中,你会如何应用本课学到的知识?
  5. 尝试将本课知识与前面课程的内容结合,构建一个小项目
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏