8.1 生产环境配置

8.1.1 环境变量配置

# .env.production
NODE_ENV=production
PORT=3000
HOST=0.0.0.0

# 数据库配置
DB_HOST=prod-db.example.com
DB_PORT=5432
DB_NAME=myapp_prod
DB_USER=myapp_user
DB_PASSWORD=secure_password

# Redis配置
REDIS_HOST=prod-redis.example.com
REDIS_PORT=6379
REDIS_PASSWORD=redis_password

# 安全配置
JWT_SECRET=your_jwt_secret_key
SESSION_SECRET=your_session_secret
CORS_ORIGIN=https://yourdomain.com

# 第三方服务
CDN_URL=https://cdn.yourdomain.com
API_BASE_URL=https://api.yourdomain.com
SENTRY_DSN=https://your-sentry-dsn

# 缓存配置
CACHE_TTL=300
COMPONENT_CACHE_MAX=1000
PAGE_CACHE_MAX=5000

# 日志配置
LOG_LEVEL=info
LOG_FILE=/var/log/myapp/app.log

# 性能配置
CLUSTER_WORKERS=4
MAX_MEMORY=512
GC_INTERVAL=30000

8.1.2 生产构建配置

// build/webpack.prod.js
const path = require('path')
const webpack = require('webpack')
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.base.js')
const TerserPlugin = require('terser-webpack-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const CompressionPlugin = require('compression-webpack-plugin')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')

module.exports = merge(baseConfig, {
  mode: 'production',
  devtool: 'source-map',
  
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true,
            drop_debugger: true,
            pure_funcs: ['console.log', 'console.info']
          },
          mangle: {
            safari10: true
          },
          format: {
            comments: false
          }
        },
        extractComments: false,
        parallel: true
      }),
      new CssMinimizerPlugin({
        minimizerOptions: {
          preset: [
            'default',
            {
              discardComments: { removeAll: true },
              normalizeWhitespace: true,
              mergeLonghand: true,
              mergeRules: true
            }
          ]
        }
      })
    ],
    
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10,
          enforce: true
        },
        common: {
          name: 'common',
          minChunks: 2,
          chunks: 'all',
          priority: 5,
          reuseExistingChunk: true
        },
        vue: {
          test: /[\\/]node_modules[\\/](vue|vue-router|vuex)[\\/]/,
          name: 'vue',
          chunks: 'all',
          priority: 20
        }
      }
    },
    
    runtimeChunk: {
      name: 'runtime'
    }
  },
  
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('production'),
      __VUE_OPTIONS_API__: true,
      __VUE_PROD_DEVTOOLS__: false
    }),
    
    // Gzip压缩
    new CompressionPlugin({
      algorithm: 'gzip',
      test: /\.(js|css|html|svg)$/,
      threshold: 8192,
      minRatio: 0.8,
      deleteOriginalAssets: false
    }),
    
    // Brotli压缩
    new CompressionPlugin({
      filename: '[path][base].br',
      algorithm: 'brotliCompress',
      test: /\.(js|css|html|svg)$/,
      compressionOptions: {
        level: 11
      },
      threshold: 8192,
      minRatio: 0.8,
      deleteOriginalAssets: false
    }),
    
    // 包分析(可选)
    ...(process.env.ANALYZE ? [
      new BundleAnalyzerPlugin({
        analyzerMode: 'static',
        openAnalyzer: false,
        reportFilename: 'bundle-report.html'
      })
    ] : [])
  ],
  
  performance: {
    maxAssetSize: 250000,
    maxEntrypointSize: 250000,
    hints: 'warning'
  }
})

8.1.3 服务器配置优化

// server/production.js
const express = require('express')
const compression = require('compression')
const helmet = require('helmet')
const rateLimit = require('express-rate-limit')
const cors = require('cors')
const cluster = require('cluster')
const numCPUs = require('os').cpus().length
const { createBundleRenderer } = require('vue-server-renderer')
const LRU = require('lru-cache')

class ProductionServer {
  constructor() {
    this.app = express()
    this.setupMiddleware()
    this.setupRenderer()
    this.setupRoutes()
    this.setupErrorHandling()
  }

  setupMiddleware() {
    // 安全中间件
    this.app.use(helmet({
      contentSecurityPolicy: {
        directives: {
          defaultSrc: ["'self'"],
          styleSrc: ["'self'", "'unsafe-inline'", process.env.CDN_URL],
          scriptSrc: ["'self'", process.env.CDN_URL],
          imgSrc: ["'self'", "data:", process.env.CDN_URL],
          fontSrc: ["'self'", process.env.CDN_URL],
          connectSrc: ["'self'", process.env.API_BASE_URL]
        }
      },
      hsts: {
        maxAge: 31536000,
        includeSubDomains: true,
        preload: true
      }
    }))

    // CORS配置
    this.app.use(cors({
      origin: process.env.CORS_ORIGIN.split(','),
      credentials: true,
      optionsSuccessStatus: 200
    }))

    // 压缩中间件
    this.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 15分钟内最多100个请求
      message: {
        error: 'Too many requests from this IP, please try again later.'
      },
      standardHeaders: true,
      legacyHeaders: false
    })
    this.app.use('/api/', limiter)

    // 静态文件服务
    this.app.use('/static', express.static('dist/static', {
      maxAge: '1y',
      etag: true,
      lastModified: true,
      setHeaders: (res, path) => {
        if (path.endsWith('.html')) {
          res.setHeader('Cache-Control', 'no-cache')
        }
      }
    }))

    // 请求日志中间件
function requestLogger(req, res, next) {
  const start = Date.now()
  const requestId = require('uuid').v4()
  
  // 添加请求ID到请求对象
  req.requestId = requestId
  res.setHeader('X-Request-ID', requestId)
  
  // 记录请求开始
  logger.info('Request started', {
    requestId,
    method: req.method,
    url: req.url,
    userAgent: req.get('User-Agent'),
    ip: req.ip,
    headers: req.headers
  })
  
  // 监听响应结束
  res.on('finish', () => {
    const duration = Date.now() - start
    const logLevel = res.statusCode >= 400 ? 'error' : 'info'
    
    logger.log(logLevel, 'Request completed', {
      requestId,
      method: req.method,
      url: req.url,
      statusCode: res.statusCode,
      duration,
      contentLength: res.get('Content-Length'),
      userAgent: req.get('User-Agent'),
      ip: req.ip
    })
  })
  
  next()
}

// SSR渲染日志
function logSSRRender(route, context, duration, error = null) {
  if (error) {
    logger.error('SSR render failed', {
      route,
      duration,
      error: error.message,
      stack: error.stack,
      context: {
        url: context.url,
        userAgent: context.userAgent
      }
    })
  } else {
    logger.info('SSR render completed', {
      route,
      duration,
      context: {
        url: context.url,
        userAgent: context.userAgent
      }
    })
  }
}

// 性能日志
function logPerformance(metric, value, tags = {}) {
  logger.info('Performance metric', {
    metric,
    value,
    tags,
    timestamp: Date.now()
  })
}

// 业务日志
function logBusiness(event, data = {}) {
  logger.info('Business event', {
    event,
    data,
    timestamp: Date.now()
  })
}

module.exports = {
  logger,
  requestLogger,
  logSSRRender,
  logPerformance,
  logBusiness
}

8.5.2 ELK Stack集成

# logging/filebeat.yml
filebeat.inputs:
- type: log
  enabled: true
  paths:
    - /var/log/myapp/*.log
  fields:
    service: vue-ssr-app
    environment: production
  fields_under_root: true
  multiline.pattern: '^{'
  multiline.negate: true
  multiline.match: after

processors:
- add_host_metadata:
    when.not.contains.tags: forwarded
- add_docker_metadata: ~
- add_kubernetes_metadata: ~

output.logstash:
  hosts: ["logstash:5044"]

logging.level: info
logging.to_files: true
logging.files:
  path: /var/log/filebeat
  name: filebeat
  keepfiles: 7
  permissions: 0644
# logging/logstash.conf
input {
  beats {
    port => 5044
  }
}

filter {
  if [service] == "vue-ssr-app" {
    json {
      source => "message"
    }
    
    date {
      match => [ "timestamp", "yyyy-MM-dd HH:mm:ss" ]
    }
    
    if [level] == "error" {
      mutate {
        add_tag => [ "error" ]
      }
    }
    
    if [duration] {
      mutate {
        convert => { "duration" => "integer" }
      }
    }
    
    if [statusCode] {
      mutate {
        convert => { "statusCode" => "integer" }
      }
    }
  }
}

output {
  elasticsearch {
    hosts => ["elasticsearch:9200"]
    index => "vue-ssr-app-%{+YYYY.MM.dd}"
  }
  
  if "error" in [tags] {
    email {
      to => "admin@yourdomain.com"
      subject => "Vue SSR App Error Alert"
      body => "Error detected: %{message}"
    }
  }
}

8.6 备份与恢复

8.6.1 数据库备份策略

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

set -e

# 配置变量
DB_HOST=${DB_HOST:-localhost}
DB_PORT=${DB_PORT:-5432}
DB_NAME=${DB_NAME:-myapp_prod}
DB_USER=${DB_USER:-myapp_user}
BACKUP_DIR=${BACKUP_DIR:-/backups/database}
RETENTION_DAYS=${RETENTION_DAYS:-30}
S3_BUCKET=${S3_BUCKET:-myapp-backups}

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

# 生成备份文件名
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="${DB_NAME}_${TIMESTAMP}.sql.gz"
BACKUP_PATH="${BACKUP_DIR}/${BACKUP_FILE}"

echo "开始数据库备份: $BACKUP_FILE"

# 执行备份
PGPASSWORD=$DB_PASSWORD pg_dump \
  -h $DB_HOST \
  -p $DB_PORT \
  -U $DB_USER \
  -d $DB_NAME \
  --verbose \
  --no-password \
  --format=custom \
  --compress=9 \
  | gzip > $BACKUP_PATH

if [ $? -eq 0 ]; then
    echo "数据库备份成功: $BACKUP_PATH"
    
    # 上传到S3
    if [ ! -z "$S3_BUCKET" ]; then
        echo "上传备份到S3..."
        aws s3 cp $BACKUP_PATH s3://$S3_BUCKET/database/
        
        if [ $? -eq 0 ]; then
            echo "S3上传成功"
        else
            echo "S3上传失败"
            exit 1
        fi
    fi
    
    # 清理旧备份
    echo "清理 $RETENTION_DAYS 天前的备份..."
    find $BACKUP_DIR -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete
    
    # 清理S3旧备份
    if [ ! -z "$S3_BUCKET" ]; then
        aws s3 ls s3://$S3_BUCKET/database/ | \
        while read -r line; do
            createDate=$(echo $line | awk '{print $1" "$2}')
            createDate=$(date -d "$createDate" +%s)
            olderThan=$(date -d "$RETENTION_DAYS days ago" +%s)
            if [[ $createDate -lt $olderThan ]]; then
                fileName=$(echo $line | awk '{print $4}')
                if [[ $fileName != "" ]]; then
                    aws s3 rm s3://$S3_BUCKET/database/$fileName
                    echo "删除S3旧备份: $fileName"
                fi
            fi
        done
    fi
    
else
    echo "数据库备份失败"
    exit 1
fi

echo "备份完成"

8.6.2 应用备份脚本

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

set -e

# 配置变量
APP_DIR=${APP_DIR:-/app}
BACKUP_DIR=${BACKUP_DIR:-/backups/application}
RETENTION_DAYS=${RETENTION_DAYS:-7}
S3_BUCKET=${S3_BUCKET:-myapp-backups}

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

# 生成备份文件名
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="app_${TIMESTAMP}.tar.gz"
BACKUP_PATH="${BACKUP_DIR}/${BACKUP_FILE}"

echo "开始应用备份: $BACKUP_FILE"

# 创建应用备份
tar -czf $BACKUP_PATH \
    --exclude='node_modules' \
    --exclude='logs' \
    --exclude='.git' \
    --exclude='dist' \
    -C $(dirname $APP_DIR) \
    $(basename $APP_DIR)

if [ $? -eq 0 ]; then
    echo "应用备份成功: $BACKUP_PATH"
    
    # 上传到S3
    if [ ! -z "$S3_BUCKET" ]; then
        echo "上传备份到S3..."
        aws s3 cp $BACKUP_PATH s3://$S3_BUCKET/application/
        
        if [ $? -eq 0 ]; then
            echo "S3上传成功"
        else
            echo "S3上传失败"
            exit 1
        fi
    fi
    
    # 清理旧备份
    echo "清理 $RETENTION_DAYS 天前的备份..."
    find $BACKUP_DIR -name "*.tar.gz" -mtime +$RETENTION_DAYS -delete
    
else
    echo "应用备份失败"
    exit 1
fi

echo "备份完成"

8.6.3 恢复脚本

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

set -e

# 配置变量
DB_HOST=${DB_HOST:-localhost}
DB_PORT=${DB_PORT:-5432}
DB_NAME=${DB_NAME:-myapp_prod}
DB_USER=${DB_USER:-myapp_user}
BACKUP_FILE=$1

if [ -z "$BACKUP_FILE" ]; then
    echo "用法: $0 <backup_file>"
    exit 1
fi

if [ ! -f "$BACKUP_FILE" ]; then
    echo "备份文件不存在: $BACKUP_FILE"
    exit 1
fi

echo "开始数据库恢复: $BACKUP_FILE"

# 确认恢复操作
read -p "确定要恢复数据库吗?这将覆盖现有数据 (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
    echo "恢复操作已取消"
    exit 1
fi

# 停止应用服务
echo "停止应用服务..."
kubectl scale deployment vue-ssr-app --replicas=0 -n vue-ssr-app

# 等待Pod停止
echo "等待Pod停止..."
kubectl wait --for=delete pod -l app=vue-ssr-app -n vue-ssr-app --timeout=60s

# 删除现有数据库
echo "删除现有数据库..."
PGPASSWORD=$DB_PASSWORD psql \
    -h $DB_HOST \
    -p $DB_PORT \
    -U $DB_USER \
    -d postgres \
    -c "DROP DATABASE IF EXISTS $DB_NAME;"

# 创建新数据库
echo "创建新数据库..."
PGPASSWORD=$DB_PASSWORD psql \
    -h $DB_HOST \
    -p $DB_PORT \
    -U $DB_USER \
    -d postgres \
    -c "CREATE DATABASE $DB_NAME;"

# 恢复数据
echo "恢复数据..."
if [[ $BACKUP_FILE == *.gz ]]; then
    gunzip -c $BACKUP_FILE | PGPASSWORD=$DB_PASSWORD pg_restore \
        -h $DB_HOST \
        -p $DB_PORT \
        -U $DB_USER \
        -d $DB_NAME \
        --verbose \
        --no-password
else
    PGPASSWORD=$DB_PASSWORD pg_restore \
        -h $DB_HOST \
        -p $DB_PORT \
        -U $DB_USER \
        -d $DB_NAME \
        --verbose \
        --no-password \
        $BACKUP_FILE
fi

if [ $? -eq 0 ]; then
    echo "数据库恢复成功"
    
    # 重启应用服务
    echo "重启应用服务..."
    kubectl scale deployment vue-ssr-app --replicas=3 -n vue-ssr-app
    
    # 等待服务就绪
    echo "等待服务就绪..."
    kubectl wait --for=condition=available deployment vue-ssr-app -n vue-ssr-app --timeout=300s
    
    echo "恢复完成"
else
    echo "数据库恢复失败"
    exit 1
fi

8.7 安全配置

8.7.1 HTTPS配置

# ssl/cert-manager.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: admin@yourdomain.com
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
    - http01:
        ingress:
          class: nginx

---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: vue-ssr-tls
  namespace: vue-ssr-app
spec:
  secretName: vue-ssr-tls
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
  - yourdomain.com
  - www.yourdomain.com

8.7.2 安全中间件

// middleware/security.js
const rateLimit = require('express-rate-limit')
const slowDown = require('express-slow-down')
const helmet = require('helmet')
const validator = require('validator')

// 速率限制
const createRateLimit = (windowMs, max, message) => {
  return rateLimit({
    windowMs,
    max,
    message: { error: message },
    standardHeaders: true,
    legacyHeaders: false,
    handler: (req, res) => {
      logger.warn('Rate limit exceeded', {
        ip: req.ip,
        userAgent: req.get('User-Agent'),
        url: req.url
      })
      res.status(429).json({ error: message })
    }
  })
}

// API限流
const apiLimiter = createRateLimit(
  15 * 60 * 1000, // 15分钟
  100, // 100个请求
  'Too many API requests, please try again later'
)

// 登录限流
const loginLimiter = createRateLimit(
  15 * 60 * 1000, // 15分钟
  5, // 5次尝试
  'Too many login attempts, please try again later'
)

// 慢速响应
const speedLimiter = slowDown({
  windowMs: 15 * 60 * 1000, // 15分钟
  delayAfter: 50, // 50个请求后开始延迟
  delayMs: 500 // 每个请求延迟500ms
})

// 输入验证中间件
function validateInput(req, res, next) {
  // 验证URL参数
  for (const [key, value] of Object.entries(req.params)) {
    if (typeof value === 'string') {
      // 检查XSS
      if (!validator.isLength(value, { max: 100 })) {
        return res.status(400).json({ error: 'Parameter too long' })
      }
      
      // 检查SQL注入
      if (value.includes(';') || value.includes('--') || value.includes('/*')) {
        logger.warn('Potential SQL injection attempt', {
          ip: req.ip,
          parameter: key,
          value: value
        })
        return res.status(400).json({ error: 'Invalid parameter' })
      }
    }
  }
  
  // 验证查询参数
  for (const [key, value] of Object.entries(req.query)) {
    if (typeof value === 'string') {
      if (!validator.isLength(value, { max: 200 })) {
        return res.status(400).json({ error: 'Query parameter too long' })
      }
    }
  }
  
  next()
}

// 安全头配置
const securityHeaders = helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
      scriptSrc: ["'self'", "https://www.google-analytics.com"],
      imgSrc: ["'self'", "data:", "https:"],
      fontSrc: ["'self'", "https://fonts.gstatic.com"],
      connectSrc: ["'self'", "https://api.yourdomain.com"],
      frameSrc: ["'none'"],
      objectSrc: ["'none'"],
      upgradeInsecureRequests: []
    }
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  },
  noSniff: true,
  xssFilter: true,
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
})

module.exports = {
  apiLimiter,
  loginLimiter,
  speedLimiter,
  validateInput,
  securityHeaders
}

8.8 本章小结

本章详细介绍了Vue SSR应用的部署与运维,包括:

核心要点

  1. 生产环境配置

    • 环境变量管理
    • 构建优化配置
    • 服务器性能调优
  2. 容器化部署

    • Docker多阶段构建
    • Docker Compose编排
    • Nginx反向代理配置
  3. Kubernetes部署

    • 完整的K8s配置文件
    • 自动扩缩容配置
    • 服务发现与负载均衡
  4. 监控与告警

    • Prometheus指标收集
    • Grafana可视化仪表板
    • 告警规则配置
  5. 日志管理

    • 结构化日志配置
    • ELK Stack集成
    • 日志分析与查询
  6. 备份与恢复

    • 数据库备份策略
    • 应用备份方案
    • 灾难恢复流程
  7. 安全配置

    • HTTPS证书管理
    • 安全中间件配置
    • 输入验证与防护

最佳实践

  1. 部署策略

    • 使用蓝绿部署或滚动更新
    • 实施健康检查和就绪探针
    • 配置资源限制和请求
  2. 监控策略

    • 建立完整的监控体系
    • 设置合理的告警阈值
    • 定期检查和优化性能
  3. 安全策略

    • 实施多层安全防护
    • 定期更新依赖和补丁
    • 进行安全审计和测试

练习作业

  1. 搭建完整的生产环境部署流程
  2. 配置监控告警系统
  3. 实施自动化备份策略
  4. 进行安全加固和测试

下一章我们将学习Vue SSR的高级特性与优化技巧。 this.app.use((req, res, next) => { const start = Date.now() res.on(‘finish’, () => { const duration = Date.now() - start console.log(${req.method} ${req.url} ${res.statusCode} ${duration}ms) }) next() }) }

setupRenderer() { const serverBundle = require(‘../dist/vue-ssr-server-bundle.json’) const clientManifest = require(‘../dist/vue-ssr-client-manifest.json’) const template = require(‘fs’).readFileSync(‘./src/index.template.html’, ‘utf-8’)

// 创建缓存
const microCache = new LRU({
  max: parseInt(process.env.COMPONENT_CACHE_MAX) || 1000,
  maxAge: parseInt(process.env.CACHE_TTL) * 1000 || 300000
})

const pageCache = new LRU({
  max: parseInt(process.env.PAGE_CACHE_MAX) || 5000,
  maxAge: parseInt(process.env.CACHE_TTL) * 1000 || 300000
})

this.renderer = createBundleRenderer(serverBundle, {
  runInNewContext: false,
  template,
  clientManifest,
  cache: microCache,
  shouldPrefetch: (file, type) => {
    if (type === 'script') {
      return !file.includes('chunk')
    }
    return true
  },
  shouldPreload: (file, type) => {
    if (type === 'script' || type === 'style') {
      return true
    }
    if (type === 'font') {
      return file.endsWith('.woff2')
    }
    return false
  }
})

this.pageCache = pageCache

}

setupRoutes() { // 健康检查 this.app.get(‘/health’, (req, res) => { res.json({ status: ‘ok’, timestamp: new Date().toISOString(), uptime: process.uptime(), memory: process.memoryUsage(), pid: process.pid }) })

// API路由
this.app.use('/api', require('./api'))

// SSR路由
this.app.get('*', this.handleSSR.bind(this))

}

async handleSSR(req, res) { const startTime = Date.now()

try {
  // 检查页面缓存
  const cacheKey = this.getCacheKey(req)
  const cached = this.pageCache.get(cacheKey)

  if (cached) {
    res.setHeader('X-Cache', 'HIT')
    return res.end(cached)
  }

  // 创建渲染上下文
  const context = {
    title: 'My App',
    url: req.url,
    userAgent: req.get('User-Agent'),
    cookies: req.headers.cookie,
    headers: req.headers
  }

  // 渲染页面
  const html = await this.renderer.renderToString(context)

  // 设置响应头
  res.setHeader('Content-Type', 'text/html')
  res.setHeader('X-Cache', 'MISS')

  // 缓存页面
  if (this.shouldCache(req)) {
    this.pageCache.set(cacheKey, html)
  }

  const renderTime = Date.now() - startTime
  res.setHeader('X-Render-Time', renderTime)

  res.end(html)

} catch (error) {
  this.handleRenderError(error, req, res)
}

}

getCacheKey(req) { const url = req.url const userAgent = req.get(‘User-Agent’) const acceptLanguage = req.get(‘Accept-Language’)

return `${url}:${this.hashString(userAgent)}:${this.hashString(acceptLanguage)}`

}

hashString(str) { if (!str) return “ let hash = 0 for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i) hash = ((hash << 5) - hash) + char hash = hash & hash // Convert to 32bit integer } return hash.toString() }

shouldCache(req) { // 不缓存包含查询参数的请求 if (Object.keys(req.query).length > 0) { return false }

// 不缓存POST请求
if (req.method !== 'GET') {
  return false
}

// 不缓存包含认证信息的请求
if (req.headers.authorization || req.headers.cookie) {
  return false
}

return true

}

handleRenderError(error, req, res) { console.error(‘SSR渲染错误:’, error)

// 发送错误到监控服务
if (process.env.SENTRY_DSN) {
  // Sentry.captureException(error)
}

// 返回错误页面
if (error.code === 404) {
  res.status(404).send('页面未找到')
} else {
  res.status(500).send('服务器内部错误')
}

}

setupErrorHandling() { // 404处理 this.app.use((req, res) => { res.status(404).json({ error: ‘Not Found’ }) })

// 错误处理
this.app.use((error, req, res, next) => {
  console.error('服务器错误:', error)

  if (res.headersSent) {
    return next(error)
  }

  res.status(500).json({
    error: process.env.NODE_ENV === 'production' 
      ? 'Internal Server Error' 
      : error.message
  })
})

// 进程错误处理
process.on('uncaughtException', (error) => {
  console.error('未捕获的异常:', error)
  process.exit(1)
})

process.on('unhandledRejection', (reason, promise) => {
  console.error('未处理的Promise拒绝:', reason)
  process.exit(1)
})

}

start() { const port = process.env.PORT || 3000 const host = process.env.HOST || ‘0.0.0.0’

this.app.listen(port, host, () => {
  console.log(`服务器运行在 http://${host}:${port}`)
})

} }

// 集群模式启动 if (cluster.isMaster && process.env.NODE_ENV === ‘production’) { const workers = parseInt(process.env.CLUSTER_WORKERS) || numCPUs

console.log(主进程 ${process.pid} 正在运行) console.log(启动 ${workers} 个工作进程)

for (let i = 0; i < workers; i++) { cluster.fork() }

cluster.on(‘exit’, (worker, code, signal) => { console.log(工作进程 ${worker.process.pid} 已退出) if (code !== 0 && !worker.exitedAfterDisconnect) { console.log(‘启动新的工作进程…’) cluster.fork() } }) } else { const server = new ProductionServer() server.start() }


## 8.2 容器化部署

### 8.2.1 Docker配置

```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 nextjs -u 1001

# 设置工作目录
WORKDIR /app

# 安装dumb-init
RUN apk add --no-cache dumb-init

# 复制构建产物
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/package*.json ./
COPY --from=builder --chown=nextjs:nodejs /app/server ./server

# 创建日志目录
RUN mkdir -p /var/log/myapp && chown nextjs:nodejs /var/log/myapp

# 切换到非root用户
USER nextjs

# 暴露端口
EXPOSE 3000

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

# 启动应用
CMD ["dumb-init", "node", "server/production.js"]
// healthcheck.js
const http = require('http')

const options = {
  hostname: 'localhost',
  port: process.env.PORT || 3000,
  path: '/health',
  method: 'GET',
  timeout: 2000
}

const req = http.request(options, (res) => {
  if (res.statusCode === 200) {
    process.exit(0)
  } else {
    process.exit(1)
  }
})

req.on('error', () => {
  process.exit(1)
})

req.on('timeout', () => {
  req.destroy()
  process.exit(1)
})

req.end()

8.2.2 Docker Compose配置

# docker-compose.yml
version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
      target: production
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DB_HOST=postgres
      - REDIS_HOST=redis
    depends_on:
      - postgres
      - redis
    volumes:
      - ./logs:/var/log/myapp
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '0.5'
        reservations:
          memory: 256M
          cpus: '0.25'
    healthcheck:
      test: ["CMD", "node", "healthcheck.js"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  postgres:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=myapp_prod
      - POSTGRES_USER=myapp_user
      - POSTGRES_PASSWORD=secure_password
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 256M
          cpus: '0.25'

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes --requirepass redis_password
    volumes:
      - redis_data:/data
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 128M
          cpus: '0.1'

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

volumes:
  postgres_data:
  redis_data:

networks:
  default:
    driver: bridge

8.2.3 Nginx配置

# nginx.conf
events {
    worker_connections 1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    
    # 日志格式
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for" '
                    '$request_time $upstream_response_time';
    
    access_log /var/log/nginx/access.log main;
    error_log /var/log/nginx/error.log warn;
    
    # 基本设置
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    client_max_body_size 10M;
    
    # Gzip压缩
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/json
        application/javascript
        application/xml+rss
        application/atom+xml
        image/svg+xml;
    
    # 上游服务器
    upstream app_servers {
        least_conn;
        server app:3000 max_fails=3 fail_timeout=30s;
        keepalive 32;
    }
    
    # 限流配置
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=general:10m rate=1r/s;
    
    # HTTP重定向到HTTPS
    server {
        listen 80;
        server_name yourdomain.com www.yourdomain.com;
        return 301 https://$server_name$request_uri;
    }
    
    # HTTPS服务器
    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_session_timeout 1d;
        ssl_session_cache shared:SSL:50m;
        ssl_session_tickets off;
        
        # 现代SSL配置
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
        ssl_prefer_server_ciphers off;
        
        # HSTS
        add_header Strict-Transport-Security "max-age=63072000" always;
        
        # 安全头
        add_header X-Frame-Options DENY;
        add_header X-Content-Type-Options nosniff;
        add_header X-XSS-Protection "1; mode=block";
        add_header Referrer-Policy "strict-origin-when-cross-origin";
        
        # 静态文件缓存
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
            expires 1y;
            add_header Cache-Control "public, immutable";
            add_header X-Cache-Status "STATIC";
            
            # 尝试本地文件,否则代理到应用
            try_files $uri @app;
        }
        
        # API路由限流
        location /api/ {
            limit_req zone=api burst=20 nodelay;
            limit_req_status 429;
            
            proxy_pass http://app_servers;
            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_connect_timeout 5s;
            proxy_send_timeout 60s;
            proxy_read_timeout 60s;
        }
        
        # 健康检查
        location /health {
            access_log off;
            proxy_pass http://app_servers;
            proxy_http_version 1.1;
            proxy_set_header Connection '';
        }
        
        # 主应用
        location / {
            limit_req zone=general burst=10 nodelay;
            
            proxy_pass http://app_servers;
            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_connect_timeout 5s;
            proxy_send_timeout 60s;
            proxy_read_timeout 60s;
            
            # 错误页面
            error_page 502 503 504 /50x.html;
        }
        
        # 错误页面
        location = /50x.html {
            root /usr/share/nginx/html;
        }
        
        # 代理到应用的fallback
        location @app {
            proxy_pass http://app_servers;
            proxy_http_version 1.1;
            proxy_set_header Connection '';
            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;
        }
    }
}

8.3 Kubernetes部署

8.3.1 Kubernetes配置文件

# k8s/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: vue-ssr-app
  labels:
    name: vue-ssr-app

---
# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: vue-ssr-app
data:
  NODE_ENV: "production"
  PORT: "3000"
  HOST: "0.0.0.0"
  CACHE_TTL: "300"
  COMPONENT_CACHE_MAX: "1000"
  PAGE_CACHE_MAX: "5000"
  LOG_LEVEL: "info"
  CLUSTER_WORKERS: "1"

---
# k8s/secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
  namespace: vue-ssr-app
type: Opaque
data:
  DB_PASSWORD: c2VjdXJlX3Bhc3N3b3Jk  # base64 encoded
  REDIS_PASSWORD: cmVkaXNfcGFzc3dvcmQ=  # base64 encoded
  JWT_SECRET: eW91cl9qd3Rfc2VjcmV0X2tleQ==  # base64 encoded
  SESSION_SECRET: eW91cl9zZXNzaW9uX3NlY3JldA==  # base64 encoded

---
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vue-ssr-app
  namespace: vue-ssr-app
  labels:
    app: vue-ssr-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: vue-ssr-app
  template:
    metadata:
      labels:
        app: vue-ssr-app
    spec:
      containers:
      - name: vue-ssr-app
        image: your-registry/vue-ssr-app:latest
        ports:
        - containerPort: 3000
        envFrom:
        - configMapRef:
            name: app-config
        env:
        - name: DB_HOST
          value: "postgres-service"
        - name: REDIS_HOST
          value: "redis-service"
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: DB_PASSWORD
        - name: REDIS_PASSWORD
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: REDIS_PASSWORD
        - name: JWT_SECRET
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: JWT_SECRET
        - name: SESSION_SECRET
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: SESSION_SECRET
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 30
          periodSeconds: 10
          timeoutSeconds: 5
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 5
          timeoutSeconds: 3
          failureThreshold: 3
        volumeMounts:
        - name: logs
          mountPath: /var/log/myapp
      volumes:
      - name: logs
        emptyDir: {}
      imagePullSecrets:
      - name: registry-secret

---
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: vue-ssr-service
  namespace: vue-ssr-app
  labels:
    app: vue-ssr-app
spec:
  selector:
    app: vue-ssr-app
  ports:
  - protocol: TCP
    port: 80
    targetPort: 3000
  type: ClusterIP

---
# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: vue-ssr-hpa
  namespace: vue-ssr-app
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: vue-ssr-app
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
      - type: Percent
        value: 10
        periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
      - type: Percent
        value: 50
        periodSeconds: 60

---
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: vue-ssr-ingress
  namespace: vue-ssr-app
  annotations:
    kubernetes.io/ingress.class: "nginx"
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
    nginx.ingress.kubernetes.io/rate-limit: "100"
    nginx.ingress.kubernetes.io/rate-limit-window: "1m"
spec:
  tls:
  - hosts:
    - yourdomain.com
    - www.yourdomain.com
    secretName: vue-ssr-tls
  rules:
  - host: yourdomain.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: vue-ssr-service
            port:
              number: 80
  - host: www.yourdomain.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: vue-ssr-service
            port:
              number: 80

8.3.2 数据库部署

# k8s/postgres.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
  namespace: vue-ssr-app
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
  storageClassName: fast-ssd

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
  namespace: vue-ssr-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:15-alpine
        env:
        - name: POSTGRES_DB
          value: "myapp_prod"
        - name: POSTGRES_USER
          value: "myapp_user"
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: DB_PASSWORD
        - name: PGDATA
          value: "/var/lib/postgresql/data/pgdata"
        ports:
        - containerPort: 5432
        volumeMounts:
        - name: postgres-storage
          mountPath: /var/lib/postgresql/data
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          exec:
            command:
            - pg_isready
            - -U
            - myapp_user
            - -d
            - myapp_prod
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          exec:
            command:
            - pg_isready
            - -U
            - myapp_user
            - -d
            - myapp_prod
          initialDelaySeconds: 5
          periodSeconds: 5
      volumes:
      - name: postgres-storage
        persistentVolumeClaim:
          claimName: postgres-pvc

---
apiVersion: v1
kind: Service
metadata:
  name: postgres-service
  namespace: vue-ssr-app
spec:
  selector:
    app: postgres
  ports:
  - protocol: TCP
    port: 5432
    targetPort: 5432
  type: ClusterIP

---
# k8s/redis.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
  namespace: vue-ssr-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
      - name: redis
        image: redis:7-alpine
        command:
        - redis-server
        - --appendonly
        - "yes"
        - --requirepass
        - $(REDIS_PASSWORD)
        env:
        - name: REDIS_PASSWORD
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: REDIS_PASSWORD
        ports:
        - containerPort: 6379
        resources:
          requests:
            memory: "64Mi"
            cpu: "100m"
          limits:
            memory: "128Mi"
            cpu: "200m"
        livenessProbe:
          exec:
            command:
            - redis-cli
            - ping
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          exec:
            command:
            - redis-cli
            - ping
          initialDelaySeconds: 5
          periodSeconds: 5

---
apiVersion: v1
kind: Service
metadata:
  name: redis-service
  namespace: vue-ssr-app
spec:
  selector:
    app: redis
  ports:
  - protocol: TCP
    port: 6379
    targetPort: 6379
  type: ClusterIP

8.3.3 部署脚本

#!/bin/bash
# deploy.sh

set -e

# 配置变量
REGISTRY="your-registry.com"
IMAGE_NAME="vue-ssr-app"
TAG=${1:-latest}
NAMESPACE="vue-ssr-app"

echo "开始部署 Vue SSR 应用..."

# 构建Docker镜像
echo "构建Docker镜像..."
docker build -t $REGISTRY/$IMAGE_NAME:$TAG .

# 推送镜像到仓库
echo "推送镜像到仓库..."
docker push $REGISTRY/$IMAGE_NAME:$TAG

# 创建命名空间(如果不存在)
kubectl create namespace $NAMESPACE --dry-run=client -o yaml | kubectl apply -f -

# 应用Kubernetes配置
echo "应用Kubernetes配置..."
kubectl apply -f k8s/

# 更新部署镜像
echo "更新部署镜像..."
kubectl set image deployment/vue-ssr-app vue-ssr-app=$REGISTRY/$IMAGE_NAME:$TAG -n $NAMESPACE

# 等待部署完成
echo "等待部署完成..."
kubectl rollout status deployment/vue-ssr-app -n $NAMESPACE --timeout=300s

# 检查部署状态
echo "检查部署状态..."
kubectl get pods -n $NAMESPACE
kubectl get services -n $NAMESPACE
kubectl get ingress -n $NAMESPACE

echo "部署完成!"

# 显示应用URL
INGRESS_IP=$(kubectl get ingress vue-ssr-ingress -n $NAMESPACE -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
if [ ! -z "$INGRESS_IP" ]; then
    echo "应用访问地址: https://$INGRESS_IP"
else
    echo "请检查Ingress配置获取访问地址"
fi

8.4 监控与告警

8.4.1 Prometheus监控配置

# monitoring/prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

rule_files:
  - "alert_rules.yml"

alerting:
  alertmanagers:
    - static_configs:
        - targets:
          - alertmanager:9093

scrape_configs:
  - job_name: 'vue-ssr-app'
    static_configs:
      - targets: ['vue-ssr-service:80']
    metrics_path: '/metrics'
    scrape_interval: 30s
    scrape_timeout: 10s

  - job_name: 'node-exporter'
    static_configs:
      - targets: ['node-exporter:9100']

  - job_name: 'postgres-exporter'
    static_configs:
      - targets: ['postgres-exporter:9187']

  - job_name: 'redis-exporter'
    static_configs:
      - targets: ['redis-exporter:9121']
# monitoring/alert_rules.yml
groups:
- name: vue-ssr-app
  rules:
  - alert: HighErrorRate
    expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.1
    for: 5m
    labels:
      severity: critical
    annotations:
      summary: "High error rate detected"
      description: "Error rate is {{ $value }} errors per second"

  - alert: HighResponseTime
    expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 1
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "High response time detected"
      description: "95th percentile response time is {{ $value }} seconds"

  - alert: HighMemoryUsage
    expr: (node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes > 0.8
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "High memory usage detected"
      description: "Memory usage is {{ $value | humanizePercentage }}"

  - alert: HighCPUUsage
    expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "High CPU usage detected"
      description: "CPU usage is {{ $value }}%"

  - alert: DatabaseDown
    expr: up{job="postgres-exporter"} == 0
    for: 1m
    labels:
      severity: critical
    annotations:
      summary: "Database is down"
      description: "PostgreSQL database is not responding"

  - alert: RedisDown
    expr: up{job="redis-exporter"} == 0
    for: 1m
    labels:
      severity: critical
    annotations:
      summary: "Redis is down"
      description: "Redis cache is not responding"

  - alert: PodCrashLooping
    expr: rate(kube_pod_container_status_restarts_total[15m]) > 0
    for: 5m
    labels:
      severity: critical
    annotations:
      summary: "Pod is crash looping"
      description: "Pod {{ $labels.pod }} in namespace {{ $labels.namespace }} is crash looping"

8.4.2 应用指标收集

// server/metrics.js
const promClient = require('prom-client')

// 创建指标注册表
const register = new promClient.Registry()

// 添加默认指标
promClient.collectDefaultMetrics({ register })

// 自定义指标
const httpRequestsTotal = new promClient.Counter({
  name: 'http_requests_total',
  help: 'Total number of HTTP requests',
  labelNames: ['method', 'route', 'status'],
  registers: [register]
})

const httpRequestDuration = new promClient.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status'],
  buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10],
  registers: [register]
})

const ssrRenderDuration = new promClient.Histogram({
  name: 'ssr_render_duration_seconds',
  help: 'Duration of SSR rendering in seconds',
  labelNames: ['route'],
  buckets: [0.1, 0.2, 0.5, 1, 2, 5],
  registers: [register]
})

const cacheHitRate = new promClient.Gauge({
  name: 'cache_hit_rate',
  help: 'Cache hit rate',
  labelNames: ['cache_type'],
  registers: [register]
})

const activeConnections = new promClient.Gauge({
  name: 'active_connections',
  help: 'Number of active connections',
  registers: [register]
})

const memoryUsage = new promClient.Gauge({
  name: 'nodejs_memory_usage_bytes',
  help: 'Node.js memory usage in bytes',
  labelNames: ['type'],
  registers: [register]
})

// 指标收集中间件
function metricsMiddleware(req, res, next) {
  const start = Date.now()
  
  res.on('finish', () => {
    const duration = (Date.now() - start) / 1000
    const route = req.route ? req.route.path : req.path
    const method = req.method
    const status = res.statusCode.toString()
    
    httpRequestsTotal.inc({ method, route, status })
    httpRequestDuration.observe({ method, route, status }, duration)
  })
  
  next()
}

// SSR渲染指标
function recordSSRRender(route, duration) {
  ssrRenderDuration.observe({ route }, duration / 1000)
}

// 缓存指标
function updateCacheMetrics(cacheType, hits, total) {
  const hitRate = total > 0 ? hits / total : 0
  cacheHitRate.set({ cache_type: cacheType }, hitRate)
}

// 内存使用指标
function updateMemoryMetrics() {
  const memUsage = process.memoryUsage()
  memoryUsage.set({ type: 'heap_used' }, memUsage.heapUsed)
  memoryUsage.set({ type: 'heap_total' }, memUsage.heapTotal)
  memoryUsage.set({ type: 'external' }, memUsage.external)
  memoryUsage.set({ type: 'rss' }, memUsage.rss)
}

// 定期更新内存指标
setInterval(updateMemoryMetrics, 10000)

// 指标端点
function metricsEndpoint(req, res) {
  res.set('Content-Type', register.contentType)
  res.end(register.metrics())
}

module.exports = {
  register,
  metricsMiddleware,
  metricsEndpoint,
  recordSSRRender,
  updateCacheMetrics,
  activeConnections
}

8.4.3 Grafana仪表板

{
  "dashboard": {
    "id": null,
    "title": "Vue SSR Application Dashboard",
    "tags": ["vue", "ssr", "nodejs"],
    "timezone": "browser",
    "panels": [
      {
        "id": 1,
        "title": "Request Rate",
        "type": "graph",
        "targets": [
          {
            "expr": "rate(http_requests_total[5m])",
            "legendFormat": "{{method}} {{route}}"
          }
        ],
        "yAxes": [
          {
            "label": "Requests/sec"
          }
        ],
        "gridPos": {
          "h": 8,
          "w": 12,
          "x": 0,
          "y": 0
        }
      },
      {
        "id": 2,
        "title": "Response Time",
        "type": "graph",
        "targets": [
          {
            "expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))",
            "legendFormat": "95th percentile"
          },
          {
            "expr": "histogram_quantile(0.50, rate(http_request_duration_seconds_bucket[5m]))",
            "legendFormat": "50th percentile"
          }
        ],
        "yAxes": [
          {
            "label": "Seconds"
          }
        ],
        "gridPos": {
          "h": 8,
          "w": 12,
          "x": 12,
          "y": 0
        }
      },
      {
        "id": 3,
        "title": "Error Rate",
        "type": "singlestat",
        "targets": [
          {
            "expr": "rate(http_requests_total{status=~\"5..\"}[5m]) / rate(http_requests_total[5m])",
            "legendFormat": "Error Rate"
          }
        ],
        "valueName": "current",
        "format": "percentunit",
        "thresholds": "0.01,0.05",
        "colorBackground": true,
        "gridPos": {
          "h": 4,
          "w": 6,
          "x": 0,
          "y": 8
        }
      },
      {
        "id": 4,
        "title": "Cache Hit Rate",
        "type": "singlestat",
        "targets": [
          {
            "expr": "cache_hit_rate",
            "legendFormat": "{{cache_type}}"
          }
        ],
        "valueName": "current",
        "format": "percentunit",
        "thresholds": "0.7,0.9",
        "colorBackground": true,
        "gridPos": {
          "h": 4,
          "w": 6,
          "x": 6,
          "y": 8
        }
      },
      {
        "id": 5,
        "title": "Memory Usage",
        "type": "graph",
        "targets": [
          {
            "expr": "nodejs_memory_usage_bytes{type=\"heap_used\"}",
            "legendFormat": "Heap Used"
          },
          {
            "expr": "nodejs_memory_usage_bytes{type=\"heap_total\"}",
            "legendFormat": "Heap Total"
          }
        ],
        "yAxes": [
          {
            "label": "Bytes",
            "logBase": 1
          }
        ],
        "gridPos": {
          "h": 8,
          "w": 12,
          "x": 0,
          "y": 12
        }
      },
      {
        "id": 6,
        "title": "SSR Render Time",
        "type": "graph",
        "targets": [
          {
            "expr": "histogram_quantile(0.95, rate(ssr_render_duration_seconds_bucket[5m]))",
            "legendFormat": "95th percentile"
          },
          {
            "expr": "histogram_quantile(0.50, rate(ssr_render_duration_seconds_bucket[5m]))",
            "legendFormat": "50th percentile"
          }
        ],
        "yAxes": [
          {
            "label": "Seconds"
          }
        ],
        "gridPos": {
          "h": 8,
          "w": 12,
          "x": 12,
          "y": 12
        }
      }
    ],
    "time": {
      "from": "now-1h",
      "to": "now"
    },
    "refresh": "30s"
  }
}

8.5 日志管理

8.5.1 结构化日志配置

”`javascript // utils/logger.js const winston = require(‘winston’) const path = require(‘path’)

// 自定义日志格式 const logFormat = winston.format.combine( winston.format.timestamp({ format: ‘YYYY-MM-DD HH:mm:ss’ }), winston.format.errors({ stack: true }), winston.format.json(), winston.format.printf(({ timestamp, level, message, …meta }) => { return JSON.stringify({ timestamp, level, message, …meta, service: ‘vue-ssr-app’, version: process.env.APP_VERSION || ‘1.0.0’, environment: process.env.NODE_ENV || ‘development’, pid: process.pid, hostname: require(‘os’).hostname() }) }) )

// 创建logger实例 const logger = winston.createLogger({ level: process.env.LOG_LEVEL || ‘info’, format: logFormat, defaultMeta: { service: ‘vue-ssr-app’ }, transports: [ // 控制台输出 new winston.transports.Console({ format: winston.format.combine( winston.format.colorize(), winston.format.simple() ) }),

// 文件输出
new winston.transports.File({
  filename: path.join(process.env.LOG_DIR || './logs', 'error.log'),
  level: 'error',
  maxsize: 10485760, // 10MB
  maxFiles: 5
}),

new winston.transports.File({
  filename: path.join(process.env.LOG_DIR || './logs', 'combined.log'),
  maxsize: 10485760, // 10MB
  maxFiles: 10
})

],

// 异常处理 exceptionHandlers: [ new winston.transports.File({ filename: path.join(process.env.LOG_DIR || ‘./logs’, ‘exceptions.log’) }) ],

// 拒绝处理 rejectionHandlers: [ new winston.transports.File({ filename: path.join(process.env.LOG_DIR || ‘./logs’, ‘rejections.log’) }) ] })

// 请求日志