Circuit Breaker trong Backend Production: failure isolation, half-open và cách ngừng retry mù

Trong nhiều incident backend, thứ giết hệ thống không phải lỗi gốc ban đầu mà là phản ứng dây chuyền sau đó. Một dependency bắt đầu chậm hoặc trả lỗi 5xx, client vẫn tiếp tục gửi request với timeout dài, retry lại nhiều lớp, connection pool cạn dần, queue backlog tăng, CPU tăng vì thread chờ I/O, rồi cả service vốn còn khỏe cũng bị kéo chìm theo.

Đó là lúc circuit breaker đáng giá. Nhưng circuit breaker không phải “bật lên là xong”. Nếu cấu hình sai, nó có thể hoặc quá nhạy khiến traffic hợp lệ bị chặn oan, hoặc quá lỏng khiến failure đã lan ra rồi mới chịu mở mạch. Tệ hơn, nhiều team bật circuit breaker nhưng vẫn để retry và timeout ở các lớp khác hoạt động vô kỷ luật, thành ra hệ thống vẫn tự DDoS chính mình.

Bài này nhìn circuit breaker dưới góc production engineering: nó giải quyết bài toán gì, closed/open/half-open nên vận hành ra sao, phối hợp với timeout-retry-bulkhead như thế nào, metrics nào đáng theo dõi, và những anti-pattern khiến circuit breaker chỉ là checkbox kiến trúc chứ không thật sự cô lập failure.

Circuit breaker thực chất giải quyết bài toán gì?

So sánh retry storm với fail fast nhờ circuit breaker trong hệ thống backend
So sánh retry storm với fail fast nhờ circuit breaker trong hệ thống backend

Circuit breaker không sửa dependency hỏng. Nó giải quyết ba việc khác quan trọng hơn trong runtime:

  • chặn request mới tới một dependency đang lỗi hoặc quá chậm trước khi tài nguyên phía caller bị đốt cạn;
  • rút ngắn feedback loop để caller fail fast thay vì treo hàng loạt thread hoặc coroutine chờ timeout dài;
  • tạo một control point để hệ thống thử hồi phục có kiểm soát thay vì bắn lại mù quáng.

Nếu chỉ có timeout mà không có circuit breaker, mỗi request vẫn phải chờ đủ timeout trước khi thất bại. Khi traffic cao, hàng nghìn request cùng chờ một dependency đang hấp hối sẽ nhanh chóng ăn hết connection, worker và memory. Circuit breaker cắt vòng lặp đó bằng cách nói thẳng: “tạm thời đừng gọi nữa”.

Failure lan truyền trong distributed system diễn ra như thế nào?

Timeline chuyển trạng thái closed open half open của circuit breaker
Timeline chuyển trạng thái closed open half open của circuit breaker

Một kịch bản quen thuộc:

  1. Service A gọi Service B.
  2. Service B chậm dần vì DB lock hoặc downstream khác.
  3. Service A timeout sau 5 giây nhưng có retry 3 lần.
  4. Load thực tế đổ vào B không còn là 100 rps mà thành 250–300 rps hiệu dụng vì retry chồng lên request mới.
  5. Connection pool ở A giữ đầy các request đang chờ; thread pool bắt đầu nghẽn.
  6. Những endpoint khác trong A dùng chung tài nguyên cũng chậm theo.
  7. Incident lan từ một dependency thành outage hệ thống.

Circuit breaker sinh ra để bẻ gãy chuỗi khuếch đại này. Nó không làm B khỏe lại, nhưng nó ngăn A tự giết mình vì tiếp tục hy vọng vô căn cứ.

Ba trạng thái nền tảng: closed, open, half-open

Các lớp timeout retry bulkhead circuit breaker và fallback trong backend production
Các lớp timeout retry bulkhead circuit breaker và fallback trong backend production

Closed

Đây là trạng thái bình thường. Request được phép đi qua dependency. Circuit breaker chỉ quan sát dữ liệu như:

  • failure rate;
  • slow call rate;
  • số lượng request tối thiểu trong cửa sổ đo;
  • loại exception nào được tính là failure.

Closed không có nghĩa dependency chắc chắn khỏe. Nó chỉ có nghĩa chưa đủ evidence để ngắt mạch.

Open

Khi vượt ngưỡng cấu hình, breaker chuyển sang open. Từ lúc này, các request mới tới dependency sẽ bị fail fast ngay tại caller mà không thử gọi thật.

Điều này cực kỳ quan trọng trong production vì:

  • bảo vệ thread/coroutine/connection pool;
  • giảm áp lực lên dependency đang lỗi;
  • khiến latency xấu biến thành failure nhanh, dễ quan sát hơn;
  • mở đường cho fallback hoặc degraded mode nếu business cho phép.

Một open breaker tốt thường đi kèm error rõ ràng, metrics rõ ràng và alert phù hợp. Nếu open mà cả team không biết, hoặc caller trả lỗi mơ hồ, breaker chỉ đổi loại đau chứ chưa giảm đau.

Half-open

Half-open là trạng thái thử hồi phục có kiểm soát. Sau một khoảng wait duration, circuit breaker cho phép một số rất nhỏ request đi qua để kiểm tra dependency đã ổn chưa.

Nếu các request probe này thành công theo tiêu chí đã định, breaker đóng lại. Nếu vẫn fail hoặc quá chậm, breaker quay về open.

Half-open là phần bị cấu hình sai nhiều nhất. Nếu cho quá nhiều request thăm dò cùng lúc, dependency vừa ngoi lên sẽ lại bị dội traffic và ngã tiếp. Nếu quá ít hoặc quá thưa, hệ thống hồi phục chậm không cần thiết.

Failure rate chưa đủ, slow call rate mới là tín hiệu đến sớm

Dashboard observability cho circuit breaker với state transition failure rate và slow call rate
Dashboard observability cho circuit breaker với state transition failure rate và slow call rate

Nhiều team chỉ mở circuit breaker khi tỉ lệ lỗi vượt ngưỡng. Cách này bỏ lỡ một tín hiệu sớm rất quan trọng: slow call rate.

Trong thực tế, dependency thường chết theo tiến trình:

  • đầu tiên là tăng latency;
  • sau đó timeout cục bộ;
  • rồi mới tăng 5xx hoặc connection error rõ rệt.

Nếu chờ tới lúc lỗi hẳn mới mở mạch, caller đã tiêu tốn quá nhiều tài nguyên. Với các thư viện như Resilience4j, slow call threshold giúp breaker phản ứng trước khi hệ thống thực sự nổ tung.

Điểm tôi thích ở slow call rate là nó phản ánh trải nghiệm thực tế của caller, không chỉ phản ánh việc dependency có trả error code hay không.

Circuit breaker không thể tách rời timeout

Một circuit breaker mà không có timeout hợp lý gần như thiếu một nửa cơ chế.

Nếu timeout quá dài:

  • request probe ở half-open sẽ tốn tài nguyên quá lâu;
  • metrics phản ứng chậm;
  • caller vẫn bị treo lâu trước khi breaker có đủ dữ liệu mở mạch.

Nếu timeout quá ngắn:

  • hệ thống dễ mở breaker oan khi dependency chỉ có một vài spike ngắn;
  • các thao tác vốn cần thời gian hơn bình thường bị đánh fail không cần thiết.

Nguyên tắc thực dụng là timeout phải bám vào SLO và latency budget của endpoint, không lấy bừa một con số tròn đẹp. Một endpoint synchronous cho user request có budget khác hẳn batch worker hoặc async reconciliation.

Retry và circuit breaker: phối hợp sai là tự tạo retry storm

Circuit breaker không thay thế retry hoàn toàn. Nhưng retry phải phục tùng kỷ luật của breaker.

Retry hữu ích khi nào?

  • lỗi mạng ngắn hạn;
  • 503/429 có tín hiệu tạm thời rõ;
  • dependency có khả năng tự hồi trong vài giây;
  • idempotency đã được đảm bảo.

Retry nguy hiểm khi nào?

  • retry ở nhiều tầng cùng lúc: SDK, HTTP client, service layer, queue consumer;
  • retry vào dependency đang quá tải;
  • retry không có jitter;
  • retry request có side effect nhưng không có idempotency key;
  • retry trong lúc breaker đáng ra nên đã mở.

Một rule rất hữu ích: circuit breaker quyết định có nên thử gọi nữa không; retry quyết định nếu còn được gọi thì thử thêm bao nhiêu lần trong budget còn lại. Nếu không phân vai như vậy, retry sẽ tiếp tục khuếch đại failure ngay cả khi breaker tồn tại trên giấy.

Bulkhead, rate limiting và circuit breaker: ba lớp khác nhau, đừng trộn vai trò

Circuit breaker thường bị kỳ vọng làm quá nhiều việc. Thực ra nó nên đi cùng các control khác:

Bulkhead

Bulkhead giới hạn tài nguyên theo lane, ví dụ pool riêng cho dependency A. Mục tiêu là nếu A nghẽn thì không kéo chết toàn service.

Rate limiting / concurrency limiting

Giới hạn tốc độ hoặc số request in-flight để tránh overload cả ở caller lẫn callee.

Circuit breaker

Dựa trên tín hiệu runtime để tạm ngắt luồng gọi khi dependency đang unhealthy.

Ba lớp này bổ sung cho nhau:

  • bulkhead ngăn chia sẻ tài nguyên quá rộng;
  • rate limiting giữ lưu lượng trong biên cho phép;
  • circuit breaker dừng gọi khi quality của dependency đã xuống dưới ngưỡng chấp nhận.

Nếu chỉ có breaker mà không bulkhead, caller vẫn có thể nghẽn trước khi breaker kịp mở. Nếu chỉ có bulkhead mà không breaker, hàng đợi lane riêng vẫn có thể bị đốt sạch vô ích.

Fallback: dùng khi business chấp nhận degraded mode, không phải để che sự cố

Open breaker thường dẫn tới câu hỏi: “thế trả gì cho user?”

Một số fallback hợp lý:

  • trả cached data với dấu hiệu stale rõ ràng;
  • bỏ qua recommendation không critical nhưng vẫn trả được trang chính;
  • queue lại tác vụ non-blocking để xử lý sau;
  • hạ tính năng xuống chế độ read-only tạm thời.

Fallback nguy hiểm khi:

  • che mất failure thật khiến monitoring tưởng hệ thống ổn;
  • trả dữ liệu cũ nhưng không gắn semantics rõ ràng;
  • tạo inconsistency nghiệp vụ, ví dụ bỏ qua kiểm tra credit hoặc inventory mà vẫn cho commit giao dịch;
  • business nghĩ degraded mode là “đã xử lý xong vấn đề”.

Fallback tốt là một quyết định sản phẩm + kỹ thuật có ý thức, không phải câu catch trả default value cho xong.

Những tham số đáng chú ý khi cấu hình circuit breaker

Dù dùng Resilience4j, Envoy, Hystrix legacy hay cơ chế tương đương, có vài nhóm tham số luôn cần suy nghĩ kỹ:

Sliding window

Đo theo count-based hay time-based? Với traffic cao, count-based có thể phản ứng nhanh. Với traffic thấp, time-based giúp tránh việc chỉ vài request hiếm hoi làm méo thống kê.

Minimum number of calls

Nếu quá thấp, breaker sẽ phản ứng theo nhiễu. Nếu quá cao, nó phản ứng quá chậm trong incident thật.

Failure rate threshold

Không nên dùng một con số mặc định cho mọi dependency. Dependency critical, low-latency user path và job nền có tolerance rất khác nhau.

Slow call threshold / slow call rate threshold

Đây là cặp rất đáng đầu tư tinh chỉnh, đặc biệt cho HTTP/database dependency nơi “chậm bất thường” thường đến trước “chết hẳn”.

Wait duration in open state

Quá ngắn thì probe dồn dập làm dependency khó hồi. Quá dài thì recovery bị chậm và tạo downtime không cần thiết.

Permitted calls in half-open

Nên nhỏ, có chủ đích, và phù hợp mức chịu tải của dependency sau incident.

Exception classification

Không phải lỗi nào cũng nên tính như nhau. Ví dụ:

  • 404 từ downstream theo ngữ nghĩa nghiệp vụ thường không phải failure hạ tầng;
  • 429, 503, timeout, connection reset` thường là failure rất đáng tính;
  • 4xx do caller gửi request sai có thể nên tách riêng khỏi breaker của dependency.

Observability: nếu không thấy breaker mở ở đâu, vì sao mở, thì gần như chưa vận hành được

Tối thiểu nên có metrics sau:

  • số lần transition closed → open;
  • số lần half-open → open và half-open → closed;
  • request blocked vì breaker open;
  • failure rate và slow call rate theo dependency;
  • latency distribution trước và sau khi breaker mở;
  • fallback activation rate;
  • error budget burn liên quan dependency đó.

Ngoài metrics, log/trace cũng nên có:

  • dependency name;
  • breaker state hiện tại;
  • exception class / HTTP status;
  • request class hoặc endpoint caller;
  • correlation id để nối với incident timeline.

Một dashboard tốt sẽ giúp trả lời nhanh:

  • dependency nào đang làm breaker mở;
  • breaker mở có giảm latency và resource pressure ở caller không;
  • open breaker là symptom của dependency hay symptom của caller cấu hình timeout/retry dở.

Anti-pattern rất phổ biến khi dùng circuit breaker

1. Dùng một breaker cho quá nhiều loại request

Nếu endpoint read nhẹ và endpoint write nặng dùng chung một breaker, hành vi của chúng sẽ làm méo số liệu lẫn nhau. Nên tách theo dependency + operation class khi cần.

2. Mở breaker chỉ dựa trên exception count thô

Điều này bỏ qua slow call, traffic pattern và chất lượng request. Hệ quả là hoặc mở quá muộn, hoặc mở bởi tín hiệu không đại diện.

3. Cho fallback “đẹp” đến mức che mất incident

User có thể đỡ bị đau ngay lúc đó, nhưng team mất visibility và business ra quyết định dựa trên dữ liệu ảo.

4. Half-open cho quá nhiều request cùng lúc

Dependency vừa hồi lại đã bị test bằng một đợt mini-thundering herd. Đây là cách nhanh để tái phát incident.

5. Breaker mở nhưng upstream vẫn retry vòng ngoài

Đây là lỗi kiến trúc khá cay. Service A fail fast nhờ breaker, nhưng API gateway hoặc job orchestrator vẫn retry dồn dập. Kết quả là traffic không biến mất, chỉ đổi hình dạng.

6. Coi breaker là giải pháp thay cho capacity planning

Breaker giúp containment, không giúp năng lực hệ thống tự nhiên tăng lên. Nếu dependency luôn sát trần, breaker sẽ mở lặp đi lặp lại như một cái báo cháy bị vô hiệu hóa.

Ví dụ policy thực dụng cho HTTP dependency

Một chính sách tương đối an toàn cho user-facing synchronous call có thể là:

  • timeout client: 300–800ms tùy latency budget;
  • retry tối đa 1 lần, chỉ cho lỗi retryable, có jitter;
  • circuit breaker mở khi slow-call-rate hoặc failure-rate vượt ngưỡng trong cửa sổ đo đủ ý nghĩa;
  • bulkhead giới hạn concurrency riêng cho dependency;
  • fallback chỉ cho dữ liệu non-critical hoặc cached read path.

Pseudo-flow:

  1. request vào service;
  2. kiểm tra breaker state;
  3. nếu open → fail fast hoặc fallback;
  4. nếu closed/half-open → gọi dependency với timeout ngắn;
  5. classify kết quả;
  6. nếu retryable và còn budget → retry trong khuôn khổ breaker;
  7. cập nhật metrics và state transition.

Điểm mấu chốt không phải con số cụ thể, mà là các lớp này phải nhất quán với nhau.

Khi nào không nên dùng circuit breaker như control chính?

Có vài tình huống breaker không phải trung tâm:

  • workload async nơi queue + backpressure + consumer concurrency control quan trọng hơn call-site breaker;
  • dependency nội bộ cực nhanh nhưng lỗi chủ yếu do business validation chứ không phải hạ tầng;
  • luồng cần consistency chặt, nơi degraded mode/fallback không có ý nghĩa sản phẩm;
  • vấn đề chính là thiếu capacity kéo dài, không phải failure burst ngắn.

Trong các trường hợp đó, breaker vẫn có thể hữu ích, nhưng không nên là thứ duy nhất team bám vào.

Checklist tự audit circuit breaker trong hệ thống của anh

  • Team có biết dependency nào hiện chưa có timeout hợp lý không?
  • Breaker hiện tại có đo slow call rate hay chỉ đo lỗi?
  • Retry có đang tồn tại ở nhiều tầng mà không có budget thống nhất không?
  • Half-open hiện cho bao nhiêu request đi qua, và con số đó dựa trên dữ liệu gì?
  • Fallback có được product/business chấp nhận về semantics không?
  • Có dashboard nào cho state transition của breaker theo từng dependency không?
  • Khi breaker mở, upstream có tiếp tục retry hoặc queue dồn traffic lại không?
  • Có dependency nào breaker mở lặp đi lặp lại hằng ngày, báo hiệu vấn đề capacity nền chưa được giải quyết không?

Nếu phần lớn câu trả lời là “chưa rõ”, khả năng cao circuit breaker của hệ thống đang tồn tại như thư viện, chưa tồn tại như operational control.

Kết luận

Circuit breaker là một trong những pattern hiếm hoi vừa đơn giản về ý tưởng vừa cực thực dụng trong production. Nó không sửa được dependency đang hỏng, nhưng nó ngăn caller tiếp tục tự đốt tài nguyên, ngăn failure lan dây chuyền, và tạo một cơ chế hồi phục có kiểm soát qua half-open.

Điều quan trọng là đừng dùng nó như một món đồ trang trí kiến trúc. Circuit breaker chỉ thực sự có giá trị khi đi cùng timeout tử tế, retry có budget, bulkhead đủ chặt, observability đủ sâu và fallback đủ trung thực về semantics.

Nếu phải chọn ba việc nên làm trước, tôi sẽ làm:

  • audit timeout và retry hiện tại trước khi bật breaker hàng loạt;
  • thêm slow call rate vào tiêu chí mở mạch cho các dependency quan trọng;
  • dựng dashboard state transition để thấy breaker có đang giảm blast radius thật hay không.

Đọc tiếp về backend reliability và production engineering