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
}

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):

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ề
- 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.
- 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.
- 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
- Martin Fowler — CircuitBreaker: https://martinfowler.com/bliki/CircuitBreaker.html
- Go — sony/gobreaker (thư viện circuit breaker): https://github.com/sony/gobreaker
- Microsoft — Circuit Breaker pattern: https://learn.microsoft.com/azure/architecture/patterns/circuit-breaker
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 đó.