Node.js 生产环境实践与部署
将 Node.js 应用部署到生产环境需要考虑多个方面,包括性能优化、安全性、监控、日志记录、错误处理等。本文将深入探讨 Node.js 在生产环境中的最佳实践。
一、环境配置与管理
1. 环境变量管理
javascript
// 环境变量配置管理
// config/index.js
const dotenv = require('dotenv');
// 根据环境加载不同的 .env 文件
const env = process.env.NODE_ENV || 'development';
dotenv.config({ path: `.env.${env}` });
// 默认配置
const defaultConfig = {
port: process.env.PORT || 3000,
database: {
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
name: process.env.DB_NAME || 'myapp',
user: process.env.DB_USER || 'user',
password: process.env.DB_PASSWORD || 'password'
},
redis: {
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379
},
jwt: {
secret: process.env.JWT_SECRET || 'your-secret-key',
expiresIn: process.env.JWT_EXPIRES_IN || '24h'
},
logging: {
level: process.env.LOG_LEVEL || 'info',
file: process.env.LOG_FILE || 'app.log'
}
};
// 验证必要配置
const requiredEnvVars = ['JWT_SECRET'];
const missingEnvVars = requiredEnvVars.filter(envVar => !process.env[envVar]);
if (missingEnvVars.length > 0) {
console.error('缺少必要的环境变量:', missingEnvVars.join(', '));
process.exit(1);
}
module.exports = defaultConfig;2. 配置文件组织
javascript
// config/development.js
module.exports = {
database: {
host: 'localhost',
port: 5432,
name: 'myapp_dev',
debug: true
},
logging: {
level: 'debug',
prettyPrint: true
}
};
// config/production.js
module.exports = {
database: {
host: process.env.DB_HOST,
port: process.env.DB_PORT,
name: process.env.DB_NAME,
ssl: true,
debug: false
},
logging: {
level: 'info',
prettyPrint: false,
destinations: [
'/var/log/myapp/app.log',
// 可以添加多个日志目的地
]
}
};
// config/index.js
const env = process.env.NODE_ENV || 'development';
const baseConfig = require('./base');
const envConfig = require(`./${env}`);
module.exports = {
...baseConfig,
...envConfig,
// 合并环境特定配置
database: {
...baseConfig.database,
...envConfig.database
},
logging: {
...baseConfig.logging,
...envConfig.logging
}
};二、进程管理与守护
1. 使用 PM2 进程管理器
javascript
// ecosystem.config.js
module.exports = {
apps: [
{
name: 'myapp-api',
script: './src/index.js',
instances: 'max', // 根据 CPU 核心数启动实例
exec_mode: 'cluster',
watch: false, // 生产环境关闭监听
max_memory_restart: '1G', // 内存超过 1G 时重启
env: {
NODE_ENV: 'production',
PORT: 3000
},
env_development: {
NODE_ENV: 'development',
PORT: 3001,
watch: true
},
error_file: './logs/err.log',
out_file: './logs/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss',
combine_logs: true,
merge_logs: true,
// 自动重启配置
min_uptime: '60s',
max_restarts: 10,
restart_delay: 5000,
// 负载均衡
listen_timeout: 5000,
kill_timeout: 3000
}
],
deploy: {
production: {
user: 'node',
host: ['server1.example.com', 'server2.example.com'],
ref: 'origin/master',
repo: 'git@github.com:username/myapp.git',
path: '/var/www/production',
'post-deploy': 'npm install && pm2 reload ecosystem.config.js --env production'
}
}
};
// PM2 常用命令
// pm2 start ecosystem.config.js
// pm2 start ecosystem.config.js --env development
// pm2 list
// pm2 monit
// pm2 logs
// pm2 reload all
// pm2 stop all
// pm2 delete all2. 优雅关闭处理
javascript
// graceful-shutdown.js
class GracefulShutdown {
constructor(server, options = {}) {
this.server = server;
this.connections = new Set();
this.timeout = options.timeout || 30000; // 30秒超时
this.signals = ['SIGTERM', 'SIGINT', 'SIGUSR2'];
this.init();
}
init() {
// 跟踪活跃连接
this.server.on('connection', (socket) => {
this.connections.add(socket);
socket.on('close', () => {
this.connections.delete(socket);
});
});
// 监听关闭信号
this.signals.forEach(signal => {
process.on(signal, () => {
console.log(`接收到 ${signal} 信号,开始优雅关闭...`);
this.shutdown();
});
});
// 处理未捕获异常
process.on('uncaughtException', (err) => {
console.error('未捕获异常:', err);
this.shutdown(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的 Promise 拒绝:', reason);
this.shutdown(1);
});
}
async shutdown(code = 0) {
console.log('开始关闭服务器...');
try {
// 停止接收新连接
this.server.close(() => {
console.log('服务器已关闭');
});
// 关闭所有活跃连接
this.connections.forEach(socket => {
socket.destroy();
});
// 执行清理任务
await this.cleanup();
// 设置超时强制退出
setTimeout(() => {
console.error('关闭超时,强制退出');
process.exit(code);
}, this.timeout);
} catch (err) {
console.error('关闭过程中出错:', err);
process.exit(1);
}
}
async cleanup() {
// 关闭数据库连接
if (global.db) {
await global.db.close();
}
// 关闭 Redis 连接
if (global.redis) {
await global.redis.quit();
}
// 关闭其他资源
console.log('清理完成');
}
}
module.exports = GracefulShutdown;
// 在应用中使用
// src/index.js
const http = require('http');
const GracefulShutdown = require('./graceful-shutdown');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello World\n');
});
server.listen(3000, () => {
console.log('服务器运行在端口 3000');
});
// 启用优雅关闭
new GracefulShutdown(server);三、安全加固
1. HTTP 安全头设置
javascript
// security-headers.js
const helmet = require('helmet');
function securityHeaders(app) {
// 使用 helmet 设置安全头
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"]
}
},
// 隐藏 X-Powered-By 头
hidePoweredBy: true,
// HTTP 严格传输安全
hsts: {
maxAge: 31536000, // 1年
includeSubDomains: true,
preload: true
},
// 防止跨站请求伪造
referrerPolicy: { policy: 'no-referrer' },
// 防止 MIME 类型嗅探
noSniff: true,
// 防止 XSS 攻击
xssFilter: true,
// 防止点击劫持
frameguard: { action: 'deny' }
}));
// 自定义安全头
app.use((req, res, next) => {
// 防止 MIME 类型嗅探
res.setHeader('X-Content-Type-Options', 'nosniff');
// 防止 XSS 攻击
res.setHeader('X-XSS-Protection', '1; mode=block');
// 防止点击劫持
res.setHeader('X-Frame-Options', 'DENY');
// 强制 HTTPS
if (process.env.NODE_ENV === 'production') {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
}
next();
});
}
module.exports = securityHeaders;2. 输入验证与清理
javascript
// validation.js
const { body, validationResult } = require('express-validator');
// 用户注册验证规则
const userRegistrationValidation = [
body('username')
.isLength({ min: 3, max: 20 })
.withMessage('用户名长度必须在3-20个字符之间')
.matches(/^[a-zA-Z0-9_]+$/)
.withMessage('用户名只能包含字母、数字和下划线'),
body('email')
.isEmail()
.withMessage('请输入有效的邮箱地址')
.normalizeEmail(),
body('password')
.isLength({ min: 8 })
.withMessage('密码长度至少8个字符')
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
.withMessage('密码必须包含大小写字母和数字'),
body('age')
.optional()
.isInt({ min: 18, max: 120 })
.withMessage('年龄必须在18-120之间')
];
// 验证中间件
function validate(req, res, next) {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: '验证失败',
errors: errors.array()
});
}
next();
}
// 数据清理中间件
function sanitizeInput(req, res, next) {
// 清理字符串输入
Object.keys(req.body).forEach(key => {
if (typeof req.body[key] === 'string') {
req.body[key] = req.body[key]
.trim() // 去除首尾空格
.replace(/<[^>]*>/g, '') // 移除 HTML 标签
.substring(0, 1000); // 限制长度
}
});
next();
}
module.exports = {
userRegistrationValidation,
validate,
sanitizeInput
};3. 认证与授权
javascript
// auth.js
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const rateLimit = require('express-rate-limit');
// JWT 配置
const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '24h';
// 登录限流
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 5, // 最多5次尝试
message: {
success: false,
message: '登录尝试次数过多,请15分钟后再试'
},
standardHeaders: true,
legacyHeaders: false
});
// 生成 JWT Token
function generateToken(user) {
const payload = {
id: user.id,
username: user.username,
email: user.email,
role: user.role
};
return jwt.sign(payload, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN,
issuer: 'myapp',
audience: 'myapp-users'
});
}
// 验证 JWT Token
function verifyToken(req, res, next) {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({
success: false,
message: '访问被拒绝,未提供 token'
});
}
try {
const decoded = jwt.verify(token, JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
return res.status(401).json({
success: false,
message: '无效的 token'
});
}
}
// 角色授权检查
function requireRole(role) {
return (req, res, next) => {
if (req.user.role !== role) {
return res.status(403).json({
success: false,
message: '权限不足'
});
}
next();
};
}
module.exports = {
loginLimiter,
generateToken,
verifyToken,
requireRole
};四、监控与日志
1. 应用监控
javascript
// monitoring.js
const prometheus = require('prom-client');
// 创建指标收集器
const collectDefaultMetrics = prometheus.collectDefaultMetrics;
collectDefaultMetrics({ timeout: 5000 });
// 自定义指标
const httpRequestDuration = new prometheus.Histogram({
name: 'http_request_duration_seconds',
help: 'HTTP 请求持续时间',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.1, 0.5, 1, 2, 5, 10]
});
const httpRequestTotal = new prometheus.Counter({
name: 'http_requests_total',
help: 'HTTP 请求总数',
labelNames: ['method', 'route', 'status_code']
});
const activeConnections = new prometheus.Gauge({
name: 'active_connections',
help: '活跃连接数'
});
// 监控中间件
function monitoringMiddleware(req, res, next) {
const startTime = Date.now();
const originalSend = res.send;
// 增加活跃连接数
activeConnections.inc();
// 重写 send 方法以捕获响应
res.send = function (body) {
res.send = originalSend;
const duration = (Date.now() - startTime) / 1000;
const statusCode = res.statusCode;
// 记录指标
httpRequestDuration
.labels(req.method, req.path, statusCode.toString())
.observe(duration);
httpRequestTotal
.labels(req.method, req.path, statusCode.toString())
.inc();
// 减少活跃连接数
activeConnections.dec();
return res.send(body);
};
next();
}
// 暴露指标端点
function metricsEndpoint(req, res) {
res.set('Content-Type', prometheus.register.contentType);
res.end(prometheus.register.metrics());
}
module.exports = {
monitoringMiddleware,
metricsEndpoint
};2. 日志记录
javascript
// logger.js
const winston = require('winston');
const path = require('path');
// 创建日志目录
const logDir = path.join(__dirname, '../logs');
require('fs').mkdirSync(logDir, { recursive: true });
// 自定义日志格式
const logFormat = winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss'
}),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json()
);
// 开发环境格式
const devFormat = winston.format.combine(
winston.format.colorize(),
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss'
}),
winston.format.printf(info => {
const { timestamp, level, message, ...meta } = info;
return `${timestamp} ${level}: ${message} ${Object.keys(meta).length ? JSON.stringify(meta) : ''}`;
})
);
// 创建日志记录器
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: process.env.NODE_ENV === 'production' ? logFormat : devFormat,
defaultMeta: { service: 'myapp' },
transports: [
// 错误日志文件
new winston.transports.File({
filename: path.join(logDir, 'error.log'),
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5
}),
// 综合日志文件
new winston.transports.File({
filename: path.join(logDir, 'combined.log'),
maxsize: 5242880, // 5MB
maxFiles: 5
})
]
});
// 在非生产环境中输出到控制台
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: devFormat
}));
}
// 请求日志中间件
function requestLogger(req, res, next) {
const startTime = Date.now();
res.on('finish', () => {
const duration = Date.now() - startTime;
const statusCode = res.statusCode;
logger.info('HTTP Request', {
method: req.method,
url: req.url,
statusCode,
duration: `${duration}ms`,
userAgent: req.get('User-Agent'),
ip: req.ip || req.connection.remoteAddress
});
});
next();
}
module.exports = {
logger,
requestLogger
};五、性能优化
1. 缓存策略
javascript
// cache.js
const redis = require('redis');
const { promisify } = require('util');
class CacheManager {
constructor(options = {}) {
this.client = redis.createClient({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
retry_strategy: (options) => {
if (options.error && options.error.code === 'ECONNREFUSED') {
return new Error('Redis 服务器拒绝连接');
}
if (options.total_retry_time > 1000 * 60 * 60) {
return new Error('重试时间已用完');
}
if (options.attempt > 10) {
return undefined;
}
return Math.min(options.attempt * 100, 3000);
}
});
this.getAsync = promisify(this.client.get).bind(this.client);
this.setAsync = promisify(this.client.set).bind(this.client);
this.delAsync = promisify(this.client.del).bind(this.client);
this.existsAsync = promisify(this.client.exists).bind(this.client);
this.client.on('error', (err) => {
console.error('Redis 错误:', err);
});
}
async get(key) {
try {
const value = await this.getAsync(key);
return value ? JSON.parse(value) : null;
} catch (err) {
console.error('缓存获取失败:', err);
return null;
}
}
async set(key, value, ttl = 3600) {
try {
const stringValue = JSON.stringify(value);
if (ttl) {
await this.setAsync(key, stringValue, 'EX', ttl);
} else {
await this.setAsync(key, stringValue);
}
} catch (err) {
console.error('缓存设置失败:', err);
}
}
async delete(key) {
try {
await this.delAsync(key);
} catch (err) {
console.error('缓存删除失败:', err);
}
}
async exists(key) {
try {
return await this.existsAsync(key);
} catch (err) {
console.error('缓存检查失败:', err);
return false;
}
}
async close() {
await this.client.quit();
}
}
// 缓存中间件
function cacheMiddleware(ttl = 3600) {
return async (req, res, next) => {
const cacheKey = `cache:${req.originalUrl}`;
const cachedResponse = await cacheManager.get(cacheKey);
if (cachedResponse) {
return res.json(cachedResponse);
}
const originalSend = res.send;
res.send = function (body) {
res.send = originalSend;
// 缓存响应
try {
const responseBody = JSON.parse(body);
cacheManager.set(cacheKey, responseBody, ttl);
} catch (err) {
// 如果不是 JSON,不缓存
}
return res.send(body);
};
next();
};
}
const cacheManager = new CacheManager();
module.exports = {
cacheManager,
cacheMiddleware
};2. 负载均衡与集群
javascript
// cluster.js
const cluster = require('cluster');
const numCPUs = require('os').cpus().length;
const { logger } = require('./logger');
if (cluster.isMaster) {
logger.info(`主进程 ${process.pid} 正在运行`);
// 根据 CPU 核心数创建工作进程
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
// 工作进程退出时重启
cluster.on('exit', (worker, code, signal) => {
logger.warn(`工作进程 ${worker.process.pid} 已退出 (code: ${code}, signal: ${signal})`);
logger.info('正在重启工作进程...');
cluster.fork();
});
// 监听消息
cluster.on('message', (worker, message) => {
logger.info(`来自主进程 ${worker.id} 的消息:`, message);
});
} else {
// 工作进程代码
logger.info(`工作进程 ${process.pid} 已启动`);
// 启动应用
require('./app');
}
// 负载均衡配置示例 (Nginx)
/*
upstream myapp {
ip_hash;
server 127.0.0.1:3000 weight=3;
server 127.0.0.1:3001 weight=2;
server 127.0.0.1:3002 weight=1;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://myapp;
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;
}
}
*/六、总结
Node.js 生产环境部署需要综合考虑多个方面:
- 环境配置:合理的环境变量管理和配置文件组织
- 进程管理:使用 PM2 等工具进行进程管理和优雅关闭
- 安全加固:设置安全头、输入验证、认证授权等安全措施
- 监控日志:建立完善的监控体系和日志记录机制
- 性能优化:实施缓存策略、负载均衡和集群部署
通过实施这些最佳实践,可以确保 Node.js 应用在生产环境中稳定、安全、高效地运行。同时,持续监控和优化是保证应用长期稳定运行的关键。