Circuit Breaker Pattern — Bảo vệ hệ thống khỏi Cascading Failure

8 phút đọc

⚡ Ý tưởng gốc

Circuit Breaker trong software mượn khái niệm từ cầu dao điện (electrical circuit breaker) — khi dòng điện quá tải → cầu dao tự ngắt → bảo vệ cả hệ thống khỏi cháy nổ.

Tương tự, khi một dependency (ví dụ AirHost API) liên tục fail → Circuit Breaker ngắt kết nối, không gọi nữa → bảo vệ hệ thống của bạn khỏi cascading failure.

Pattern này được phổ biến bởi Michael Nygard trong cuốn Release It! (2007) và là một trong những core patterns của resilient distributed systems.


🔄 3 trạng thái của Circuit Breaker

stateDiagram-v2
    [*] --> Closed
    Closed --> Open : Failures vượt threshold
    Open --> HalfOpen : Hết thời gian chờ (cooldown)
    HalfOpen --> Closed : Request thử thành công
    HalfOpen --> Open : Request thử vẫn fail
Trạng thái Hành vi Ví dụ cụ thể (AirHost)
🟢 Closed (bình thường) Mọi request đi qua bình thường. Đếm số lỗi. Nếu lỗi vượt ngưỡng → chuyển sang Open. AirHost trả 200 OK → mọi thứ hoạt động bình thường. Đếm: 0 fail.
🔴 Open (ngắt mạch) Không gọi dependency nữa. Trả lỗi ngay lập tức (fail-fast). Chờ cooldown period. AirHost fail 10 lần trong 1 phút → breaker mở → mọi request booking trả lỗi ngay trong 0ms thay vì chờ timeout 10s.
🟡 Half-Open (thăm dò) Cho 1–2 request thử đi qua. Nếu thành công → đóng lại (Closed). Nếu fail → mở lại (Open). Sau 30s cooldown → cho 1 request gọi AirHost. AirHost trả 200 → breaker đóng lại, hoạt động bình thường.

🤔 Tại sao cần Circuit Breaker? (Không có thì sao?)

Scenario: AirHost down 5 phút, KHÔNG có Circuit Breaker

  1. Mỗi request tạo booking → gọi AirHost → chờ timeout 10s → fail
  2. 100 request/phút → 100 worker/thread bị block 10s mỗi cái
  3. Worker pool cạn kiệt → API của bạn cũng chết theo (cascading failure)
  4. Các endpoint khác (search, list bookings) cũng bị ảnh hưởng vì không còn worker
  5. Toàn bộ system down chỉ vì 1 dependency down

Scenario: CÓ Circuit Breaker

  1. AirHost fail 10 lần → breaker Open
  2. Request tiếp theo → trả lỗi ngay trong 0ms ("Service temporarily unavailable")
  3. Worker không bị block → các endpoint khác hoạt động bình thường
  4. Sau 30s → Half-Open → thử 1 request → AirHost recover → Closed → hoạt động lại

🔧 Các thông số cấu hình quan trọng

Parameter Mô tả Giá trị gợi ý
failureThreshold Số lần fail liên tiếp trước khi mở breaker 5–10 (tuỳ SLA của dependency)
cooldownPeriod Thời gian chờ ở trạng thái Open trước khi thử lại (Half-Open) 15–60s
successThreshold Số request thành công ở Half-Open cần để chuyển về Closed 1–3
timeout Thời gian tối đa chờ response từ dependency 3–10s
monitoringWindow Sliding window để đếm failure (tránh lỗi cũ ảnh hưởng) 60s
failureRateThreshold % failure trong window thay vì đếm tuyệt đối (advanced) 50–80%

💻 Implementation trong Laravel/PHP

Cách 1: Tự implement đơn giản với Redis

class CircuitBreaker
{
    private string $service;
    private int $failureThreshold;
    private int $cooldownPeriod;

    const STATE_CLOSED = 'closed';
    const STATE_OPEN = 'open';
    const STATE_HALF_OPEN = 'half_open';

    public function __construct(
        string $service,
        int $failureThreshold = 5,
        int $cooldownPeriod = 30
    ) {
        $this->service = $service;
        $this->failureThreshold = $failureThreshold;
        $this->cooldownPeriod = $cooldownPeriod;
    }

    public function call(callable $action, callable $fallback = null)
    {
        $state = $this->getState();

        if ($state === self::STATE_OPEN) {
            // Fail-fast: không gọi dependency
            return $fallback ? $fallback() : throw new CircuitOpenException(
                "Circuit breaker is OPEN for {$this->service}"
            );
        }

        try {
            $result = $action();
            $this->recordSuccess();
            return $result;
        } catch (\Throwable $e) {
            $this->recordFailure();
            if ($fallback) return $fallback();
            throw $e;
        }
    }

    private function getState(): string
    {
        $failures = (int) Redis::get("cb:{$this->service}:failures");
        $openedAt = Redis::get("cb:{$this->service}:opened_at");

        if ($failures >= $this->failureThreshold) {
            if ($openedAt && (time() - $openedAt) >= $this->cooldownPeriod) {
                return self::STATE_HALF_OPEN;
            }
            return self::STATE_OPEN;
        }

        return self::STATE_CLOSED;
    }

    private function recordFailure(): void
    {
        $failures = Redis::incr("cb:{$this->service}:failures");
        if ($failures >= $this->failureThreshold) {
            Redis::set("cb:{$this->service}:opened_at", time());
        }
    }

    private function recordSuccess(): void
    {
        Redis::del("cb:{$this->service}:failures");
        Redis::del("cb:{$this->service}:opened_at");
    }
}

Sử dụng:

$breaker = new CircuitBreaker('airhost_api', failureThreshold: 5, cooldownPeriod: 30);

$result = $breaker->call(
    action: fn() => Http::timeout(5)->post('https://api.airhost.co/bookings', $data),
    fallback: fn() => ['status' => 'queued', 'message' => 'Booking sẽ được xử lý sau']
);

Cách 2: Dùng package ackintosh/ganesha (production-ready)

composer require ackintosh/ganesha
use Ackintosh\Ganesha;
use Ackintosh\Ganesha\Builder;
use Ackintosh\Ganesha\Storage\Adapter\Redis as GaneshaRedis;

$ganesha = Builder::withRateStrategy()
    ->adapter(new GaneshaRedis($redis))
    ->failureRateThreshold(50)       // 50% failure rate
    ->intervalToHalfOpen(10)          // 10s cooldown
    ->minimumRequests(10)             // Ít nhất 10 request trước khi tính rate
    ->timeWindow(30)                  // Sliding window 30s
    ->build();

if (!$ganesha->isAvailable('airhost_api')) {
    // Circuit is OPEN → fail-fast
    return response()->json(['error' => 'Service temporarily unavailable'], 503);
}

try {
    $response = Http::timeout(5)->post('https://api.airhost.co/bookings', $data);
    $ganesha->success('airhost_api');
    return $response;
} catch (\Exception $e) {
    $ganesha->failure('airhost_api');
    throw $e;
}

🧩 Circuit Breaker kết hợp với các pattern khác

Pattern Kết hợp thế nào Ví dụ
Retry + Backoff Retry trước → nếu vẫn fail → Circuit Breaker mở. Retry nằm BÊN TRONG breaker, không phải bên ngoài. Retry 3 lần với exponential backoff (1s, 2s, 4s) → fail cả 3 → đếm 1 failure cho breaker.
Fallback Khi breaker Open → trả về fallback thay vì error. Có thể là cached data, default value, hoặc queue lại. AirHost down → trả về "Booking đã được ghi nhận, sẽ xác nhận sau" → queue job retry.
Bulkhead Giới hạn concurrent requests đến mỗi dependency → kết hợp với Circuit Breaker để double protection. Max 20 concurrent calls đến AirHost. Nếu >20 → reject ngay. Nếu fail rate cao → breaker mở.
Timeout Timeout là input cho Circuit Breaker — request timeout được đếm là failure. Http::timeout(5) → nếu AirHost không trả trong 5s → đếm failure.
Queue/Async Khi breaker Open → đẩy request vào queue để retry sau khi service recover. Booking request → breaker Open → dispatch ProcessBookingJob → job sẽ retry khi breaker Closed.

⚠️ Những sai lầm phổ biến khi implement


📊 Monitoring & Alerting

Circuit Breaker phải có observability — nếu không bạn sẽ không biết khi nào breaker mở:

  • Metrics cần track:
    • Số lần state transition (Closed → Open, Open → Half-Open, ...)
    • Current state per service
    • Failure rate trong sliding window
    • Số request bị reject (fail-fast) khi breaker Open
  • Alerting:
    • Alert khi breaker chuyển sang Open → team biết dependency đang có vấn đề
    • Alert khi breaker ở Open quá lâu (>5 phút) → cần manual intervention
  • Tools: Prometheus + Grafana, Datadog, hoặc đơn giản là Laravel Log + Slack webhook
// Log state transitions
Log::warning('Circuit breaker OPENED', [
    'service' => 'airhost_api',
    'failures' => $failures,
    'threshold' => $this->failureThreshold,
]);

// Slack notification
Slack::send("🔴 Circuit Breaker OPEN for airhost_api — {$failures} failures in last 60s");

🏗️ Khi nào KHÔNG cần Circuit Breaker?

  • Dependency là internal service cùng infra và có health check + auto-restart → retry + timeout đủ rồi
  • Fire-and-forget jobs — job fail thì retry sau, không block user request
  • Read-only cache (Redis cache) — miss cache thì fallback về DB, không cần breaker
  • Hệ thống monolith đơn giản — chỉ 1–2 external API, traffic thấp → try-catch + retry là đủ

📚 Tham khảo

Bài viết liên quan

Đang cập nhật...