Node.js: 部署与优化
最后更新:2026-08-26
Charlie 用 node app.js 把 API 部署到生产服务器后,经常因为未捕获异常导致进程崩溃,而无人值守时服务长时间中断。他引入 PM2 实现自动重启与集群模式,搭配 Docker 容器化和 Nginx 反向代理,服务可用性从 95% 提升到 99.9%,终于睡上了安稳觉。
你将学到:
- PM2 进程管理(start / restart / logs / monit)
- Docker 基础(Dockerfile / docker-compose)
- 环境变量管理(.env / 生产配置)
- 集群模式(cluster module)
- 性能分析(console.time / performance hooks / clinic.js)
- 健康检查端点设计
- Nginx 反向代理配置
1. PM2 进程管理
(1) 为什么需要进程管理器
生产环境中 Node.js 进程可能因未捕获异常、内存泄漏或系统资源不足而崩溃。直接用 node app.js 启动,进程崩溃后无人重启,导致服务中断。PM2 是 Node.js 最流行的进程管理器,提供自动重启、日志管理、负载均衡和监控功能。
▶ 示例:(2) 安装与基本命令
npm install -g pm2
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 启动与监控
pm2 start app.js --name "charlie-api" -i max
pm2 monit
pm2 save
pm2 startup
┌─────┬──────────────┬─────────────┬─────────┬─────────┬──────────┐
│ 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 编写
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 多服务编排
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 镜像
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 文件
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) 生产环境配置策略
.env # 默认配置(不提交 Git)
.env.development # 开发环境
.env.test # 测试环境
.env.production # 生产环境(通过 CI/CD 注入,不存代码仓库)
(4) 环境变量管理方式对比
| 方式 | 安全性 | 灵活性 | 团队协作 | 适用场景 |
|---|---|---|---|---|
.env 文件 |
中 | 高 | 好 | 通用项目 |
| 系统环境变量 | 高 | 低 | 差 | 简单部署 |
| Docker secrets | 高 | 中 | 好 | Docker 环境 |
| K8s Secrets | 高 | 高 | 优 | Kubernetes |
| 云平台配置中心 | 高 | 高 | 优 | 云原生项目 |
▶ 示例:多环境配置加载
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) 手动实现集群
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 内置集群支持,无需修改代码即可启用:
pm2 start app.js -i max
pm2 start app.js -i 4
▶ 示例:集群模式性能对比
node single.js &
ab -n 10000 -c 100 http://localhost:3000/
Single process:
Requests per second: 3254.21 [#/sec]
Cluster (4 workers):
Requests per second: 11280.67 [#/sec]
5. 性能分析
(1) console.time 基础计时
最简单的性能测量方式,适合快速定位慢操作:
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 模块提供高精度计时:
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 官方推荐的性能诊断工具套件:
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 负载测试
npm install -g autocannon
autocannon -c 100 -d 10 http://localhost:3000/api/health
┌─────────┬──────┬──────┬───────┬──────┬───────┬───────┬───────┐
│ 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) 基础健康检查
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
▶ 示例:(3) 深度健康检查
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(是否准备好接收流量):
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 健康检查脚本
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 生产部署架构
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 配置
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 反向代理配置
# 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 把所有部署配置整合到一起,实现了从代码到生产的全流程自动化。
▶ 示例:项目目录结构
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
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': '',
},
},
};
▶ 示例: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
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
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 中的健康检查端点
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);
});
❓ 常见问题
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) 本课要点
- PM2 实现进程自动重启、集群模式和日志管理
- Docker 容器化保证环境一致性和部署标准化
- 环境变量分离配置,支持多环境切换
- 集群模式充分利用多核 CPU 提升吞吐量
- Performance Hooks 和 Clinic.js 精准定位性能瓶颈
- 健康检查端点是容器编排和负载均衡的基础
- Nginx 反向代理处理 SSL、负载均衡和静态资源
(2) 作业
- 用 PM2 启动你的 Express 项目,配置
ecosystem.config.js,启用集群模式并用pm2 monit观察运行状态 - 为项目编写 Dockerfile,使用多阶段构建优化镜像大小,并用
docker-compose同时启动 Node.js 和 MongoDB - 实现
/health、/healthz、/readyz三个健康检查端点,并在 Docker 中配置HEALTHCHECK - 使用
autocannon对你的 API 进行负载测试,分别测试单进程和集群模式的吞吐量差异 - 编写 Nginx 反向代理配置,实现 SSL 终止和负载均衡,用
ab或curl验证代理转发正常
📝 作业
- 完成本课所有代码示例,确保每个示例都能正确运行
- 修改综合示例,添加自己的扩展功能
- 查阅官方文档,找出本课未涉及的1-2个API并编写测试代码
- 思考:在实际项目中,你会如何应用本课学到的知识?
- 尝试将本课知识与前面课程的内容结合,构建一个小项目