Skip to content

Sequelize 部署与运维 - 生产环境最佳实践

发布时间:2024-04-10
作者:一介布衣
标签:Sequelize, 部署, 运维, 生产环境, 监控

前言

今天咱们来聊聊 Sequelize 应用的部署和运维。说实话,开发环境跑得好好的应用,到了生产环境就各种问题,这是很多开发者都遇到过的痛点。

我记得第一次部署 Sequelize 应用到生产环境时,遇到了各种问题:连接池配置不当导致连接泄漏、迁移脚本执行失败、性能监控缺失等等。后来慢慢积累经验,总结出了一套完整的部署和运维方案。

今天我就把这些生产环境的最佳实践分享给大家。

生产环境配置

1. 数据库连接配置

javascript
// config/database.js
const config = {
  production: {
    username: process.env.DB_USER,
    password: process.env.DB_PASS,
    database: process.env.DB_NAME,
    host: process.env.DB_HOST,
    port: process.env.DB_PORT || 3306,
    dialect: 'mysql',
    
    // 连接池配置(重要!)
    pool: {
      max: 20,          // 最大连接数
      min: 5,           // 最小连接数
      acquire: 30000,   // 获取连接超时时间
      idle: 10000,      // 连接空闲时间
      evict: 1000,      // 检查空闲连接间隔
      handleDisconnects: true  // 自动处理断开连接
    },
    
    // 查询超时
    dialectOptions: {
      acquireTimeout: 60000,
      timeout: 60000,
      
      // SSL 配置(如果需要)
      ssl: process.env.DB_SSL === 'true' ? {
        require: true,
        rejectUnauthorized: false
      } : false,
      
      // 字符集配置
      charset: 'utf8mb4',
      collate: 'utf8mb4_unicode_ci'
    },
    
    // 生产环境关闭日志
    logging: false,
    
    // 重试配置
    retry: {
      max: 3,
      match: [
        /ETIMEDOUT/,
        /EHOSTUNREACH/,
        /ECONNRESET/,
        /ECONNREFUSED/,
        /ETIMEDOUT/,
        /ESOCKETTIMEDOUT/,
        /EHOSTUNREACH/,
        /EPIPE/,
        /EAI_AGAIN/,
        /SequelizeConnectionError/,
        /SequelizeConnectionRefusedError/,
        /SequelizeHostNotFoundError/,
        /SequelizeHostNotReachableError/,
        /SequelizeInvalidConnectionError/,
        /SequelizeConnectionTimedOutError/
      ]
    }
  }
};

module.exports = config;

2. 环境变量管理

bash
# .env.production
NODE_ENV=production

# 数据库配置
DB_HOST=your-db-host
DB_PORT=3306
DB_NAME=your_production_db
DB_USER=your_db_user
DB_PASS=your_secure_password
DB_SSL=true

# 应用配置
PORT=3000
JWT_SECRET=your-super-secure-jwt-secret
ENCRYPTION_KEY=your-encryption-key

# 外部服务
REDIS_URL=redis://your-redis-host:6379
EMAIL_SERVICE_API_KEY=your-email-api-key

# 监控配置
SENTRY_DSN=your-sentry-dsn
NEW_RELIC_LICENSE_KEY=your-newrelic-key

3. 安全配置

javascript
// config/security.js
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');

module.exports = {
  // 安全头配置
  helmet: helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        scriptSrc: ["'self'"],
        imgSrc: ["'self'", "data:", "https:"]
      }
    }
  }),
  
  // 限流配置
  rateLimit: rateLimit({
    windowMs: 15 * 60 * 1000, // 15分钟
    max: 100, // 限制每个IP 15分钟内最多100个请求
    message: {
      error: '请求过于频繁,请稍后再试'
    }
  }),
  
  // API 限流
  apiRateLimit: rateLimit({
    windowMs: 15 * 60 * 1000,
    max: 1000,
    skip: (req) => {
      // 跳过内部服务调用
      return req.ip === '127.0.0.1' || req.ip === '::1';
    }
  })
};

数据库迁移策略

1. 生产环境迁移脚本

bash
#!/bin/bash
# scripts/deploy-migrate.sh

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

echo "开始数据库迁移..."

# 备份数据库
echo "备份数据库..."
mysqldump -h $DB_HOST -u $DB_USER -p$DB_PASS $DB_NAME > backup_$(date +%Y%m%d_%H%M%S).sql

# 检查迁移状态
echo "检查迁移状态..."
npx sequelize-cli db:migrate:status

# 执行迁移
echo "执行迁移..."
npx sequelize-cli db:migrate

# 验证迁移结果
echo "验证迁移结果..."
npx sequelize-cli db:migrate:status

echo "迁移完成!"

2. 零停机迁移策略

javascript
// migrations/safe-migration-example.js
'use strict';

module.exports = {
  async up(queryInterface, Sequelize) {
    const transaction = await queryInterface.sequelize.transaction();
    
    try {
      // 第一步:添加新字段(允许为空)
      await queryInterface.addColumn('users', 'new_email', {
        type: Sequelize.STRING(150),
        allowNull: true
      }, { transaction });
      
      // 第二步:添加索引(在线添加)
      await queryInterface.addIndex('users', ['new_email'], {
        name: 'users_new_email_index',
        concurrently: true  // PostgreSQL 支持
      }, { transaction });
      
      await transaction.commit();
      
      console.log('迁移第一阶段完成,应用可以继续运行');
      
    } catch (error) {
      await transaction.rollback();
      throw error;
    }
  },

  async down(queryInterface, Sequelize) {
    const transaction = await queryInterface.sequelize.transaction();
    
    try {
      await queryInterface.removeIndex('users', 'users_new_email_index', { transaction });
      await queryInterface.removeColumn('users', 'new_email', { transaction });
      
      await transaction.commit();
    } catch (error) {
      await transaction.rollback();
      throw error;
    }
  }
};

3. 迁移回滚策略

bash
#!/bin/bash
# scripts/rollback-migration.sh

MIGRATION_FILE=$1

if [ -z "$MIGRATION_FILE" ]; then
  echo "请指定要回滚到的迁移文件"
  echo "用法: ./rollback-migration.sh 20240410000000-migration-name.js"
  exit 1
fi

echo "警告:即将回滚到迁移文件 $MIGRATION_FILE"
echo "这可能会导致数据丢失,请确认操作"
read -p "确认回滚?(yes/no): " confirm

if [ "$confirm" = "yes" ]; then
  echo "创建回滚前备份..."
  mysqldump -h $DB_HOST -u $DB_USER -p$DB_PASS $DB_NAME > rollback_backup_$(date +%Y%m%d_%H%M%S).sql
  
  echo "执行回滚..."
  npx sequelize-cli db:migrate:undo:all --to $MIGRATION_FILE
  
  echo "回滚完成"
else
  echo "回滚已取消"
fi

监控和日志

1. 应用监控

javascript
// middleware/monitoring.js
const prometheus = require('prom-client');

// 创建指标
const httpRequestDuration = new prometheus.Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP请求耗时',
  labelNames: ['method', 'route', 'status_code']
});

const dbConnectionPool = new prometheus.Gauge({
  name: 'db_connection_pool_size',
  help: '数据库连接池大小',
  labelNames: ['type']
});

const dbQueryDuration = new prometheus.Histogram({
  name: 'db_query_duration_seconds',
  help: '数据库查询耗时',
  labelNames: ['operation']
});

// 监控中间件
function monitoringMiddleware(req, res, next) {
  const start = Date.now();
  
  res.on('finish', () => {
    const duration = (Date.now() - start) / 1000;
    httpRequestDuration
      .labels(req.method, req.route?.path || req.path, res.statusCode)
      .observe(duration);
  });
  
  next();
}

// 数据库监控
function setupDatabaseMonitoring(sequelize) {
  // 监控连接池
  setInterval(() => {
    const pool = sequelize.connectionManager.pool;
    dbConnectionPool.labels('total').set(pool.size);
    dbConnectionPool.labels('used').set(pool.using);
    dbConnectionPool.labels('waiting').set(pool.waiting);
  }, 10000);
  
  // 监控查询性能
  sequelize.addHook('beforeQuery', (options) => {
    options.startTime = Date.now();
  });
  
  sequelize.addHook('afterQuery', (options) => {
    const duration = (Date.now() - options.startTime) / 1000;
    const operation = options.type || 'unknown';
    dbQueryDuration.labels(operation).observe(duration);
  });
}

module.exports = {
  monitoringMiddleware,
  setupDatabaseMonitoring,
  register: prometheus.register
};

2. 结构化日志

javascript
// utils/logger.js
const winston = require('winston');
const { format } = winston;

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: format.combine(
    format.timestamp(),
    format.errors({ stack: true }),
    format.json()
  ),
  defaultMeta: {
    service: 'sequelize-app',
    version: process.env.APP_VERSION || '1.0.0'
  },
  transports: [
    // 错误日志
    new winston.transports.File({
      filename: 'logs/error.log',
      level: 'error',
      maxsize: 10485760, // 10MB
      maxFiles: 5
    }),
    
    // 所有日志
    new winston.transports.File({
      filename: 'logs/combined.log',
      maxsize: 10485760,
      maxFiles: 10
    })
  ]
});

// 生产环境不输出到控制台
if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: format.combine(
      format.colorize(),
      format.simple()
    )
  }));
}

// 数据库操作日志
function logDatabaseOperation(operation, model, data, duration) {
  logger.info('Database operation', {
    operation,
    model,
    duration,
    recordCount: Array.isArray(data) ? data.length : 1,
    timestamp: new Date().toISOString()
  });
}

// 错误日志
function logError(error, context = {}) {
  logger.error('Application error', {
    message: error.message,
    stack: error.stack,
    ...context,
    timestamp: new Date().toISOString()
  });
}

module.exports = {
  logger,
  logDatabaseOperation,
  logError
};

3. 健康检查

javascript
// routes/health.js
const express = require('express');
const router = express.Router();
const { sequelize } = require('../models');
const redis = require('../config/redis');

// 基础健康检查
router.get('/health', (req, res) => {
  res.json({
    status: 'ok',
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    memory: process.memoryUsage(),
    version: process.env.APP_VERSION || '1.0.0'
  });
});

// 详细健康检查
router.get('/health/detailed', async (req, res) => {
  const health = {
    status: 'ok',
    timestamp: new Date().toISOString(),
    services: {}
  };

  // 检查数据库连接
  try {
    await sequelize.authenticate();
    health.services.database = {
      status: 'ok',
      responseTime: await measureResponseTime(() => sequelize.authenticate())
    };
  } catch (error) {
    health.status = 'error';
    health.services.database = {
      status: 'error',
      error: error.message
    };
  }

  // 检查 Redis 连接
  try {
    await redis.ping();
    health.services.redis = {
      status: 'ok',
      responseTime: await measureResponseTime(() => redis.ping())
    };
  } catch (error) {
    health.status = 'degraded';
    health.services.redis = {
      status: 'error',
      error: error.message
    };
  }

  const statusCode = health.status === 'ok' ? 200 : 
                    health.status === 'degraded' ? 200 : 503;
  
  res.status(statusCode).json(health);
});

async function measureResponseTime(fn) {
  const start = Date.now();
  await fn();
  return Date.now() - start;
}

module.exports = router;

性能优化

1. 连接池优化

javascript
// config/database-optimized.js
const config = {
  production: {
    // ... 其他配置
    
    pool: {
      // 根据服务器规格调整
      max: Math.min(20, process.env.DB_MAX_CONNECTIONS || 20),
      min: 5,
      
      // 连接获取超时
      acquire: 30000,
      
      // 空闲连接超时
      idle: 10000,
      
      // 连接检查间隔
      evict: 1000,
      
      // 自动处理断开连接
      handleDisconnects: true,
      
      // 连接验证
      validate: (client) => {
        return client && !client.connection._closing;
      }
    },
    
    // 查询优化
    dialectOptions: {
      // 查询超时
      timeout: 60000,
      
      // 连接超时
      acquireTimeout: 60000,
      
      // 启用查询缓存(MySQL)
      queryCache: true,
      
      // 批量插入优化
      multipleStatements: true
    }
  }
};

2. 查询优化监控

javascript
// middleware/queryOptimization.js
const { logger } = require('../utils/logger');

function setupQueryOptimization(sequelize) {
  // 慢查询监控
  sequelize.addHook('beforeQuery', (options) => {
    options.startTime = process.hrtime.bigint();
  });

  sequelize.addHook('afterQuery', (options, result) => {
    const endTime = process.hrtime.bigint();
    const duration = Number(endTime - options.startTime) / 1000000; // 转换为毫秒

    // 记录慢查询
    if (duration > 1000) { // 超过1秒
      logger.warn('Slow query detected', {
        sql: options.sql,
        duration: `${duration.toFixed(2)}ms`,
        type: options.type,
        model: options.model?.name
      });
    }

    // 记录查询统计
    if (process.env.NODE_ENV === 'development') {
      console.log(`Query: ${duration.toFixed(2)}ms - ${options.sql.substring(0, 100)}...`);
    }
  });

  // N+1 查询检测
  const queryCount = new Map();
  
  sequelize.addHook('beforeQuery', (options) => {
    const key = `${options.model?.name || 'unknown'}-${options.type}`;
    queryCount.set(key, (queryCount.get(key) || 0) + 1);
  });

  // 定期检查 N+1 查询
  setInterval(() => {
    for (const [key, count] of queryCount.entries()) {
      if (count > 10) { // 短时间内同类查询超过10次
        logger.warn('Potential N+1 query detected', {
          queryType: key,
          count
        });
      }
    }
    queryCount.clear();
  }, 60000); // 每分钟检查一次
}

module.exports = { setupQueryOptimization };

备份和恢复

1. 自动备份脚本

bash
#!/bin/bash
# scripts/backup.sh

# 配置
BACKUP_DIR="/var/backups/mysql"
RETENTION_DAYS=7
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/backup_$DATE.sql"

# 创建备份目录
mkdir -p $BACKUP_DIR

# 执行备份
echo "开始备份数据库..."
mysqldump \
  --host=$DB_HOST \
  --user=$DB_USER \
  --password=$DB_PASS \
  --single-transaction \
  --routines \
  --triggers \
  --events \
  --hex-blob \
  --quick \
  --lock-tables=false \
  $DB_NAME > $BACKUP_FILE

# 压缩备份文件
gzip $BACKUP_FILE

# 检查备份是否成功
if [ $? -eq 0 ]; then
  echo "备份成功: $BACKUP_FILE.gz"
  
  # 上传到云存储(可选)
  # aws s3 cp $BACKUP_FILE.gz s3://your-backup-bucket/
  
else
  echo "备份失败"
  exit 1
fi

# 清理旧备份
find $BACKUP_DIR -name "backup_*.sql.gz" -mtime +$RETENTION_DAYS -delete

echo "备份完成"

2. 恢复脚本

bash
#!/bin/bash
# scripts/restore.sh

BACKUP_FILE=$1

if [ -z "$BACKUP_FILE" ]; then
  echo "请指定备份文件"
  echo "用法: ./restore.sh /path/to/backup.sql.gz"
  exit 1
fi

echo "警告:这将覆盖当前数据库"
read -p "确认恢复?(yes/no): " confirm

if [ "$confirm" = "yes" ]; then
  echo "开始恢复数据库..."
  
  # 解压备份文件
  if [[ $BACKUP_FILE == *.gz ]]; then
    gunzip -c $BACKUP_FILE | mysql \
      --host=$DB_HOST \
      --user=$DB_USER \
      --password=$DB_PASS \
      $DB_NAME
  else
    mysql \
      --host=$DB_HOST \
      --user=$DB_USER \
      --password=$DB_PASS \
      $DB_NAME < $BACKUP_FILE
  fi
  
  if [ $? -eq 0 ]; then
    echo "恢复成功"
  else
    echo "恢复失败"
    exit 1
  fi
else
  echo "恢复已取消"
fi

Docker 部署

1. Dockerfile

dockerfile
# Dockerfile
FROM node:18-alpine

# 设置工作目录
WORKDIR /app

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

# 安装依赖
RUN npm ci --only=production && npm cache clean --force

# 复制应用代码
COPY . .

# 创建非 root 用户
RUN addgroup -g 1001 -S nodejs && \
    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"]

2. Docker Compose

yaml
# docker-compose.prod.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DB_HOST=db
      - DB_NAME=myapp
      - DB_USER=myapp_user
      - DB_PASS=secure_password
      - REDIS_URL=redis://redis:6379
    depends_on:
      - db
      - redis
    restart: unless-stopped
    volumes:
      - ./logs:/app/logs
    networks:
      - app-network

  db:
    image: mysql:8.0
    environment:
      - MYSQL_ROOT_PASSWORD=root_password
      - MYSQL_DATABASE=myapp
      - MYSQL_USER=myapp_user
      - MYSQL_PASSWORD=secure_password
    volumes:
      - db_data:/var/lib/mysql
      - ./backups:/backups
    restart: unless-stopped
    networks:
      - app-network

  redis:
    image: redis:7-alpine
    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:
  db_data:

networks:
  app-network:
    driver: bridge

部署自动化

1. 部署脚本

bash
#!/bin/bash
# scripts/deploy.sh

set -e

echo "开始部署..."

# 拉取最新代码
git pull origin main

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

# 运行测试
npm test

# 备份数据库
./scripts/backup.sh

# 执行迁移
./scripts/deploy-migrate.sh

# 重启应用
pm2 reload ecosystem.config.js --env production

# 健康检查
sleep 10
curl -f http://localhost:3000/health || exit 1

echo "部署完成"

2. PM2 配置

javascript
// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'sequelize-app',
    script: './src/app.js',
    instances: 'max',
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'development'
    },
    env_production: {
      NODE_ENV: 'production',
      PORT: 3000
    },
    
    // 监控配置
    monitoring: true,
    pmx: true,
    
    // 日志配置
    log_file: './logs/combined.log',
    out_file: './logs/out.log',
    error_file: './logs/error.log',
    log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
    
    // 重启配置
    max_restarts: 10,
    min_uptime: '10s',
    max_memory_restart: '1G',
    
    // 优雅关闭
    kill_timeout: 5000,
    listen_timeout: 3000,
    
    // 健康检查
    health_check_grace_period: 3000
  }]
};

总结

今天我们深入学习了 Sequelize 应用的部署和运维:

  • ✅ 生产环境配置和安全设置
  • ✅ 数据库迁移和回滚策略
  • ✅ 监控、日志和健康检查
  • ✅ 性能优化和查询监控
  • ✅ 备份恢复和容灾方案
  • ✅ Docker 容器化部署
  • ✅ 自动化部署流程

掌握了这些知识,你就能够:

  • 构建稳定可靠的生产环境
  • 实现高效的运维管理
  • 保证应用的高可用性
  • 快速响应和解决问题

生产环境的稳定性是应用成功的关键,希望这些实践能帮助大家构建更好的系统!


相关文章推荐:

有问题欢迎留言讨论,我会及时回复大家!