Rate limiting (bài 1) bảo vệ bạn khỏi client dồn dập. Circuit breaker bảo vệ bạn khỏi một thứ khác: một service phụ thuộc đang chết. Khi service B (database, API bên thứ ba) lỗi liên tục, nếu service A cứ gọi mãi, mỗi call phải chờ timeout rồi mới lỗi — tốn luồng, tốn kết nối, độ trễ tăng vọt, và cuối cùng A cũng nghẽn theo. Đây là sụp dây chuyền (cascading failure): B chết kéo A chết. Circuit breaker (cầu dao) cắt vòng lặp này: sau vài lỗi, nó "ngắt mạch" và trả lỗi ngay lập tức thay vì gọi B. Bài này (phần 2 loạt Hệ thống chịu tải) chạy thật để thấy nó hoạt động.

Cơ chế: máy trạng thái ba pha

Circuit breaker là một máy trạng thái với ba pha:

  • CLOSED (bình thường): mọi call đi qua tới service, breaker đếm lỗi. Khi số lỗi liên tiếp vượt ngưỡng → chuyển OPEN.
  • OPEN (ngắt): chặn ngay mọi call, trả lỗi nhanh (~0ms), không gọi service. Sau một khoảng cooldown → chuyển HALF_OPEN.
  • HALF_OPEN (thử): cho vài call thử đi qua. Thành công → CLOSED (service đã hồi); thất bại → OPEN lại.
func (b *Breaker) Call(fn func() error) error {
    if b.state == Open {
        if time.Since(b.openedAt) >= b.cooldown { b.transition(Open, HalfOpen) }
        else { return ErrOpen }   // chặn NGAY, không gọi service
    }
    err := fn()                    // gọi service THẬT
    if err != nil {
        b.failCount++
        if b.failCount >= b.threshold { b.transition(Closed, Open) }
    } else {
        if b.state == HalfOpen { b.transition(HalfOpen, Closed) }
        b.failCount = 0
    }
    return err
}

Ảnh chụp đoạn mã Go nền tối minh hoạ circuit breaker, vấn đề gọi mãi một service đang chết tự làm mình chết service phụ thuộc lỗi mỗi call chờ timeout vài giây rồi lỗi tiếp tục gọi tốn luồng kết nối độ trễ tăng lan sang caller sụp dây chuyền cascading failure, cơ chế máy trạng thái 3 pha CLOSED bình thường gọi service đếm lỗi lỗi liên tiếp vượt ngưỡng OPEN ngắt chặn ngay mọi call trả lỗi nhanh 0ms không gọi service sau cooldown HALF_OPEN thử cho vài call thử thành công CLOSED thất bại OPEN lại, cài đặt tự cài mutex đếm lỗi cooldown func Call fn func error error if state Open if time Since openedAt lớn hơn cooldown transition Open HalfOpen else return ErrOpen chặn ngay không gọi service err bằng fn gọi service thật if err failCount cộng nếu vượt threshold transition Closed Open else if HalfOpen transition CLOSED failCount bằng 0

Hình 1: Circuit breaker là máy trạng thái CLOSED → OPEN (khi lỗi vượt ngưỡng) → HALF_OPEN (sau cooldown) → CLOSED (thử thành công); ở OPEN, call bị chặn ngay không gọi service.

Đo thật: fail fast và tự hồi phục

Mình giả lập một service chậm (200ms) rồi lỗi, gọi 20 lần qua breaker (ngưỡng 5, cooldown 300ms):

Ảnh chụp bảng kết quả chạy thật circuit breaker output thật, PHA 1 service lỗi liên tục gọi 20 lần qua breaker breaker CLOSED sang OPEN sau 5 lỗi liên tiếp call 6 chặn nhanh 0s call 7 chặn nhanh 0s tổng thời gian 20 call 1.02 s call thật chạm service 5 mỗi cái 200ms bị chặn nhanh 15 0ms không breaker 20 nhân 200ms bằng 4000ms có breaker chỉ 5 call thật bằng 1.02s, PHA 2 service hồi phục chờ cooldown 300ms breaker OPEN sang HALF_OPEN hết cooldown cho thử breaker HALF_OPEN sang CLOSED call thử thành công call hồi phục 1 OK state CLOSED call hồi phục 2 OK state CLOSED tự động phục hồi, kết luận fail fast khi service chết breaker mở chặn 0ms thay vì chờ timeout bảo vệ khỏi cascading chỉ 5 call thật thay vì 20 không tự làm nghẽn tự hồi phục half-open thử thành công đóng lại đánh đổi có thể chặn nhầm khi lỗi thoáng qua chỉnh ngưỡng cooldown thường kết hợp với timeout bài 4 fallback bài 11

Hình 2: Chạy thật — Pha 1: sau 5 lỗi breaker OPEN, 15 call còn lại bị chặn nhanh (~0s), chỉ 5 call thật chạm service; tổng 1,02s thay vì 4000ms nếu không có breaker. Pha 2: hết cooldown → HALF_OPEN → thử thành công → CLOSED, tự hồi phục.

Đọc kết quả đo được:

  • Fail fast: chỉ 5 call thật, 15 chặn ~0ms: service lỗi liên tục, sau 5 lỗi (ngưỡng) breaker chuyển OPEN. Từ call thứ 6 trở đi, breaker chặn ngay lập tức (0s) — không gọi service, không chờ timeout. Kết quả: chỉ 5 call thật sự chạm service (mỗi cái 200ms), 15 call còn lại trả lỗi tức thì.
  • Tiết kiệm thời gian và tài nguyên rõ rệt: tổng 20 call chỉ tốn 1,02s. Nếu không có breaker, mỗi call phải chờ 200ms → 20 × 200ms = 4000ms, và tệ hơn là 20 luồng bị chiếm giữ chờ đợi. Breaker biến "chờ mòn mỏi rồi lỗi" thành "lỗi ngay" — giải phóng tài nguyên để phục vụ việc khác.
  • Tự hồi phục qua half-open: sau cooldown 300ms, breaker chuyển HALF_OPEN và cho một call thử; service đã sống lại nên call thành công → breaker về CLOSED. Không cần can thiệp tay: hệ tự phát hiện service hồi phục và mở lại lưu lượng.

Đánh đổi cần cân nhắc

Ngưỡng và cooldown là hai núm chỉnh quan trọng — và dễ sai. Ngưỡng quá thấp (mở sau 1-2 lỗi) khiến breaker mở vì lỗi thoáng qua (một request chậm ngẫu nhiên), chặn nhầm lưu lượng lành mạnh. Ngưỡng quá cao thì mở quá muộn, không bảo vệ kịp. Cooldown quá ngắn → thử lại dồn dập khi service chưa hồi; quá dài → chậm phục hồi khi service đã khỏe. Nên dùng tỉ lệ lỗi trong cửa sổ (ví dụ >50% lỗi trong 10 giây) thay vì đếm lỗi liên tiếp tuyệt đối, để ổn định hơn với traffic thật.

Circuit breaker fail fast — nhưng "fail" vẫn là lỗi. Khi breaker mở, caller nhận lỗi ngay thay vì chờ, nhưng vẫn là lỗi. Bản thân breaker không làm request thành công. Vì vậy nó thường đi cùng fallback (bài 11): khi breaker mở, trả một giá trị mặc định, dữ liệu cache cũ, hoặc chế độ suy giảm (graceful degradation) thay vì báo lỗi trần cho người dùng. Breaker + fallback mới cho trải nghiệm tốt.

Breaker per-dependency, không per-toàn-hệ. Mỗi service phụ thuộc nên có breaker riêng. Nếu dùng chung một breaker cho mọi phụ thuộc, lỗi ở service B sẽ mở breaker chặn luôn cả call tới service C (đang khỏe) — biến một sự cố cục bộ thành toàn cục. Đây chính là nguyên lý bulkhead (bài 10): cô lập để lỗi ở một chỗ không lan. Mỗi phụ thuộc = một breaker + timeout riêng.

Ba ý mang về

  1. Circuit breaker ngắt mạch để fail fast khi service chết: đo thật 20 call tới service lỗi chỉ 5 chạm thật, 15 bị chặn ~0ms (tổng 1,02s vs 4000ms không breaker) — biến "chờ timeout rồi lỗi" thành "lỗi ngay", giải phóng tài nguyên.
  2. Ba trạng thái tự động: CLOSED → OPEN → HALF_OPEN → CLOSED: đo thật breaker mở sau 5 lỗi, rồi sau cooldown tự thử qua half-open và đóng lại khi service hồi phục — không cần can thiệp tay.
  3. Chỉnh ngưỡng/cooldown cẩn thận, kết hợp fallback và breaker riêng cho mỗi phụ thuộc: ngưỡng nên dùng tỉ lệ lỗi trong cửa sổ; breaker fail fast nhưng vẫn là lỗi nên cần fallback (bài 11); mỗi phụ thuộc một breaker để lỗi không lan (bulkhead, bài 10).

Nguồn

Phần sau ta gặp mặt còn lại của đồng xu: retry với exponential backoff và jitter — khi lỗi thoáng qua, thử lại là đúng, nhưng thử lại sai cách gây "bão retry" làm sập service đang gượng dậy; đo thật cách backoff + jitter tránh điều đó.