Skip to content

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 all

2. 优雅关闭处理

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 生产环境部署需要综合考虑多个方面:

  1. 环境配置:合理的环境变量管理和配置文件组织
  2. 进程管理:使用 PM2 等工具进行进程管理和优雅关闭
  3. 安全加固:设置安全头、输入验证、认证授权等安全措施
  4. 监控日志:建立完善的监控体系和日志记录机制
  5. 性能优化:实施缓存策略、负载均衡和集群部署

通过实施这些最佳实践,可以确保 Node.js 应用在生产环境中稳定、安全、高效地运行。同时,持续监控和优化是保证应用长期稳定运行的关键。