article

Circuit Breaker Pattern - เกราะป้องกันระบบ Microservices

8 min read

Circuit Breaker คืออะไร?

Circuit Breaker Pattern เป็นรูปแบบการออกแบบที่ช่วยป้องกันไม่ให้ระบบหนึ่งเรียกใช้บริการที่กำลังล้มเหลวอย่างต่อเนื่อง โดยจะ “ตัดวงจร” เมื่อเกิดความผิดพลาดเกินขีดจำกัด

เหมือนกับ Circuit Breaker ในระบบไฟฟ้าที่จะตัดไฟเมื่อมีกระแสเกินเพื่อป้องกันไฟไหม้

States ของ Circuit Breaker

1. Closed State (ปกติ)

  • Request ผ่านไปยัง Service ได้ปกติ
  • นับจำนวน Failures
  • เมื่อ Failure เกิน Threshold จะเปลี่ยนเป็น Open

2. Open State (ตัดวงจร)

  • Block Request ทั้งหมด ส่ง Error กลับทันที
  • มี Timeout สำหรับเปลี่ยนเป็น Half-Open
  • ช่วยป้องกัน Cascade Failure

3. Half-Open State (ทดลอง)

  • อนุญาต Request จำนวนจำกัด
  • ถ้าสำเร็จ → กลับเป็น Closed
  • ถ้าล้มเหลว → กลับเป็น Open
graph LR
    A[Closed] -->|Failures > Threshold| B[Open]
    B -->|Timeout| C[Half-Open]
    C -->|Success| A
    C -->|Failure| B

การ Implement Circuit Breaker

JavaScript/Node.js Implementation

class CircuitBreaker {
  constructor(options = {}) {
    this.failureThreshold = options.failureThreshold || 5;
    this.resetTimeout = options.resetTimeout || 30000; // 30 seconds
    this.monitor = options.monitor || console.log;
    
    this.state = 'CLOSED';
    this.failureCount = 0;
    this.lastFailureTime = null;
    this.nextAttempt = null;
  }

  async call(service) {
    if (this.state === 'OPEN') {
      if (Date.now() < this.nextAttempt) {
        throw new Error('Circuit breaker is OPEN');
      }
      // Try to recover
      this.state = 'HALF_OPEN';
      this.monitor('Circuit breaker entering HALF_OPEN state');
    }

    try {
      const result = await service();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  onSuccess() {
    this.failureCount = 0;
    if (this.state === 'HALF_OPEN') {
      this.state = 'CLOSED';
      this.monitor('Circuit breaker recovered to CLOSED state');
    }
  }

  onFailure() {
    this.failureCount++;
    this.lastFailureTime = Date.now();

    if (this.state === 'HALF_OPEN') {
      this.state = 'OPEN';
      this.nextAttempt = Date.now() + this.resetTimeout;
      this.monitor('Circuit breaker back to OPEN state');
    } else if (this.failureCount >= this.failureThreshold) {
      this.state = 'OPEN';
      this.nextAttempt = Date.now() + this.resetTimeout;
      this.monitor(`Circuit breaker OPENED due to ${this.failureCount} failures`);
    }
  }

  getState() {
    return {
      state: this.state,
      failureCount: this.failureCount,
      lastFailureTime: this.lastFailureTime,
      nextAttempt: this.nextAttempt
    };
  }
}

การใช้งานในจริง

const axios = require('axios');

class APIService {
  constructor() {
    this.circuitBreaker = new CircuitBreaker({
      failureThreshold: 3,
      resetTimeout: 60000, // 1 minute
      monitor: (message) => console.log(`[Circuit Breaker] ${message}`)
    });
  }

  async getUserById(userId) {
    try {
      return await this.circuitBreaker.call(async () => {
        const response = await axios.get(`/api/users/${userId}`, {
          timeout: 5000
        });
        return response.data;
      });
    } catch (error) {
      // Fallback strategy
      return this.getUserFromCache(userId) || this.getDefaultUser();
    }
  }

  getUserFromCache(userId) {
    // Implementation for cache lookup
    return cache.get(`user:${userId}`);
  }

  getDefaultUser() {
    return {
      id: 'unknown',
      name: 'Anonymous User',
      email: 'unknown@example.com'
    };
  }
}

Advanced Circuit Breaker Features

1. Sliding Window Counter

class SlidingWindowCircuitBreaker {
  constructor(options = {}) {
    this.windowSize = options.windowSize || 10;
    this.failureThreshold = options.failureThreshold || 0.5; // 50%
    this.resetTimeout = options.resetTimeout || 30000;
    
    this.requests = [];
    this.state = 'CLOSED';
  }

  async call(service) {
    if (this.state === 'OPEN' && !this.shouldAttemptReset()) {
      throw new Error('Circuit breaker is OPEN');
    }

    const startTime = Date.now();
    try {
      const result = await service();
      this.recordSuccess(startTime);
      return result;
    } catch (error) {
      this.recordFailure(startTime);
      throw error;
    }
  }

  recordSuccess(timestamp) {
    this.addRequest(timestamp, true);
    if (this.state === 'HALF_OPEN') {
      this.state = 'CLOSED';
    }
  }

  recordFailure(timestamp) {
    this.addRequest(timestamp, false);
    
    const failureRate = this.getFailureRate();
    if (failureRate >= this.failureThreshold) {
      this.state = 'OPEN';
      this.openedAt = Date.now();
    }
  }

  addRequest(timestamp, success) {
    this.requests.push({ timestamp, success });
    
    // Keep only recent requests
    const cutoff = timestamp - this.windowSize * 1000;
    this.requests = this.requests.filter(req => req.timestamp > cutoff);
  }

  getFailureRate() {
    if (this.requests.length === 0) return 0;
    
    const failures = this.requests.filter(req => !req.success).length;
    return failures / this.requests.length;
  }

  shouldAttemptReset() {
    return this.state === 'OPEN' && 
           Date.now() - this.openedAt >= this.resetTimeout;
  }
}

2. Multiple Failure Types

class SmartCircuitBreaker {
  constructor(options = {}) {
    this.thresholds = {
      timeout: options.timeoutThreshold || 3,
      serverError: options.serverErrorThreshold || 5,
      networkError: options.networkErrorThreshold || 2
    };
    
    this.counts = {
      timeout: 0,
      serverError: 0,
      networkError: 0
    };
    
    this.state = 'CLOSED';
  }

  classifyError(error) {
    if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
      return 'networkError';
    }
    if (error.code === 'ECONNABORTED' || error.message.includes('timeout')) {
      return 'timeout';
    }
    if (error.response && error.response.status >= 500) {
      return 'serverError';
    }
    return 'unknown';
  }

  onFailure(error) {
    const errorType = this.classifyError(error);
    
    if (errorType in this.counts) {
      this.counts[errorType]++;
      
      if (this.counts[errorType] >= this.thresholds[errorType]) {
        this.state = 'OPEN';
        console.log(`Circuit opened due to ${errorType} errors`);
      }
    }
  }
}

Circuit Breaker สำหรับ Database Connections

class DatabaseCircuitBreaker {
  constructor(database, options = {}) {
    this.database = database;
    this.circuitBreaker = new CircuitBreaker({
      failureThreshold: options.failureThreshold || 3,
      resetTimeout: options.resetTimeout || 30000
    });
  }

  async query(sql, params = []) {
    return this.circuitBreaker.call(async () => {
      const connection = await this.database.getConnection();
      try {
        return await connection.query(sql, params);
      } finally {
        connection.release();
      }
    });
  }

  async transaction(operations) {
    return this.circuitBreaker.call(async () => {
      const connection = await this.database.getConnection();
      await connection.beginTransaction();
      
      try {
        const results = [];
        for (const operation of operations) {
          const result = await connection.query(operation.sql, operation.params);
          results.push(result);
        }
        
        await connection.commit();
        return results;
      } catch (error) {
        await connection.rollback();
        throw error;
      } finally {
        connection.release();
      }
    });
  }
}

Integration กับ HTTP Clients

Axios Interceptor

class HTTPCircuitBreaker {
  constructor() {
    this.breakers = new Map();
    this.setupInterceptors();
  }

  getBreaker(baseURL) {
    if (!this.breakers.has(baseURL)) {
      this.breakers.set(baseURL, new CircuitBreaker({
        failureThreshold: 5,
        resetTimeout: 60000
      }));
    }
    return this.breakers.get(baseURL);
  }

  setupInterceptors() {
    // Request interceptor
    axios.interceptors.request.use(
      (config) => {
        const breaker = this.getBreaker(config.baseURL);
        
        if (breaker.getState().state === 'OPEN') {
          return Promise.reject(new Error('Circuit breaker is OPEN'));
        }
        
        return config;
      },
      (error) => Promise.reject(error)
    );

    // Response interceptor
    axios.interceptors.response.use(
      (response) => {
        const breaker = this.getBreaker(response.config.baseURL);
        breaker.onSuccess();
        return response;
      },
      (error) => {
        if (error.config && error.config.baseURL) {
          const breaker = this.getBreaker(error.config.baseURL);
          breaker.onFailure();
        }
        return Promise.reject(error);
      }
    );
  }
}

// การใช้งาน
const httpBreaker = new HTTPCircuitBreaker();

// ทุก HTTP request จะผ่าน Circuit Breaker อัตโนมัติ
const api = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 5000
});

Monitoring และ Metrics

class CircuitBreakerMonitor {
  constructor() {
    this.metrics = {
      requests: 0,
      successes: 0,
      failures: 0,
      circuitOpens: 0,
      circuitCloses: 0
    };
    
    this.events = [];
  }

  recordRequest() {
    this.metrics.requests++;
  }

  recordSuccess() {
    this.metrics.successes++;
    this.addEvent('SUCCESS');
  }

  recordFailure(error) {
    this.metrics.failures++;
    this.addEvent('FAILURE', { error: error.message });
  }

  recordCircuitOpen() {
    this.metrics.circuitOpens++;
    this.addEvent('CIRCUIT_OPEN');
  }

  recordCircuitClose() {
    this.metrics.circuitCloses++;
    this.addEvent('CIRCUIT_CLOSE');
  }

  addEvent(type, data = {}) {
    this.events.push({
      timestamp: new Date().toISOString(),
      type,
      ...data
    });
    
    // Keep only last 100 events
    if (this.events.length > 100) {
      this.events = this.events.slice(-100);
    }
  }

  getMetrics() {
    const successRate = this.metrics.requests > 0 
      ? (this.metrics.successes / this.metrics.requests * 100).toFixed(2)
      : 0;

    return {
      ...this.metrics,
      successRate: `${successRate}%`,
      recentEvents: this.events.slice(-10)
    };
  }

  // Export metrics for Prometheus
  toPrometheusFormat() {
    return `
# HELP circuit_breaker_requests_total Total number of requests
# TYPE circuit_breaker_requests_total counter
circuit_breaker_requests_total ${this.metrics.requests}

# HELP circuit_breaker_successes_total Total number of successful requests
# TYPE circuit_breaker_successes_total counter
circuit_breaker_successes_total ${this.metrics.successes}

# HELP circuit_breaker_failures_total Total number of failed requests
# TYPE circuit_breaker_failures_total counter
circuit_breaker_failures_total ${this.metrics.failures}

# HELP circuit_breaker_opens_total Total number of circuit opens
# TYPE circuit_breaker_opens_total counter
circuit_breaker_opens_total ${this.metrics.circuitOpens}
    `.trim();
  }
}

Fallback Strategies

1. Cache Fallback

class CacheFirstService {
  constructor() {
    this.cache = new Map();
    this.circuitBreaker = new CircuitBreaker();
  }

  async getData(key) {
    try {
      // Try primary service
      const data = await this.circuitBreaker.call(() => 
        this.fetchFromPrimaryService(key)
      );
      
      // Cache successful result
      this.cache.set(key, {
        data,
        timestamp: Date.now(),
        ttl: 300000 // 5 minutes
      });
      
      return data;
    } catch (error) {
      // Fallback to cache
      const cached = this.cache.get(key);
      if (cached && Date.now() - cached.timestamp < cached.ttl) {
        console.log('Serving from cache due to circuit breaker');
        return cached.data;
      }
      
      // Last resort: default data
      return this.getDefaultData(key);
    }
  }
}

2. Secondary Service Fallback

class MultiServiceClient {
  constructor() {
    this.primaryBreaker = new CircuitBreaker({ name: 'primary' });
    this.secondaryBreaker = new CircuitBreaker({ name: 'secondary' });
  }

  async fetchUser(userId) {
    try {
      return await this.primaryBreaker.call(() =>
        this.fetchFromPrimary(userId)
      );
    } catch (primaryError) {
      console.log('Primary service failed, trying secondary...');
      
      try {
        return await this.secondaryBreaker.call(() =>
          this.fetchFromSecondary(userId)
        );
      } catch (secondaryError) {
        // Both services failed
        throw new Error('All services unavailable');
      }
    }
  }
}

Best Practices

1. กำหนด Threshold ที่เหมาะสม

// สำหรับ Critical Services
const criticalServiceBreaker = new CircuitBreaker({
  failureThreshold: 2,     // เปิดเร็ว
  resetTimeout: 10000      // ลองใหม่เร็ว
});

// สำหรับ Non-Critical Services  
const nonCriticalBreaker = new CircuitBreaker({
  failureThreshold: 10,    // อดทนมากกว่า
  resetTimeout: 60000      // ลองใหม่ช้ากว่า
});

2. Health Check Integration

class HealthAwareCircuitBreaker extends CircuitBreaker {
  constructor(healthCheckUrl, options = {}) {
    super(options);
    this.healthCheckUrl = healthCheckUrl;
    this.healthCheckInterval = options.healthCheckInterval || 30000;
    this.startHealthCheck();
  }

  startHealthCheck() {
    setInterval(async () => {
      if (this.state === 'OPEN') {
        const isHealthy = await this.checkHealth();
        if (isHealthy) {
          this.state = 'HALF_OPEN';
          console.log('Service appears healthy, entering HALF_OPEN');
        }
      }
    }, this.healthCheckInterval);
  }

  async checkHealth() {
    try {
      const response = await axios.get(this.healthCheckUrl, {
        timeout: 5000
      });
      return response.status === 200;
    } catch {
      return false;
    }
  }
}

3. Graceful Degradation

class GracefulService {
  async getRecommendations(userId) {
    try {
      return await this.circuitBreaker.call(() =>
        this.getPersonalizedRecommendations(userId)
      );
    } catch (error) {
      console.log('Personalized recommendations unavailable, using fallback');
      
      // Fallback to popular items
      return this.getPopularRecommendations();
    }
  }

  async getPopularRecommendations() {
    // Return cached popular items or default recommendations
    return this.cache.get('popular') || this.getDefaultRecommendations();
  }
}

สรุป

Circuit Breaker Pattern เป็นเครื่องมือสำคัญในการสร้าง Resilient Microservices โดย:

  1. ป้องกัน Cascade Failure - ไม่ให้ความล้มเหลวลุกลามไปยังระบบอื่น
  2. Fail Fast - ให้ Error กลับเร็วแทนที่จะรอ Timeout
  3. Auto Recovery - ลองเชื่อมต่อใหม่อัตโนมัติเมื่อเวลาผ่านไป
  4. Graceful Degradation - ให้บริการลดทอนแทนการหยุดทำงาน

การใช้ Circuit Breaker อย่างถูกต้องจะทำให้ระบบมีความเสถียรและสามารถรับมือกับความผิดพลาดได้อย่างมีประสิทธิภาพ!