Skip to content
On this page

Node.js 部署与运维

Node.js应用的部署和运维是确保应用稳定运行的关键环节。本章详细介绍Node.js应用的部署策略、运维实践和监控方法。

部署策略

环境配置

javascript
// config.js - 环境配置管理
const path = require('path');

class Config {
  constructor() {
    this.env = process.env.NODE_ENV || 'development';
    this.isProduction = this.env === 'production';
    this.isDevelopment = this.env === 'development';
    this.isTest = this.env === 'test';
    
    this.port = process.env.PORT || 3000;
    this.host = process.env.HOST || '0.0.0.0';
    
    this.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',
      ssl: this.isProduction
    };
    
    this.jwt = {
      secret: process.env.JWT_SECRET || 'dev-secret',
      expiresIn: process.env.JWT_EXPIRES_IN || '24h'
    };
    
    this.redis = {
      host: process.env.REDIS_HOST || 'localhost',
      port: process.env.REDIS_PORT || 6379,
      password: process.env.REDIS_PASSWORD
    };
    
    this.logLevel = process.env.LOG_LEVEL || 'info';
  }
  
  validate() {
    const required = this.isProduction 
      ? ['DB_PASSWORD', 'JWT_SECRET', 'SESSION_SECRET'] 
      : [];
    
    const missing = required.filter(key => !process.env[key]);
    
    if (missing.length > 0) {
      throw new Error(`生产环境缺少必需的环境变量: ${missing.join(', ')}`);
    }
  }
}

const config = new Config();
config.validate();

module.exports = config;

Docker部署

text
# Dockerfile
FROM node:18-alpine

# 设置工作目录
WORKDIR /app

# 复制package文件
COPY package*.json ./

# 安装依赖
RUN npm ci --only=production

# 复制应用代码
COPY . .

# 创建非root用户
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

# 更改文件所有权
RUN chown -R nextjs:nodejs /app
USER nextjs

# 暴露端口
EXPOSE 3000

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

# 启动命令
CMD ["npm", "start"]
yaml
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - PORT=3000
      - DB_HOST=database
      - DB_USER=postgres
      - DB_PASSWORD=secretpassword
      - REDIS_HOST=redis
    depends_on:
      - database
      - redis
    restart: unless-stopped
    networks:
      - app-network

  database:
    image: postgres:14
    environment:
      - POSTGRES_DB=myapp
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=secretpassword
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    restart: unless-stopped
    networks:
      - app-network

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    restart: unless-stopped
    networks:
      - app-network

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./ssl:/etc/nginx/ssl
    depends_on:
      - app
    restart: unless-stopped
    networks:
      - app-network

volumes:
  postgres_data:
  redis_data:

networks:
  app-network:
    driver: bridge

PM2进程管理

javascript
// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'my-node-app',
    script: './app.js',
    
    // 环境变量
    env: {
      NODE_ENV: 'development',
      PORT: 3000
    },
    
    env_production: {
      NODE_ENV: 'production',
      PORT: 3000
    },
    
    // 进程管理
    instances: 'max', // 使用所有CPU核心
    exec_mode: 'cluster', // 集群模式
    max_memory_restart: '1G', // 内存超限自动重启
    
    // 监控
    watch: false, // 生产环境关闭文件监听
    ignore_watch: ['node_modules', 'logs'],
    
    // 日志
    error_file: './logs/err.log',
    out_file: './logs/out.log',
    log_file: './logs/combined.log',
    time: true,
    
    // 性能
    max_restarts: 10,
    min_uptime: '10s',
    
    // 安全
    kill_timeout: 5000,
    wait_ready: true
  }]
};

CI/CD流水线

GitHub Actions部署

yaml
# .github/workflows/deploy.yml
name: Deploy Node.js App

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        node-version: [16.x, 18.x]
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Run tests
      run: npm test
    
    - name: Run linting
      run: npm run lint
    
    - name: Run security audit
      run: npm audit --audit-level moderate

  build-and-deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18.x'
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci --only=production
    
    - name: Build
      run: npm run build
      env:
        NODE_ENV: production
    
    - name: Deploy to server
      uses: appleboy/ssh-action@v0.1.5
      with:
        host: ${{ secrets.HOST }}
        username: ${{ secrets.USERNAME }}
        key: ${{ secrets.KEY }}
        script: |
          cd /var/www/myapp
          git pull origin main
          npm install --production
          pm2 reload my-node-app
          pm2 save

部署脚本

bash
#!/bin/bash
# deploy.sh - 部署脚本

set -e  # 遇到错误立即退出

echo "开始部署..."

# 检查Node.js版本
NODE_VERSION=$(node -v)
echo "Node.js 版本: $NODE_VERSION"

# 检查npm版本
NPM_VERSION=$(npm -v)
echo "NPM 版本: $NPM_VERSION"

# 检查环境变量
if [ -z "$NODE_ENV" ]; then
    echo "错误: NODE_ENV 未设置"
    exit 1
fi

if [ "$NODE_ENV" = "production" ]; then
    # 生产环境检查
    if [ -z "$DB_PASSWORD" ] || [ -z "$JWT_SECRET" ]; then
        echo "错误: 生产环境必需的环境变量未设置"
        exit 1
    fi
fi

# 拉取最新代码
echo "拉取最新代码..."
git pull origin main

# 安装依赖
echo "安装依赖..."
npm ci --only=production

# 运行数据库迁移
echo "运行数据库迁移..."
npm run migrate

# 构建应用
if [ "$NODE_ENV" = "production" ]; then
    echo "构建生产环境应用..."
    npm run build
fi

# 重启应用
echo "重启应用..."
pm2 reload my-node-app

# 等待应用启动
sleep 5

# 运行健康检查
HEALTH_CHECK=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/health)
if [ $HEALTH_CHECK -eq 200 ]; then
    echo "部署成功!"
    pm2 save
else
    echo "健康检查失败,回滚..."
    pm2 reload my-node-app --reverse
    exit 1
fi

echo "部署完成!"

监控和日志

应用监控

javascript
// monitoring.js - 应用监控
const os = require('os');
const cluster = require('cluster');

class ApplicationMonitor {
  constructor() {
    this.metrics = {
      cpu: [],
      memory: [],
      requests: [],
      errors: []
    };
    
    this.startMonitoring();
  }
  
  startMonitoring() {
    // CPU使用率监控
    setInterval(() => {
      const cpuUsage = process.cpuUsage();
      const loadAvg = os.loadavg();
      
      this.metrics.cpu.push({
        timestamp: Date.now(),
        usage: cpuUsage,
        loadAvg: loadAvg,
        processCpu: process.cpuUsage()
      });
      
      // 限制历史数据大小
      if (this.metrics.cpu.length > 1000) {
        this.metrics.cpu.shift();
      }
    }, 5000);
    
    // 内存使用监控
    setInterval(() => {
      const memoryUsage = process.memoryUsage();
      this.metrics.memory.push({
        timestamp: Date.now(),
        usage: memoryUsage,
        heapUsedPercent: (memoryUsage.heapUsed / memoryUsage.heapTotal) * 100
      });
      
      if (this.metrics.memory.length > 1000) {
        this.metrics.memory.shift();
      }
    }, 5000);
  }
  
  getMetrics() {
    return {
      cpu: this.metrics.cpu[this.metrics.cpu.length - 1],
      memory: this.metrics.memory[this.metrics.memory.length - 1],
      uptime: process.uptime(),
      pid: process.pid,
      ppid: process.ppid,
      title: process.title,
      version: process.version,
      platform: process.platform,
      arch: process.arch
    };
  }
  
  logRequest(req, res, startTime) {
    const duration = Date.now() - startTime;
    
    this.metrics.requests.push({
      timestamp: Date.now(),
      method: req.method,
      url: req.url,
      statusCode: res.statusCode,
      duration: duration,
      ip: req.ip,
      userAgent: req.get('User-Agent')
    });
    
    if (this.metrics.requests.length > 10000) {
      this.metrics.requests.shift();
    }
  }
  
  logError(error, context = {}) {
    this.metrics.errors.push({
      timestamp: Date.now(),
      message: error.message,
      stack: error.stack,
      context: context
    });
    
    if (this.metrics.errors.length > 1000) {
      this.metrics.errors.shift();
    }
  }
}

// Express中间件集成
function monitoringMiddleware(monitor) {
  return (req, res, next) => {
    const startTime = Date.now();
    
    // 记录响应结束事件
    res.on('finish', () => {
      monitor.logRequest(req, res, startTime);
    });
    
    next();
  };
}

// 错误处理中间件
function errorLoggingMiddleware(monitor) {
  return (err, req, res, next) => {
    monitor.logError(err, {
      url: req.url,
      method: req.method,
      ip: req.ip,
      userAgent: req.get('User-Agent')
    });
    
    next(err);
  };
}

const monitor = new ApplicationMonitor();
module.exports = { monitor, monitoringMiddleware, errorLoggingMiddleware };

日志管理

javascript
// logger.js - 高级日志管理
const winston = require('winston'); // npm install winston
const LokiTransport = require('winston-loki'); // npm install winston-loki

// 创建logger实例
const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.splat(),
    winston.format.json()
  ),
  defaultMeta: { service: 'my-node-app' },
  transports: [
    // 控制台输出
    new winston.transports.Console({
      format: winston.format.combine(
        winston.format.colorize(),
        winston.format.simple()
      )
    }),
    
    // 文件输出
    new winston.transports.File({
      filename: 'logs/error.log',
      level: 'error',
      maxsize: 5242880, // 5MB
      maxFiles: 5
    }),
    
    new winston.transports.File({
      filename: 'logs/combined.log',
      maxsize: 5242880, // 5MB
      maxFiles: 5
    })
  ]
});

// 在生产环境添加远程日志
if (process.env.NODE_ENV === 'production') {
  logger.add(new LokiTransport({
    host: process.env.LOKI_HOST || 'http://localhost:3100',
    json: true,
    labels: {
      job: 'nodejs-app',
      instance: os.hostname()
    },
    level: 'info'
  }));
}

// 日志中间件
function requestLogger(req, res, next) {
  const start = Date.now();
  
  res.on('finish', () => {
    const duration = Date.now() - start;
    logger.info('HTTP Request', {
      method: req.method,
      url: req.url,
      statusCode: res.statusCode,
      duration: `${duration}ms`,
      ip: req.ip,
      userAgent: req.get('User-Agent')
    });
  });
  
  next();
}

module.exports = { logger, requestLogger };

性能优化

缓存策略

javascript
// cache.js - 多层缓存实现
const NodeCache = require('node-cache'); // npm install node-cache
const redis = require('redis');

class MultiLevelCache {
  constructor() {
    // 内存缓存(快速访问)
    this.memoryCache = new NodeCache({ stdTTL: 600 }); // 10分钟
    
    // Redis缓存(分布式缓存)
    this.redisClient = redis.createClient({
      host: process.env.REDIS_HOST || 'localhost',
      port: process.env.REDIS_PORT || 6379
    });
    
    this.redisClient.on('error', (err) => {
      console.error('Redis错误:', err);
    });
  }
  
  async get(key) {
    // 首先检查内存缓存
    let value = this.memoryCache.get(key);
    if (value !== undefined) {
      return value;
    }
    
    // 然后检查Redis缓存
    try {
      const redisValue = await this.redisClient.get(key);
      if (redisValue) {
        // 将值存入内存缓存以加速后续访问
        this.memoryCache.set(key, JSON.parse(redisValue));
        return JSON.parse(redisValue);
      }
    } catch (err) {
      console.error('Redis获取失败:', err);
    }
    
    return null;
  }
  
  async set(key, value, ttl = 600) {
    // 同时设置内存和Redis缓存
    this.memoryCache.set(key, value, ttl);
    
    try {
      await this.redisClient.setEx(key, ttl, JSON.stringify(value));
    } catch (err) {
      console.error('Redis设置失败:', err);
    }
  }
  
  async del(key) {
    // 同时删除内存和Redis缓存
    this.memoryCache.del(key);
    
    try {
      await this.redisClient.del(key);
    } catch (err) {
      console.error('Redis删除失败:', err);
    }
  }
  
  async clear() {
    this.memoryCache.flushAll();
    
    try {
      await this.redisClient.flushdb();
    } catch (err) {
      console.error('Redis清空失败:', err);
    }
  }
}

// 缓存中间件
function cacheMiddleware(cache, ttl = 300) {
  return async (req, res, next) => {
    const cacheKey = `response:${req.originalUrl}`;
    const cachedResponse = await cache.get(cacheKey);
    
    if (cachedResponse) {
      res.set('X-Cache', 'HIT');
      res.status(cachedResponse.statusCode).json(cachedResponse.data);
      return;
    }
    
    // 包装res.json方法以捕获响应
    const originalJson = res.json;
    res.json = function(data) {
      // 存储响应到缓存
      cache.set(cacheKey, {
        statusCode: res.statusCode,
        data: data
      }, ttl);
      
      res.set('X-Cache', 'MISS');
      originalJson.call(this, data);
    };
    
    next();
  };
}

const cache = new MultiLevelCache();
module.exports = { cache, cacheMiddleware };

数据库连接池优化

javascript
// database.js - 优化的数据库连接池
const mysql = require('mysql2/promise');

class DatabasePool {
  constructor() {
    this.pool = mysql.createPool({
      host: process.env.DB_HOST || 'localhost',
      user: process.env.DB_USER || 'root',
      password: process.env.DB_PASSWORD || '',
      database: process.env.DB_NAME || 'test',
      
      // 连接池配置
      connectionLimit: process.env.DB_CONNECTION_LIMIT || 10,
      queueLimit: 0,
      acquireTimeout: 60000,
      timeout: 60000,
      
      // 连接保活
      enableKeepAlive: true,
      keepAliveInitialDelay: 0,
      
      // SSL配置(生产环境)
      ssl: process.env.NODE_ENV === 'production' ? {
        rejectUnauthorized: false
      } : false,
      
      // 字符集
      charset: 'utf8mb4'
    });
  }
  
  async query(sql, params) {
    let connection;
    try {
      connection = await this.pool.getConnection();
      const [results] = await connection.execute(sql, params);
      return results;
    } catch (err) {
      console.error('数据库查询错误:', err);
      throw err;
    } finally {
      if (connection) {
        connection.release();
      }
    }
  }
  
  async transaction(transactionFn) {
    const connection = await this.pool.getConnection();
    
    try {
      await connection.beginTransaction();
      
      const result = await transactionFn(connection);
      
      await connection.commit();
      return result;
    } catch (err) {
      await connection.rollback();
      throw err;
    } finally {
      connection.release();
    }
  }
  
  async healthCheck() {
    try {
      const connection = await this.pool.getConnection();
      await connection.ping();
      connection.release();
      return { status: 'healthy', timestamp: new Date() };
    } catch (err) {
      return { status: 'unhealthy', error: err.message, timestamp: new Date() };
    }
  }
  
  async close() {
    await this.pool.end();
  }
}

module.exports = new DatabasePool();

部署检查清单

部署前检查

javascript
// deployment-checklist.js
class DeploymentChecklist {
  static async runPreDeploymentChecks() {
    console.log('开始部署前检查...');
    
    const checks = [
      this.checkEnvironmentVariables,
      this.checkDatabaseConnection,
      this.checkDependencies,
      this.checkDiskSpace,
      this.checkPortAvailability,
      this.checkSSLConfiguration
    ];
    
    let allPassed = true;
    
    for (const check of checks) {
      try {
        await check();
        console.log(`${check.name} 检查通过`);
      } catch (error) {
        console.error(`${check.name} 检查失败:`, error.message);
        allPassed = false;
      }
    }
    
    if (!allPassed) {
      throw new Error('部署前检查未全部通过');
    }
    
    console.log('所有部署前检查通过!');
    return true;
  }
  
  static async checkEnvironmentVariables() {
    const required = ['NODE_ENV', 'PORT', 'DB_HOST', 'DB_USER', 'DB_PASSWORD'];
    const missing = required.filter(key => !process.env[key]);
    
    if (missing.length > 0) {
      throw new Error(`缺少环境变量: ${missing.join(', ')}`);
    }
  }
  
  static async checkDatabaseConnection() {
    // 实际的数据库连接检查
    console.log('检查数据库连接...');
    // 这里应该实现实际的数据库连接测试
  }
  
  static async checkDependencies() {
    // 检查依赖版本和安全漏洞
    console.log('检查依赖...');
    // 这里应该实现依赖检查逻辑
  }
  
  static async checkDiskSpace() {
    // 检查磁盘空间
    console.log('检查磁盘空间...');
    // 这里应该实现磁盘空间检查
  }
  
  static async checkPortAvailability() {
    // 检查端口可用性
    console.log('检查端口可用性...');
    // 这里应该实现端口检查逻辑
  }
  
  static async checkSSLConfiguration() {
    // 检查SSL配置
    console.log('检查SSL配置...');
    // 这里应该实现SSL检查逻辑
  }
}

module.exports = DeploymentChecklist;

健康检查和自愈

健康检查端点

javascript
// health-check.js
const os = require('os');
const database = require('./database');

class HealthChecker {
  constructor() {
    this.startupTime = new Date();
  }
  
  async getHealthStatus() {
    const checks = await Promise.allSettled([
      this.checkDatabase(),
      this.checkMemory(),
      this.checkDisk(),
      this.checkLoad()
    ]);
    
    const status = {
      status: 'pass',
      timestamp: new Date().toISOString(),
      uptime: process.uptime(),
      checks: {
        database: this.formatCheckResult(checks[0]),
        memory: this.formatCheckResult(checks[1]),
        disk: this.formatCheckResult(checks[2]),
        load: this.formatCheckResult(checks[3])
      }
    };
    
    // 如果有任何检查失败,将整体状态设为fail
    if (checks.some(result => result.status === 'rejected')) {
      status.status = 'fail';
    }
    
    return status;
  }
  
  async checkDatabase() {
    const result = await database.healthCheck();
    return result;
  }
  
  checkMemory() {
    const usage = process.memoryUsage();
    const heapUsedPercent = (usage.heapUsed / usage.heapTotal) * 100;
    
    return {
      status: heapUsedPercent < 80 ? 'pass' : 'warn',
      heapUsedPercent: heapUsedPercent,
      usage: usage
    };
  }
  
  checkDisk() {
    // 简化的磁盘检查(实际应用中需要更复杂的实现)
    const total = os.totalmem();
    const free = os.freemem();
    const usedPercent = ((total - free) / total) * 100;
    
    return {
      status: usedPercent < 80 ? 'pass' : 'warn',
      usedPercent: usedPercent
    };
  }
  
  checkLoad() {
    const load = os.loadavg();
    const avgLoad = load[0];
    
    return {
      status: avgLoad < 2.0 ? 'pass' : 'warn',
      load: load
    };
  }
  
  formatCheckResult(result) {
    if (result.status === 'fulfilled') {
      return result.value;
    } else {
      return {
        status: 'fail',
        error: result.reason.message
      };
    }
  }
}

const healthChecker = new HealthChecker();

// 在Express应用中使用
function healthCheckMiddleware(healthChecker) {
  return async (req, res) => {
    try {
      const status = await healthChecker.getHealthStatus();
      res.status(status.status === 'pass' ? 200 : 503).json(status);
    } catch (error) {
      res.status(503).json({
        status: 'fail',
        error: error.message,
        timestamp: new Date().toISOString()
      });
    }
  };
}

module.exports = { healthChecker, healthCheckMiddleware };

通过实施这些部署和运维最佳实践,可以确保Node.js应用在生产环境中稳定、高效地运行。