Skip to content

Feathers.js 部署与运维 - 生产环境最佳实践

发布时间:2024-07-15
作者:一介布衣
标签:Feathers.js, 部署, 运维, Docker, 生产环境

前言

经过前面十几篇文章的学习,相信大家已经能够熟练开发 Feathers.js 应用了。今天咱们来学习最后也是最重要的一环 - 如何将应用部署到生产环境。说实话,开发和部署是两个完全不同的世界,很多在开发环境运行良好的应用,到了生产环境就各种问题。

我记得第一次部署 Node.js 应用的时候,踩了无数的坑:环境变量配置错误、数据库连接超时、内存泄漏、进程崩溃等等。后来总结了一套完整的部署流程,才让应用在生产环境稳定运行。

今天我就把这些年积累的部署和运维经验分享给大家,让你的 Feathers.js 应用能够稳定、高效地运行在生产环境中。

生产环境准备

1. 环境配置

javascript
// config/production.json
{
  "host": "0.0.0.0",
  "port": 3030,
  "public": "./public/",
  "origins": [
    "https://yourdomain.com",
    "https://www.yourdomain.com"
  ],
  "mongodb": "mongodb://username:password@mongodb:27017/production_db?authSource=admin",
  "redis": {
    "host": "redis",
    "port": 6379,
    "password": "your_redis_password"
  },
  "authentication": {
    "secret": "your-super-secret-production-key",
    "strategies": ["jwt", "local"],
    "path": "/authentication",
    "service": "users",
    "jwt": {
      "header": { "typ": "access" },
      "audience": "https://yourdomain.com",
      "subject": "anonymous",
      "issuer": "feathers",
      "algorithm": "HS256",
      "expiresIn": "7d"
    }
  },
  "paginate": {
    "default": 10,
    "max": 100
  },
  "rateLimit": {
    "windowMs": 900000,
    "max": 100
  },
  "cors": {
    "origin": ["https://yourdomain.com", "https://www.yourdomain.com"],
    "credentials": true
  }
}

// .env.production
NODE_ENV=production
PORT=3030
HOST=0.0.0.0

# 数据库
MONGODB_URI=mongodb://username:password@mongodb:27017/production_db?authSource=admin
REDIS_URL=redis://username:password@redis:6379

# 认证
JWT_SECRET=your-super-secret-production-key
BCRYPT_ROUNDS=12

# 文件上传
UPLOAD_PATH=/app/uploads
MAX_FILE_SIZE=10485760

# 邮件服务
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password

# 监控
SENTRY_DSN=https://your-sentry-dsn
LOG_LEVEL=info

# 缓存
CACHE_TTL=3600
CACHE_MAX_SIZE=1000

2. 安全配置

javascript
// src/app.js - 生产环境安全配置
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const compression = require('compression');

// 安全头部
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
      fontSrc: ["'self'", "https://fonts.gstatic.com"],
      imgSrc: ["'self'", "data:", "https:"],
      scriptSrc: ["'self'"],
      connectSrc: ["'self'", "wss:", "https:"]
    }
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  }
}));

// 压缩响应
app.use(compression({
  level: 6,
  threshold: 1024,
  filter: (req, res) => {
    if (req.headers['x-no-compression']) {
      return false;
    }
    return compression.filter(req, res);
  }
}));

// 速率限制
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分钟
  max: 100, // 限制每个IP 100个请求
  message: {
    error: {
      message: '请求过于频繁,请稍后再试',
      code: 429
    }
  },
  standardHeaders: true,
  legacyHeaders: false,
  skip: (req) => {
    // 跳过健康检查
    return req.path === '/health';
  }
});

app.use('/api', limiter);

// API 特定的速率限制
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5, // 登录尝试限制
  skipSuccessfulRequests: true
});

app.use('/authentication', authLimiter);

3. 日志配置

javascript
// src/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: 'feathers-app',
    version: process.env.npm_package_version 
  },
  transports: [
    // 错误日志
    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,
      maxFiles: 10
    })
  ]
});

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

// 生产环境错误监控
if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) {
  const Sentry = require('@sentry/node');
  
  Sentry.init({
    dsn: process.env.SENTRY_DSN,
    environment: process.env.NODE_ENV,
    tracesSampleRate: 0.1
  });
  
  logger.add(new winston.transports.Console({
    level: 'error',
    format: format.combine(
      format.timestamp(),
      format.json()
    ),
    handleExceptions: true,
    handleRejections: true
  }));
}

module.exports = logger;

Docker 容器化

1. Dockerfile

dockerfile
# Dockerfile
FROM node:18-alpine AS builder

# 设置工作目录
WORKDIR /app

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

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

# 复制源代码
COPY . .

# 构建应用
RUN npm run build

# 生产镜像
FROM node:18-alpine AS production

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

# 设置工作目录
WORKDIR /app

# 复制依赖和构建结果
COPY --from=builder --chown=feathers:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=feathers:nodejs /app/dist ./dist
COPY --from=builder --chown=feathers:nodejs /app/package*.json ./
COPY --from=builder --chown=feathers:nodejs /app/config ./config

# 创建必要的目录
RUN mkdir -p logs uploads && chown -R feathers:nodejs logs uploads

# 切换到非 root 用户
USER feathers

# 暴露端口
EXPOSE 3030

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node healthcheck.js

# 启动应用
CMD ["node", "dist/index.js"]

2. 健康检查

javascript
// healthcheck.js
const http = require('http');

const options = {
  host: 'localhost',
  port: process.env.PORT || 3030,
  path: '/health',
  timeout: 2000
};

const request = http.request(options, (res) => {
  console.log(`健康检查状态: \${res.statusCode}`);
  if (res.statusCode === 200) {
    process.exit(0);
  } else {
    process.exit(1);
  }
});

request.on('error', (err) => {
  console.log('健康检查失败:', err);
  process.exit(1);
});

request.end();

3. Docker Compose

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

services:
  app:
    build: .
    ports:
      - "3030:3030"
    environment:
      - NODE_ENV=production
      - MONGODB_URI=mongodb://mongo:27017/feathers_app
      - REDIS_URL=redis://redis:6379
    depends_on:
      - mongo
      - redis
    volumes:
      - ./uploads:/app/uploads
      - ./logs:/app/logs
    restart: unless-stopped
    networks:
      - app-network

  mongo:
    image: mongo:5.0
    environment:
      - MONGO_INITDB_ROOT_USERNAME=admin
      - MONGO_INITDB_ROOT_PASSWORD=password
      - MONGO_INITDB_DATABASE=feathers_app
    volumes:
      - mongo-data:/data/db
      - ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
    restart: unless-stopped
    networks:
      - app-network

  redis:
    image: redis:7-alpine
    command: redis-server --requirepass password
    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:ro
      - ./ssl:/etc/nginx/ssl:ro
      - ./uploads:/var/www/uploads:ro
    depends_on:
      - app
    restart: unless-stopped
    networks:
      - app-network

volumes:
  mongo-data:
  redis-data:

networks:
  app-network:
    driver: bridge

4. Nginx 配置

nginx
# nginx.conf
events {
    worker_connections 1024;
}

http {
    upstream app {
        server app:3030;
    }

    # 速率限制
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=auth:10m rate=1r/s;

    # 文件上传大小限制
    client_max_body_size 10M;

    # Gzip 压缩
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

    server {
        listen 80;
        server_name yourdomain.com www.yourdomain.com;
        return 301 https://$server_name$request_uri;
    }

    server {
        listen 443 ssl http2;
        server_name yourdomain.com www.yourdomain.com;

        # SSL 配置
        ssl_certificate /etc/nginx/ssl/cert.pem;
        ssl_certificate_key /etc/nginx/ssl/key.pem;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
        ssl_prefer_server_ciphers off;

        # 安全头部
        add_header X-Frame-Options DENY;
        add_header X-Content-Type-Options nosniff;
        add_header X-XSS-Protection "1; mode=block";
        add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";

        # API 代理
        location /api/ {
            limit_req zone=api burst=20 nodelay;
            
            proxy_pass http://app/;
            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;
            proxy_read_timeout 86400;
        }

        # 认证接口特殊限制
        location /api/authentication {
            limit_req zone=auth burst=5 nodelay;
            
            proxy_pass http://app/authentication;
            proxy_http_version 1.1;
            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;
        }

        # WebSocket 支持
        location /socket.io/ {
            proxy_pass http://app/socket.io/;
            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;
        }

        # 静态文件
        location /uploads/ {
            alias /var/www/uploads/;
            expires 1y;
            add_header Cache-Control "public, immutable";
        }

        # 健康检查
        location /health {
            proxy_pass http://app/health;
            access_log off;
        }
    }
}

监控和日志

1. 应用监控

javascript
// src/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]
});

const httpRequestTotal = new prometheus.Counter({
  name: 'http_requests_total',
  help: 'HTTP 请求总数',
  labelNames: ['method', 'route', 'status_code']
});

const activeConnections = new prometheus.Gauge({
  name: 'websocket_connections_active',
  help: '活跃的 WebSocket 连接数'
});

const databaseOperations = new prometheus.Counter({
  name: 'database_operations_total',
  help: '数据库操作总数',
  labelNames: ['operation', 'collection', 'status']
});

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

// 导出指标
module.exports = {
  metricsMiddleware,
  register: prometheus.register,
  metrics: {
    httpRequestDuration,
    httpRequestTotal,
    activeConnections,
    databaseOperations
  }
};

2. 性能监控

javascript
// src/performance.js
const logger = require('./logger');

class PerformanceMonitor {
  constructor() {
    this.metrics = {
      requests: new Map(),
      memory: [],
      cpu: []
    };
    
    this.startMonitoring();
  }

  startMonitoring() {
    // 内存监控
    setInterval(() => {
      const memUsage = process.memoryUsage();
      this.metrics.memory.push({
        timestamp: Date.now(),
        rss: memUsage.rss,
        heapUsed: memUsage.heapUsed,
        heapTotal: memUsage.heapTotal,
        external: memUsage.external
      });
      
      // 保留最近1小时的数据
      const oneHourAgo = Date.now() - 60 * 60 * 1000;
      this.metrics.memory = this.metrics.memory.filter(
        m => m.timestamp > oneHourAgo
      );
      
      // 内存泄漏检测
      if (memUsage.heapUsed > 500 * 1024 * 1024) { // 500MB
        logger.warn('内存使用过高', {
          heapUsed: memUsage.heapUsed,
          heapTotal: memUsage.heapTotal
        });
      }
    }, 30000); // 30秒检查一次

    // CPU 监控
    setInterval(() => {
      const cpuUsage = process.cpuUsage();
      this.metrics.cpu.push({
        timestamp: Date.now(),
        user: cpuUsage.user,
        system: cpuUsage.system
      });
      
      // 保留最近1小时的数据
      const oneHourAgo = Date.now() - 60 * 60 * 1000;
      this.metrics.cpu = this.metrics.cpu.filter(
        c => c.timestamp > oneHourAgo
      );
    }, 30000);
  }

  trackRequest(req, res, next) {
    const start = process.hrtime.bigint();
    const requestId = req.headers['x-request-id'] || Math.random().toString(36);
    
    req.requestId = requestId;
    req.startTime = start;
    
    res.on('finish', () => {
      const duration = Number(process.hrtime.bigint() - start) / 1000000; // ms
      
      const requestInfo = {
        id: requestId,
        method: req.method,
        url: req.url,
        statusCode: res.statusCode,
        duration: duration,
        userAgent: req.headers['user-agent'],
        ip: req.ip,
        timestamp: new Date()
      };
      
      // 慢请求警告
      if (duration > 5000) { // 5秒
        logger.warn('慢请求检测', requestInfo);
      }
      
      // 记录请求
      logger.info('请求完成', requestInfo);
    });
    
    next();
  }

  getMetrics() {
    return {
      memory: this.getMemoryStats(),
      cpu: this.getCpuStats(),
      uptime: process.uptime(),
      version: process.version,
      pid: process.pid
    };
  }

  getMemoryStats() {
    if (this.metrics.memory.length === 0) return null;
    
    const latest = this.metrics.memory[this.metrics.memory.length - 1];
    const avg = this.metrics.memory.reduce((sum, m) => sum + m.heapUsed, 0) / this.metrics.memory.length;
    
    return {
      current: latest,
      average: avg,
      samples: this.metrics.memory.length
    };
  }

  getCpuStats() {
    if (this.metrics.cpu.length === 0) return null;
    
    const latest = this.metrics.cpu[this.metrics.cpu.length - 1];
    
    return {
      current: latest,
      samples: this.metrics.cpu.length
    };
  }
}

module.exports = new PerformanceMonitor();

部署脚本

1. 自动化部署

bash
#!/bin/bash
# deploy.sh

set -e

echo "🚀 开始部署 Feathers.js 应用..."

# 配置
APP_NAME="feathers-app"
DOCKER_IMAGE="$APP_NAME:latest"
CONTAINER_NAME="$APP_NAME-container"

# 构建镜像
echo "📦 构建 Docker 镜像..."
docker build -t $DOCKER_IMAGE .

# 停止旧容器
echo "🛑 停止旧容器..."
docker stop $CONTAINER_NAME || true
docker rm $CONTAINER_NAME || true

# 启动新容器
echo "🔄 启动新容器..."
docker-compose up -d

# 等待应用启动
echo "⏳ 等待应用启动..."
sleep 30

# 健康检查
echo "🏥 执行健康检查..."
if curl -f http://localhost:3030/health; then
    echo "✅ 部署成功!"
else
    echo "❌ 健康检查失败,回滚部署..."
    docker-compose down
    exit 1
fi

# 清理旧镜像
echo "🧹 清理旧镜像..."
docker image prune -f

echo "🎉 部署完成!"

2. 零停机部署

bash
#!/bin/bash
# zero-downtime-deploy.sh

set -e

APP_NAME="feathers-app"
NEW_IMAGE="$APP_NAME:$(date +%s)"
BLUE_CONTAINER="$APP_NAME-blue"
GREEN_CONTAINER="$APP_NAME-green"

# 检查当前运行的容器
if docker ps | grep -q $BLUE_CONTAINER; then
    CURRENT_CONTAINER=$BLUE_CONTAINER
    NEW_CONTAINER=$GREEN_CONTAINER
    NEW_PORT=3031
else
    CURRENT_CONTAINER=$GREEN_CONTAINER
    NEW_CONTAINER=$BLUE_CONTAINER
    NEW_PORT=3030
fi

echo "🔄 当前容器: $CURRENT_CONTAINER"
echo "🆕 新容器: $NEW_CONTAINER"

# 构建新镜像
echo "📦 构建新镜像..."
docker build -t $NEW_IMAGE .

# 启动新容器
echo "🚀 启动新容器..."
docker run -d \
  --name $NEW_CONTAINER \
  --network feathers-network \
  -p $NEW_PORT:3030 \
  -e NODE_ENV=production \
  $NEW_IMAGE

# 等待新容器就绪
echo "⏳ 等待新容器就绪..."
for i in {1..30}; do
  if curl -f http://localhost:$NEW_PORT/health; then
    echo "✅ 新容器就绪"
    break
  fi
  
  if [ $i -eq 30 ]; then
    echo "❌ 新容器启动失败"
    docker stop $NEW_CONTAINER
    docker rm $NEW_CONTAINER
    exit 1
  fi
  
  sleep 2
done

# 更新负载均衡器配置
echo "🔄 更新负载均衡器..."
# 这里需要根据你的负载均衡器类型进行配置
# 例如更新 Nginx upstream 配置

# 等待流量切换完成
echo "⏳ 等待流量切换..."
sleep 10

# 停止旧容器
echo "🛑 停止旧容器..."
docker stop $CURRENT_CONTAINER || true
docker rm $CURRENT_CONTAINER || true

echo "🎉 零停机部署完成!"

生产环境优化

1. 进程管理

javascript
// ecosystem.config.js - PM2 配置
module.exports = {
  apps: [{
    name: 'feathers-app',
    script: './dist/index.js',
    instances: 'max',
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'development'
    },
    env_production: {
      NODE_ENV: 'production',
      PORT: 3030
    },
    error_file: './logs/err.log',
    out_file: './logs/out.log',
    log_file: './logs/combined.log',
    time: true,
    max_memory_restart: '1G',
    node_args: '--max-old-space-size=1024',
    watch: false,
    ignore_watch: ['node_modules', 'logs'],
    max_restarts: 10,
    min_uptime: '10s'
  }]
};

2. 数据库优化

javascript
// src/mongodb-optimized.js
const { MongoClient } = require('mongodb');

const mongoOptions = {
  useNewUrlParser: true,
  useUnifiedTopology: true,
  
  // 连接池配置
  maxPoolSize: 20,
  minPoolSize: 5,
  maxIdleTimeMS: 30000,
  
  // 连接超时
  serverSelectionTimeoutMS: 5000,
  socketTimeoutMS: 45000,
  connectTimeoutMS: 10000,
  
  // 重试配置
  retryWrites: true,
  retryReads: true,
  
  // 压缩
  compressors: ['zlib'],
  zlibCompressionLevel: 6,
  
  // 读写关注
  readPreference: 'secondaryPreferred',
  readConcern: { level: 'majority' },
  writeConcern: { w: 'majority', j: true }
};

module.exports = function (app) {
  const connection = app.get('mongodb');
  
  const mongoClient = MongoClient.connect(connection, mongoOptions)
    .then(client => {
      console.log('MongoDB 连接成功');
      
      // 监控连接池
      client.on('connectionPoolCreated', () => {
        console.log('MongoDB 连接池已创建');
      });
      
      client.on('connectionPoolClosed', () => {
        console.log('MongoDB 连接池已关闭');
      });
      
      return client.db();
    })
    .catch(error => {
      console.error('MongoDB 连接失败:', error);
      process.exit(1);
    });

  app.set('mongoClient', mongoClient);
};

总结

通过这篇文章,我们学习了 Feathers.js 应用的完整部署流程:

生产环境准备

  • 环境配置和安全设置
  • 日志和监控配置
  • 性能优化策略

容器化部署

  • Docker 镜像构建
  • Docker Compose 编排
  • Nginx 反向代理配置

监控和运维

  • 应用性能监控
  • 日志收集和分析
  • 健康检查机制

自动化部署

  • 部署脚本编写
  • 零停机部署策略
  • 进程管理优化

掌握了这些知识,你就能够将 Feathers.js 应用稳定、高效地部署到生产环境,并进行有效的运维管理。

至此,我们的 Feathers.js 系列文章就全部完成了!从入门到精通,从开发到部署,希望这个系列能够帮助大家真正掌握 Feathers.js 这个强大的框架。


系列文章回顾:

感谢大家的阅读和支持!如有问题欢迎留言讨论!