iT邦幫忙

2025 iThome 鐵人賽

DAY 8
0
Software Development

30 天打造工作室 SaaS 產品 (後端篇)系列 第 8

Day 8: 30天打造SaaS產品後端篇-效能監控與最佳化

  • 分享至 

  • xImage
  •  

前情提要

昨天我們建立了完整的測試策略和品質保證架構,確保我們的程式品質和穩定性。今天我們要來看另一個關鍵問題:應用效能監控與最佳化

在現代軟體開發中,效能不僅影響使用者體驗,也直接關係到系統的擴展性和運營成本:

  • 即時效能監控:快速發現效能瓶頸
  • 📊 數據驅動優化:基於實際數據進行優化決策
  • 🎯 主動式維護:在問題影響使用者前解決
  • 💰 成本控制:優化資源使用,降低運營成本

效能監控架構

我們的效能監控採用多層次策略:

📱 Frontend (Browser)
    ↓ Real User Monitoring
🖥️ Application Layer
    ↓ APM & Custom Metrics
🐳 Container Layer
    ↓ Resource Monitoring
☁️ Infrastructure Layer
    ↓ System Metrics

實作步驟

1. 應用程式效能監控(APM)

首先整合 APM 解決方案:

// packages/kyo-core/src/monitoring/performance.ts
import { performance } from 'perf_hooks';
import { EventEmitter } from 'events';

interface PerformanceMetric {
  name: string;
  value: number;
  unit: string;
  timestamp: Date;
  labels?: Record<string, string>;
}

interface TimingResult {
  duration: number;
  timestamp: Date;
}

export class PerformanceMonitor extends EventEmitter {
  private metrics: Map<string, PerformanceMetric[]> = new Map();
  private activeTimers: Map<string, number> = new Map();

  // 記錄計時器
  startTimer(name: string, labels?: Record<string, string>): string {
    const timerId = `${name}_${Date.now()}_${Math.random()}`;
    this.activeTimers.set(timerId, performance.now());
    return timerId;
  }

  endTimer(timerId: string, labels?: Record<string, string>): TimingResult {
    const startTime = this.activeTimers.get(timerId);
    if (!startTime) {
      throw new Error(`Timer ${timerId} not found`);
    }

    const endTime = performance.now();
    const duration = endTime - startTime;
    const timestamp = new Date();

    this.activeTimers.delete(timerId);

    // 提取計時器名稱
    const name = timerId.split('_')[0];

    // 記錄指標
    this.recordMetric({
      name: `${name}_duration`,
      value: duration,
      unit: 'ms',
      timestamp,
      labels,
    });

    const result = { duration, timestamp };
    this.emit('timing', { name, ...result, labels });

    return result;
  }

  // 直接記錄指標
  recordMetric(metric: PerformanceMetric): void {
    if (!this.metrics.has(metric.name)) {
      this.metrics.set(metric.name, []);
    }

    const metrics = this.metrics.get(metric.name)!;
    metrics.push(metric);

    // 保持最近 1000 個記錄
    if (metrics.length > 1000) {
      metrics.shift();
    }

    this.emit('metric', metric);
  }

  // 計數器
  increment(name: string, value: number = 1, labels?: Record<string, string>): void {
    this.recordMetric({
      name,
      value,
      unit: 'count',
      timestamp: new Date(),
      labels,
    });
  }

  // 儀表板
  gauge(name: string, value: number, unit: string = 'unit', labels?: Record<string, string>): void {
    this.recordMetric({
      name,
      value,
      unit,
      timestamp: new Date(),
      labels,
    });
  }

  // 獲取指標統計
  getMetricStats(name: string, timeWindow: number = 60000): {
    count: number;
    min: number;
    max: number;
    avg: number;
    p95: number;
    p99: number;
  } | null {
    const metrics = this.metrics.get(name);
    if (!metrics || metrics.length === 0) return null;

    const now = Date.now();
    const recentMetrics = metrics.filter(
      m => now - m.timestamp.getTime() <= timeWindow
    );

    if (recentMetrics.length === 0) return null;

    const values = recentMetrics.map(m => m.value).sort((a, b) => a - b);
    const count = values.length;
    const min = values[0];
    const max = values[count - 1];
    const avg = values.reduce((sum, val) => sum + val, 0) / count;
    const p95 = values[Math.floor(count * 0.95)] || max;
    const p99 = values[Math.floor(count * 0.99)] || max;

    return { count, min, max, avg, p95, p99 };
  }

  // 獲取所有指標名稱
  getMetricNames(): string[] {
    return Array.from(this.metrics.keys());
  }

  // 清理舊數據
  cleanup(maxAge: number = 3600000): void { // 1 小時
    const cutoff = Date.now() - maxAge;

    for (const [name, metrics] of this.metrics.entries()) {
      const filtered = metrics.filter(m => m.timestamp.getTime() > cutoff);
      if (filtered.length === 0) {
        this.metrics.delete(name);
      } else {
        this.metrics.set(name, filtered);
      }
    }
  }
}

// 全域效能監控實例
export const performanceMonitor = new PerformanceMonitor();

// 自動清理
setInterval(() => {
  performanceMonitor.cleanup();
}, 300000); // 每 5 分鐘清理一次

2. Fastify 整合中間件

// apps/kyo-otp-service/src/plugins/monitoring.ts
import fp from 'fastify-plugin';
import { performanceMonitor } from '@kyong/kyo-core';

declare module 'fastify' {
  interface FastifyRequest {
    timer?: string;
  }
}

export default fp(async function (fastify) {
  // 請求計時中間件
  fastify.addHook('onRequest', async (request, reply) => {
    const labels = {
      method: request.method,
      route: request.routerPath || 'unknown',
    };

    request.timer = performanceMonitor.startTimer('http_request', labels);

    // 記錄請求數
    performanceMonitor.increment('http_requests_total', 1, labels);
  });

  fastify.addHook('onResponse', async (request, reply) => {
    if (request.timer) {
      const labels = {
        method: request.method,
        route: request.routerPath || 'unknown',
        status_code: reply.statusCode.toString(),
      };

      performanceMonitor.endTimer(request.timer, labels);

      // 記錄回應狀態
      performanceMonitor.increment('http_responses_total', 1, labels);

      // 記錄錯誤率
      if (reply.statusCode >= 400) {
        performanceMonitor.increment('http_errors_total', 1, labels);
      }
    }
  });

  // 暴露指標端點
  fastify.get('/metrics', async (request, reply) => {
    const metrics = performanceMonitor.getMetricNames().map(name => {
      const stats = performanceMonitor.getMetricStats(name);
      return { name, stats };
    }).filter(m => m.stats !== null);

    return {
      timestamp: new Date().toISOString(),
      metrics,
      uptime: process.uptime(),
      memoryUsage: process.memoryUsage(),
      cpuUsage: process.cpuUsage(),
    };
  });
});

3. 資料庫查詢效能監控

// packages/kyo-core/src/monitoring/database.ts
import { performanceMonitor } from './performance.js';

export interface DatabaseMetrics {
  queryCount: number;
  slowQueries: number;
  connectionPoolSize: number;
  connectionPoolUsed: number;
}

export class DatabaseMonitor {
  private static instance: DatabaseMonitor;
  private slowQueryThreshold: number = 1000; // 1 秒

  static getInstance(): DatabaseMonitor {
    if (!DatabaseMonitor.instance) {
      DatabaseMonitor.instance = new DatabaseMonitor();
    }
    return DatabaseMonitor.instance;
  }

  // 包裝資料庫查詢
  async wrapQuery<T>(
    queryName: string,
    queryFn: () => Promise<T>,
    params?: any[]
  ): Promise<T> {
    const labels = { query: queryName };
    const timer = performanceMonitor.startTimer('db_query', labels);

    try {
      const result = await queryFn();
      const timing = performanceMonitor.endTimer(timer, labels);

      // 記錄慢查詢
      if (timing.duration > this.slowQueryThreshold) {
        performanceMonitor.increment('db_slow_queries_total', 1, {
          ...labels,
          duration: timing.duration.toString(),
        });

        console.warn(`Slow query detected: ${queryName} took ${timing.duration.toFixed(2)}ms`, {
          params: params?.slice(0, 3), // 只記錄前 3 個參數
        });
      }

      performanceMonitor.increment('db_queries_total', 1, {
        ...labels,
        status: 'success',
      });

      return result;
    } catch (error) {
      performanceMonitor.endTimer(timer, { ...labels, status: 'error' });
      performanceMonitor.increment('db_queries_total', 1, {
        ...labels,
        status: 'error',
      });

      throw error;
    }
  }

  // 監控連接池
  recordConnectionPoolMetrics(metrics: DatabaseMetrics): void {
    performanceMonitor.gauge('db_connection_pool_size', metrics.connectionPoolSize, 'connections');
    performanceMonitor.gauge('db_connection_pool_used', metrics.connectionPoolUsed, 'connections');
    performanceMonitor.gauge('db_connection_pool_utilization',
      metrics.connectionPoolUsed / metrics.connectionPoolSize * 100, 'percent');
  }

  setSlowQueryThreshold(threshold: number): void {
    this.slowQueryThreshold = threshold;
  }
}

export const dbMonitor = DatabaseMonitor.getInstance();

4. Redis 效能監控

// packages/kyo-core/src/monitoring/redis.ts
import { performanceMonitor } from './performance.js';
import { Redis } from 'ioredis';

export class RedisMonitor {
  constructor(private redis: Redis) {
    this.setupMonitoring();
  }

  private setupMonitoring(): void {
    // 監控 Redis 命令
    this.redis.on('monitor', (time, args, source, database) => {
      const command = args[0]?.toLowerCase();
      if (command) {
        performanceMonitor.increment('redis_commands_total', 1, {
          command,
          database: database.toString(),
        });
      }
    });

    // 監控連接狀態
    this.redis.on('connect', () => {
      performanceMonitor.increment('redis_connections_total', 1, {
        event: 'connect',
      });
    });

    this.redis.on('error', (error) => {
      performanceMonitor.increment('redis_errors_total', 1, {
        error: error.name,
      });
    });

    // 定期收集 Redis 資訊
    setInterval(() => {
      this.collectRedisInfo();
    }, 30000); // 每 30 秒
  }

  private async collectRedisInfo(): Promise<void> {
    try {
      const info = await this.redis.info();
      const lines = info.split('\r\n');
      const metrics: Record<string, number> = {};

      for (const line of lines) {
        if (line.includes(':') && !line.startsWith('#')) {
          const [key, value] = line.split(':');
          const numValue = parseFloat(value);
          if (!isNaN(numValue)) {
            metrics[key] = numValue;
          }
        }
      }

      // 記錄關鍵指標
      if (metrics.used_memory) {
        performanceMonitor.gauge('redis_memory_used_bytes', metrics.used_memory, 'bytes');
      }

      if (metrics.connected_clients) {
        performanceMonitor.gauge('redis_connected_clients', metrics.connected_clients, 'count');
      }

      if (metrics.total_commands_processed) {
        performanceMonitor.gauge('redis_commands_processed_total', metrics.total_commands_processed, 'count');
      }

      if (metrics.keyspace_hits && metrics.keyspace_misses) {
        const hitRate = metrics.keyspace_hits / (metrics.keyspace_hits + metrics.keyspace_misses) * 100;
        performanceMonitor.gauge('redis_hit_rate', hitRate, 'percent');
      }

    } catch (error) {
      console.error('Failed to collect Redis info:', error);
    }
  }

  // 包裝 Redis 命令以進行監控
  async wrapCommand<T>(
    commandName: string,
    commandFn: () => Promise<T>
  ): Promise<T> {
    const labels = { command: commandName.toLowerCase() };
    const timer = performanceMonitor.startTimer('redis_command', labels);

    try {
      const result = await commandFn();
      performanceMonitor.endTimer(timer, { ...labels, status: 'success' });
      return result;
    } catch (error) {
      performanceMonitor.endTimer(timer, { ...labels, status: 'error' });
      performanceMonitor.increment('redis_command_errors_total', 1, labels);
      throw error;
    }
  }
}

5. 記憶體和 CPU 監控

// packages/kyo-core/src/monitoring/system.ts
import { performanceMonitor } from './performance.js';
import { promisify } from 'util';
import { exec } from 'child_process';

const execAsync = promisify(exec);

export class SystemMonitor {
  private interval: NodeJS.Timeout | null = null;

  start(intervalMs: number = 10000): void { // 每 10 秒
    if (this.interval) {
      return; // 已經在運行
    }

    this.interval = setInterval(() => {
      this.collectSystemMetrics();
    }, intervalMs);

    // 立即收集一次
    this.collectSystemMetrics();
  }

  stop(): void {
    if (this.interval) {
      clearInterval(this.interval);
      this.interval = null;
    }
  }

  private async collectSystemMetrics(): Promise<void> {
    try {
      // Node.js 內建指標
      const memUsage = process.memoryUsage();
      const cpuUsage = process.cpuUsage();

      // 記憶體指標
      performanceMonitor.gauge('nodejs_memory_rss_bytes', memUsage.rss, 'bytes');
      performanceMonitor.gauge('nodejs_memory_heap_used_bytes', memUsage.heapUsed, 'bytes');
      performanceMonitor.gauge('nodejs_memory_heap_total_bytes', memUsage.heapTotal, 'bytes');
      performanceMonitor.gauge('nodejs_memory_external_bytes', memUsage.external, 'bytes');

      // CPU 指標(微秒)
      performanceMonitor.gauge('nodejs_cpu_user_microseconds', cpuUsage.user, 'microseconds');
      performanceMonitor.gauge('nodejs_cpu_system_microseconds', cpuUsage.system, 'microseconds');

      // 事件循環延遲
      const eventLoopDelay = await this.measureEventLoopDelay();
      performanceMonitor.gauge('nodejs_eventloop_delay_ms', eventLoopDelay, 'ms');

      // 作業系統指標(如果可用)
      await this.collectOSMetrics();

    } catch (error) {
      console.error('Failed to collect system metrics:', error);
    }
  }

  private async measureEventLoopDelay(): Promise<number> {
    return new Promise((resolve) => {
      const start = process.hrtime.bigint();
      setImmediate(() => {
        const delay = Number(process.hrtime.bigint() - start) / 1000000; // 轉換為毫秒
        resolve(delay);
      });
    });
  }

  private async collectOSMetrics(): Promise<void> {
    try {
      // 獲取系統負載(Linux/macOS)
      if (process.platform !== 'win32') {
        const { stdout: loadavg } = await execAsync('cat /proc/loadavg 2>/dev/null || uptime | grep -o "load average.*" | cut -d: -f2');
        const loads = loadavg.trim().split(/\s+/);

        if (loads.length >= 3) {
          performanceMonitor.gauge('system_load_1m', parseFloat(loads[0]) || 0, 'load');
          performanceMonitor.gauge('system_load_5m', parseFloat(loads[1]) || 0, 'load');
          performanceMonitor.gauge('system_load_15m', parseFloat(loads[2]) || 0, 'load');
        }
      }

      // 獲取記憶體使用情況(Linux)
      if (process.platform === 'linux') {
        const { stdout: meminfo } = await execAsync('cat /proc/meminfo 2>/dev/null || echo ""');
        const memData = this.parseMeminfo(meminfo);

        if (memData.MemTotal && memData.MemAvailable) {
          const usedMemory = memData.MemTotal - memData.MemAvailable;
          const memoryUtilization = (usedMemory / memData.MemTotal) * 100;

          performanceMonitor.gauge('system_memory_total_bytes', memData.MemTotal * 1024, 'bytes');
          performanceMonitor.gauge('system_memory_used_bytes', usedMemory * 1024, 'bytes');
          performanceMonitor.gauge('system_memory_utilization', memoryUtilization, 'percent');
        }
      }

    } catch (error) {
      // 忽略系統指標收集錯誤
      console.debug('Could not collect OS metrics:', error.message);
    }
  }

  private parseMeminfo(meminfo: string): Record<string, number> {
    const data: Record<string, number> = {};
    const lines = meminfo.split('\n');

    for (const line of lines) {
      const match = line.match(/^(\w+):\s+(\d+)\s+kB$/);
      if (match) {
        data[match[1]] = parseInt(match[2], 10);
      }
    }

    return data;
  }
}

export const systemMonitor = new SystemMonitor();

6. 效能瓶頸分析工具

// packages/kyo-core/src/monitoring/profiler.ts
import { performanceMonitor } from './performance.js';

interface BottleneckAnalysis {
  endpoint: string;
  averageResponseTime: number;
  p95ResponseTime: number;
  errorRate: number;
  requestCount: number;
  severity: 'low' | 'medium' | 'high' | 'critical';
}

interface ResourceAnalysis {
  type: 'cpu' | 'memory' | 'database' | 'redis';
  utilization: number;
  trend: 'increasing' | 'stable' | 'decreasing';
  recommendation: string;
}

export class PerformanceAnalyzer {
  // 分析API端點效能
  analyzeEndpointPerformance(timeWindow: number = 300000): BottleneckAnalysis[] { // 5 分鐘
    const results: BottleneckAnalysis[] = [];
    const requestMetrics = performanceMonitor.getMetricStats('http_request_duration', timeWindow);

    if (!requestMetrics) return results;

    // 這裡應該按路由分組分析,簡化版本使用總體數據
    const errorStats = performanceMonitor.getMetricStats('http_errors_total', timeWindow);
    const totalRequests = requestMetrics.count;
    const errorCount = errorStats?.count || 0;
    const errorRate = totalRequests > 0 ? (errorCount / totalRequests) * 100 : 0;

    let severity: BottleneckAnalysis['severity'] = 'low';
    if (requestMetrics.p95 > 2000 || errorRate > 5) {
      severity = 'critical';
    } else if (requestMetrics.p95 > 1000 || errorRate > 2) {
      severity = 'high';
    } else if (requestMetrics.p95 > 500 || errorRate > 1) {
      severity = 'medium';
    }

    results.push({
      endpoint: 'overall',
      averageResponseTime: requestMetrics.avg,
      p95ResponseTime: requestMetrics.p95,
      errorRate,
      requestCount: totalRequests,
      severity,
    });

    return results;
  }

  // 分析資源使用情況
  analyzeResourceUsage(): ResourceAnalysis[] {
    const results: ResourceAnalysis[] = [];

    // 記憶體分析
    const memoryStats = performanceMonitor.getMetricStats('nodejs_memory_heap_used_bytes');
    if (memoryStats) {
      const memoryUtilization = (memoryStats.avg / (1024 * 1024 * 1024)) * 100; // GB 轉百分比

      results.push({
        type: 'memory',
        utilization: memoryUtilization,
        trend: 'stable', // 簡化版本,實際應該分析趨勢
        recommendation: memoryUtilization > 80
          ? 'Consider increasing memory allocation or optimize memory usage'
          : 'Memory usage is within acceptable range',
      });
    }

    // 資料庫分析
    const dbStats = performanceMonitor.getMetricStats('db_query_duration');
    if (dbStats) {
      results.push({
        type: 'database',
        utilization: dbStats.p95,
        trend: 'stable',
        recommendation: dbStats.p95 > 1000
          ? 'Database queries are slow, consider adding indexes or optimizing queries'
          : 'Database performance is acceptable',
      });
    }

    // Redis 分析
    const redisStats = performanceMonitor.getMetricStats('redis_command_duration');
    if (redisStats) {
      results.push({
        type: 'redis',
        utilization: redisStats.p95,
        trend: 'stable',
        recommendation: redisStats.p95 > 100
          ? 'Redis commands are slow, check network latency or Redis server performance'
          : 'Redis performance is good',
      });
    }

    return results;
  }

  // 生成效能報告
  generatePerformanceReport(): {
    timestamp: string;
    bottlenecks: BottleneckAnalysis[];
    resources: ResourceAnalysis[];
    recommendations: string[];
  } {
    const bottlenecks = this.analyzeEndpointPerformance();
    const resources = this.analyzeResourceUsage();

    const recommendations: string[] = [];

    // 基於分析結果生成建議
    bottlenecks.forEach(bottleneck => {
      if (bottleneck.severity === 'critical') {
        recommendations.push(`URGENT: ${bottleneck.endpoint} has critical performance issues (P95: ${bottleneck.p95ResponseTime.toFixed(0)}ms, Error Rate: ${bottleneck.errorRate.toFixed(1)}%)`);
      } else if (bottleneck.severity === 'high') {
        recommendations.push(`HIGH: ${bottleneck.endpoint} needs optimization (P95: ${bottleneck.p95ResponseTime.toFixed(0)}ms)`);
      }
    });

    resources.forEach(resource => {
      if (resource.utilization > 80) {
        recommendations.push(`RESOURCE: ${resource.type} utilization is high (${resource.utilization.toFixed(1)}%) - ${resource.recommendation}`);
      }
    });

    return {
      timestamp: new Date().toISOString(),
      bottlenecks,
      resources,
      recommendations,
    };
  }
}

export const performanceAnalyzer = new PerformanceAnalyzer();

7. 前端效能監控

// apps/kyo-dashboard/src/utils/performance.ts
interface WebVitalsMetric {
  name: string;
  value: number;
  rating: 'good' | 'needs-improvement' | 'poor';
  timestamp: number;
}

export class FrontendPerformanceMonitor {
  private metrics: WebVitalsMetric[] = [];
  private observer: PerformanceObserver | null = null;

  init(): void {
    // 監控 Web Vitals
    this.initWebVitals();

    // 監控資源載入
    this.initResourceTiming();

    // 監控導航
    this.initNavigationTiming();
  }

  private initWebVitals(): void {
    if ('PerformanceObserver' in window) {
      // Largest Contentful Paint (LCP)
      this.observer = new PerformanceObserver((list) => {
        const entries = list.getEntries();
        const lastEntry = entries[entries.length - 1] as any;

        if (lastEntry) {
          this.recordMetric({
            name: 'LCP',
            value: lastEntry.startTime,
            rating: this.rateLCP(lastEntry.startTime),
            timestamp: Date.now(),
          });
        }
      });

      this.observer.observe({ entryTypes: ['largest-contentful-paint'] });

      // Cumulative Layout Shift (CLS)
      let clsValue = 0;
      const clsObserver = new PerformanceObserver((list) => {
        for (const entry of list.getEntries() as any[]) {
          if (!entry.hadRecentInput) {
            clsValue += entry.value;
          }
        }

        this.recordMetric({
          name: 'CLS',
          value: clsValue,
          rating: this.rateCLS(clsValue),
          timestamp: Date.now(),
        });
      });

      clsObserver.observe({ entryTypes: ['layout-shift'] });
    }

    // First Input Delay (FID) - 使用事件監聽器模擬
    let isFirstInput = true;
    const firstInputHandler = (event: Event) => {
      if (isFirstInput) {
        isFirstInput = false;
        const fid = performance.now() - (event as any).timeStamp;

        this.recordMetric({
          name: 'FID',
          value: fid,
          rating: this.rateFID(fid),
          timestamp: Date.now(),
        });

        // 移除監聽器
        ['click', 'keydown', 'touchstart'].forEach(type => {
          document.removeEventListener(type, firstInputHandler, true);
        });
      }
    };

    ['click', 'keydown', 'touchstart'].forEach(type => {
      document.addEventListener(type, firstInputHandler, true);
    });
  }

  private initResourceTiming(): void {
    if ('PerformanceObserver' in window) {
      const resourceObserver = new PerformanceObserver((list) => {
        for (const entry of list.getEntries()) {
          const resourceEntry = entry as PerformanceResourceTiming;

          // 記錄大型資源載入時間
          if (resourceEntry.transferSize > 100000) { // > 100KB
            this.recordMetric({
              name: 'resource_load_time',
              value: resourceEntry.responseEnd - resourceEntry.startTime,
              rating: 'good', // 簡化評級
              timestamp: Date.now(),
            });
          }
        }
      });

      resourceObserver.observe({ entryTypes: ['resource'] });
    }
  }

  private initNavigationTiming(): void {
    window.addEventListener('load', () => {
      const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;

      if (navigation) {
        // Time to First Byte (TTFB)
        const ttfb = navigation.responseStart - navigation.requestStart;
        this.recordMetric({
          name: 'TTFB',
          value: ttfb,
          rating: this.rateTTFB(ttfb),
          timestamp: Date.now(),
        });

        // DOM Content Loaded
        const dcl = navigation.domContentLoadedEventEnd - navigation.navigationStart;
        this.recordMetric({
          name: 'DCL',
          value: dcl,
          rating: 'good', // 簡化評級
          timestamp: Date.now(),
        });

        // Page Load
        const loadTime = navigation.loadEventEnd - navigation.navigationStart;
        this.recordMetric({
          name: 'page_load',
          value: loadTime,
          rating: 'good', // 簡化評級
          timestamp: Date.now(),
        });
      }
    });
  }

  private recordMetric(metric: WebVitalsMetric): void {
    this.metrics.push(metric);

    // 保持最近 100 個指標
    if (this.metrics.length > 100) {
      this.metrics.shift();
    }

    // 發送到後端
    this.sendMetricToBackend(metric);
  }

  private async sendMetricToBackend(metric: WebVitalsMetric): Promise<void> {
    try {
      await fetch('/api/metrics/frontend', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          ...metric,
          userAgent: navigator.userAgent,
          url: window.location.href,
          viewport: {
            width: window.innerWidth,
            height: window.innerHeight,
          },
        }),
      });
    } catch (error) {
      console.debug('Failed to send metric to backend:', error);
    }
  }

  // Web Vitals 評級函數
  private rateLCP(value: number): WebVitalsMetric['rating'] {
    if (value <= 2500) return 'good';
    if (value <= 4000) return 'needs-improvement';
    return 'poor';
  }

  private rateFID(value: number): WebVitalsMetric['rating'] {
    if (value <= 100) return 'good';
    if (value <= 300) return 'needs-improvement';
    return 'poor';
  }

  private rateCLS(value: number): WebVitalsMetric['rating'] {
    if (value <= 0.1) return 'good';
    if (value <= 0.25) return 'needs-improvement';
    return 'poor';
  }

  private rateTTFB(value: number): WebVitalsMetric['rating'] {
    if (value <= 800) return 'good';
    if (value <= 1800) return 'needs-improvement';
    return 'poor';
  }

  getMetrics(): WebVitalsMetric[] {
    return [...this.metrics];
  }

  getMetricSummary(): Record<string, { avg: number; rating: WebVitalsMetric['rating'] }> {
    const summary: Record<string, { values: number[]; ratings: WebVitalsMetric['rating'][] }> = {};

    this.metrics.forEach(metric => {
      if (!summary[metric.name]) {
        summary[metric.name] = { values: [], ratings: [] };
      }
      summary[metric.name].values.push(metric.value);
      summary[metric.name].ratings.push(metric.rating);
    });

    const result: Record<string, { avg: number; rating: WebVitalsMetric['rating'] }> = {};

    Object.entries(summary).forEach(([name, data]) => {
      const avg = data.values.reduce((sum, val) => sum + val, 0) / data.values.length;
      const poorCount = data.ratings.filter(r => r === 'poor').length;
      const goodCount = data.ratings.filter(r => r === 'good').length;

      let overallRating: WebVitalsMetric['rating'] = 'needs-improvement';
      if (poorCount / data.ratings.length > 0.25) {
        overallRating = 'poor';
      } else if (goodCount / data.ratings.length > 0.75) {
        overallRating = 'good';
      }

      result[name] = { avg, rating: overallRating };
    });

    return result;
  }

  destroy(): void {
    if (this.observer) {
      this.observer.disconnect();
      this.observer = null;
    }
  }
}

// 全域實例
export const frontendPerformanceMonitor = new FrontendPerformanceMonitor();

// 自動初始化
if (typeof window !== 'undefined') {
  frontendPerformanceMonitor.init();
}

8. 效能優化建議系統

// packages/kyo-core/src/monitoring/optimizer.ts
import { performanceAnalyzer } from './profiler.js';
import { performanceMonitor } from './performance.js';

interface OptimizationSuggestion {
  category: 'database' | 'cache' | 'api' | 'frontend' | 'infrastructure';
  priority: 'low' | 'medium' | 'high' | 'critical';
  title: string;
  description: string;
  implementation: string;
  estimatedImpact: string;
  estimatedEffort: 'low' | 'medium' | 'high';
}

export class PerformanceOptimizer {
  generateOptimizationSuggestions(): OptimizationSuggestion[] {
    const suggestions: OptimizationSuggestion[] = [];
    const report = performanceAnalyzer.generatePerformanceReport();

    // 分析資料庫效能
    const dbBottlenecks = report.bottlenecks.filter(b =>
      b.averageResponseTime > 500 || b.p95ResponseTime > 1000
    );

    if (dbBottlenecks.length > 0) {
      suggestions.push({
        category: 'database',
        priority: 'high',
        title: '資料庫查詢優化',
        description: '檢測到資料庫查詢延遲較高,建議優化慢查詢',
        implementation: '1. 添加適當的索引\n2. 優化複雜查詢\n3. 考慮使用查詢快取\n4. 分析查詢執行計畫',
        estimatedImpact: '回應時間可改善 30-50%',
        estimatedEffort: 'medium',
      });
    }

    // 分析快取使用
    const redisHitRate = performanceMonitor.getMetricStats('redis_hit_rate');
    if (redisHitRate && redisHitRate.avg < 80) {
      suggestions.push({
        category: 'cache',
        priority: 'medium',
        title: '快取策略優化',
        description: `Redis 快取命中率較低 (${redisHitRate.avg.toFixed(1)}%),建議改善快取策略`,
        implementation: '1. 增加快取TTL\n2. 預熱常用資料\n3. 優化快取鍵設計\n4. 實施多層快取',
        estimatedImpact: '減少 20-30% 資料庫負載',
        estimatedEffort: 'low',
      });
    }

    // 分析記憶體使用
    const memoryStats = performanceMonitor.getMetricStats('nodejs_memory_heap_used_bytes');
    if (memoryStats && memoryStats.avg > 500 * 1024 * 1024) { // > 500MB
      suggestions.push({
        category: 'infrastructure',
        priority: 'medium',
        title: '記憶體使用優化',
        description: '應用程式記憶體使用量較高,建議進行記憶體優化',
        implementation: '1. 分析記憶體洩漏\n2. 優化物件生命週期\n3. 使用記憶體池\n4. 考慮垂直擴展',
        estimatedImpact: '提升應用穩定性,降低 OOM 風險',
        estimatedEffort: 'high',
      });
    }

    // API 效能分析
    const highErrorRate = report.bottlenecks.filter(b => b.errorRate > 1);
    if (highErrorRate.length > 0) {
      suggestions.push({
        category: 'api',
        priority: 'high',
        title: 'API 錯誤率優化',
        description: '檢測到較高的 API 錯誤率,建議改善錯誤處理',
        implementation: '1. 加強輸入驗證\n2. 改善錯誤處理邏輯\n3. 添加重試機制\n4. 實施斷路器模式',
        estimatedImpact: '降低錯誤率,提升使用者體驗',
        estimatedEffort: 'medium',
      });
    }

    // 前端效能建議
    suggestions.push({
      category: 'frontend',
      priority: 'low',
      title: '前端效能優化',
      description: '持續優化前端效能,提升使用者體驗',
      implementation: '1. 程式碼分割\n2. 懶載入\n3. 圖片優化\n4. CDN 使用\n5. Bundle 大小優化',
      estimatedImpact: '改善載入時間 20-40%',
      estimatedEffort: 'medium',
    });

    return suggestions.sort((a, b) => {
      const priorityOrder = { critical: 4, high: 3, medium: 2, low: 1 };
      return priorityOrder[b.priority] - priorityOrder[a.priority];
    });
  }

  // 生成效能儀表板數據
  generateDashboardData(): {
    overview: {
      avgResponseTime: number;
      errorRate: number;
      throughput: number;
      uptime: number;
    };
    trends: {
      responseTime: number[];
      errorRate: number[];
      throughput: number[];
    };
    alerts: {
      level: 'info' | 'warning' | 'error';
      message: string;
      timestamp: Date;
    }[];
  } {
    const report = performanceAnalyzer.generatePerformanceReport();
    const requestStats = performanceMonitor.getMetricStats('http_request_duration');
    const errorStats = performanceMonitor.getMetricStats('http_errors_total');
    const totalStats = performanceMonitor.getMetricStats('http_requests_total');

    const overview = {
      avgResponseTime: requestStats?.avg || 0,
      errorRate: totalStats && errorStats ? (errorStats.count / totalStats.count) * 100 : 0,
      throughput: totalStats?.count || 0,
      uptime: process.uptime(),
    };

    // 簡化的趨勢數據(實際應該從時間序列資料庫獲取)
    const trends = {
      responseTime: Array(10).fill(0).map(() => Math.random() * 1000 + 200),
      errorRate: Array(10).fill(0).map(() => Math.random() * 5),
      throughput: Array(10).fill(0).map(() => Math.random() * 100 + 50),
    };

    const alerts = report.recommendations.map(rec => ({
      level: rec.includes('URGENT') ? 'error' as const :
             rec.includes('HIGH') ? 'warning' as const : 'info' as const,
      message: rec,
      timestamp: new Date(),
    }));

    return { overview, trends, alerts };
  }
}

export const performanceOptimizer = new PerformanceOptimizer();

今天的成果

我們建立了全方位的效能監控和優化架構:

多層監控:應用、資料庫、Redis、系統資源全覆蓋
即時指標:自動收集和分析關鍵效能指標
智能分析:自動識別效能瓶頸和優化機會
前端監控:Web Vitals 和使用者體驗指標
優化建議:基於數據的具體優化建議
可視化監控:效能儀表板和趨勢分析


上一篇
Day 7: 30天打造SaaS產品後端篇-測試策略與品質保證架構
下一篇
Day 9: 30天打造SaaS產品後端篇-後端前端整合與效能最佳化
系列文
30 天打造工作室 SaaS 產品 (後端篇)10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言