Dependency (HTTP API, DB, gRPC) đang 5xx / timeout hàng loạt. Caller vẫn bắn full traffic + retry → thread/pool chờ, queue phình, user chờ 30s rồi error — và dependency càng không gượng dậy được vì retry storm.

Circuit breaker cắt mạch: sau đủ lỗi, tạm không gọi nữa (fail-fast), chờ một nhịp, thử lại dè dặt. Ý tưởng mượn aptomat điện: quá tải thì ngắt, không để cháy cả nhà.

Demo state machine: Circuit breaker — closed / open / half-open.

Ba trạng thái

        thành công
   ┌──────────────────┐
   ▼                  │
 CLOSED ──(lỗi đủ)──► OPEN
   ▲                  │
   │            (hết cooldown)
   │                  ▼
   └──(probe ok)── HALF-OPEN
         (probe fail)──► OPEN
  • Closed (bình thường): mọi call đi qua; đếm lỗi (theo cửa sổ thời gian hoặc consecutive).
  • Open: call mới fail ngay (hoặc fallback) — không đụng dependency. Timer / deadline sang half-open.
  • Half-open: cho một (vài) probe; success → closed; fail → open lại.

Sketch tối thiểu

function createBreaker({
  failureThreshold = 5,
  cooldownMs = 5000,
  halfOpenMax = 1,
} = {}) {
  let state = "closed"; // closed | open | half-open
  let failures = 0;
  let openedAt = 0;
  let halfOpenInFlight = 0;

  async function exec(fn) {
    const now = Date.now();
    if (state === "open") {
      if (now - openedAt < cooldownMs) {
        const err = new Error("circuit_open");
        err.code = "CIRCUIT_OPEN";
        throw err;
      }
      state = "half-open";
      halfOpenInFlight = 0;
    }

    if (state === "half-open") {
      if (halfOpenInFlight >= halfOpenMax) {
        const err = new Error("circuit_half_open_busy");
        err.code = "CIRCUIT_OPEN";
        throw err;
      }
      halfOpenInFlight++;
    }

    try {
      const result = await fn();
      // success
      failures = 0;
      if (state === "half-open") state = "closed";
      return result;
    } catch (e) {
      if (e.code === "CIRCUIT_OPEN") throw e; // không đếm reject open là fail upstream
      failures++;
      if (state === "half-open" || failures >= failureThreshold) {
        state = "open";
        openedAt = Date.now();
        failures = 0;
      }
      throw e;
    } finally {
      // chỉ giảm nếu vẫn half-open (success đã closed; fail đã open)
      if (state === "half-open") {
        halfOpenInFlight = Math.max(0, halfOpenInFlight - 1);
      } else {
        halfOpenInFlight = 0;
      }
    }
  }

  return { exec, getState: () => state };
}

Production library (opossum, resilience4j, Polly…) thêm: sliding window, % error, slow-call threshold, bulkhead — nhưng state machine cốt lõi vẫn vậy.

Breaker ≠ retry ≠ timeout

  • Timeout: một call không được treo vô hạn.
  • Retry + backoff: lỗi tạm; cần idempotency nếu side-effect.
  • Breaker: “dependency đang đỏ — đừng thử thêm một lúc”.
  • Bulkhead / limit: trần in-flight tới dependency (unbounded).

Hay xếp: timeout → (retry có budget) → breaker bọc ngoài → fallback. Breaker đếm sau khi timeout/fail, không thay timeout.

Fallback

Khi open, thay vì chỉ throw:

  • Cache stale / last-known-good
  • Default an toàn (“feature off”)
  • Hàng đợi defer (async) — cẩn thận backpressure
  • Degrade UX: “tạm ẩn gợi ý”, không phải trắng trang 30s

Fallback sai domain (trả “đã thanh toán” khi không chắc) nguy hiểm hơn error rõ.

Cấu hình hay sai

  • Threshold quá thấp — một blip 2 lỗi → open → flapping.
  • Cooldown quá dài — dependency sống lại mà mình vẫn fail-fast.
  • Đếm cả 4xx business — user sai OTP không phải dependency chết; chỉ đếm 5xx / timeout / connection.
  • Một breaker cho mọi endpoint — path chết kéo cả service “khỏe”. Tách theo dependency + operation class.
  • Half-open cho unlimited probe — vừa mở đã đập lại full traffic. Giữ probe nhỏ.

Quan sát

  • State hiện tại (gauge/enum)
  • calls_rejected_open
  • Success/fail trong half-open
  • Thời gian ở open (alert nếu dài bất thường)

Dashboard “dependency đỏ” + “breaker open” giúp phân biệt: mình cắt chủ động hay user đang dính latency.

Một câu để nhớ

Breaker đánh đổi availability tức thì để bảo vệ hồi phục và capacity còn lại. Fail nhanh, có kiểm soát, thử lại có liều lượng — hơn là timeout dây chuyền và retry làm sập cả chuỗi.

Ghép với pool limit, backpressure, idempotent retry: đó là bộ vệ sinh tối thiểu khi gọi service khác qua mạng.