Circuit Breaker + Resilience4j: chặn lỗi lan truyền
Tiếp đi: Circuit Breaker.
Đây là phần rất quan trọng khi phỏng vấn Java Backend, Microservices và System Design. Hệ thống đang có:
Client → API Gateway → Order Service ──OpenFeign──> Payment Service
Giờ giả sử Payment bị lỗi hoặc rất chậm. Làm sao để lỗi không lan sang Order?
1. Bài toán
- Order Service có 200 Tomcat worker threads.
- Payment Service quá tải, mỗi request mất 30 giây rồi timeout.
- 1.000 người dùng đồng thời tạo đơn hàng.
2. Vấn đề: Cascading Failure
Order Service
|
v
Payment Service
|
X DOWN / chậm 30s
- Order vẫn tiếp tục gửi request đến Payment dù Payment đang hỏng.
- 200 Tomcat thread đều đứng chờ Payment.
- Request mới phải chờ hoặc bị từ chối; cả các API không liên quan Payment cũng chết theo.
Một service lỗi kéo theo nhiều service khác. Đó là Cascading Failure.
3. Giải pháp: Circuit Breaker
Ý tưởng giống cầu dao điện: dependency lỗi liên tục thì tạm ngừng gọi nó. 3 trạng thái:
- CLOSED: bình thường. Request đi qua, Circuit Breaker đếm tỷ lệ lỗi và slow call.
- OPEN: từ chối ngay, không gọi HTTP. Ném
CallNotPermittedException, chạy fallback. - HALF_OPEN: cho một số request thử để xem Payment hồi phục chưa.
Các cơ chế đi kèm: Timeout (không chờ vô hạn), Retry (thử lại có kiểm soát), Fallback (phương án thay thế), Bulkhead (giới hạn tài nguyên cho một dependency), Rate Limiter (giới hạn số request).
Điểm cần hiểu: Circuit Breaker không sửa được Payment. Nó bảo vệ Order khỏi việc tiêu tốn tài nguyên cho một dependency đang lỗi.
4. Code từng bước
Bước 1: Dependency (Spring Boot 3, order-service):
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Chọn version Resilience4j tương thích Spring Boot. Annotation chạy qua AOP proxy nên cần starter aop.
Bước 2: application.yml
resilience4j:
circuitbreaker:
instances:
paymentService:
slidingWindowType: COUNT_BASED
slidingWindowSize: 10
minimumNumberOfCalls: 10
failureRateThreshold: 50
slowCallDurationThreshold: 2s
slowCallRateThreshold: 50
waitDurationInOpenState: 10s
permittedNumberOfCallsInHalfOpenState: 3
automaticTransitionFromOpenToHalfOpenEnabled: true
retry:
instances:
paymentStatus:
maxAttempts: 3
waitDuration: 500ms
retryExceptions:
- java.io.IOException
timelimiter:
instances:
paymentService:
timeoutDuration: 3s
bulkhead:
instances:
paymentService:
maxConcurrentCalls: 20
maxWaitDuration: 0
management:
endpoints:
web:
exposure:
include: health,circuitbreakers,circuitbreakerevents
| Config | Ý nghĩa |
|---|---|
slidingWindowSize: 10 | Theo dõi tối đa 10 call gần nhất |
minimumNumberOfCalls: 10 | Cần ít nhất 10 call mới đánh giá tỷ lệ lỗi |
failureRateThreshold: 50 | Tỷ lệ lỗi ≥ 50% thì OPEN |
slowCallDurationThreshold | Call > 2s bị tính là slow call |
waitDurationInOpenState: 10s | Chờ 10 giây trước khi thử phục hồi |
permittedNumberOfCallsInHalfOpenState: 3 | Cho phép 3 probe call |
Bước 3: Code Java
@Service
public class OrderService {
private final PaymentClient paymentClient;
public OrderService(PaymentClient paymentClient) {
this.paymentClient = paymentClient;
}
@Bulkhead(name = "paymentService")
@CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
public PaymentResponse createPayment(PaymentRequest request) {
return paymentClient.createPayment(request);
}
public PaymentResponse paymentFallback(PaymentRequest request, Throwable ex) {
return new PaymentResponse("UNKNOWN", "Payment result requires verification");
}
// GET idempotent → an toàn để retry
@Retry(name = "paymentStatus")
@CircuitBreaker(name = "paymentService")
public PaymentResponse getPaymentStatus(Long id) {
return paymentClient.getPayment(id);
}
// TimeLimiter dùng cho async (CompletableFuture)
@TimeLimiter(name = "paymentService")
@CircuitBreaker(name = "paymentService")
public CompletableFuture<PaymentResponse> getPaymentAsync(Long id) {
return CompletableFuture.supplyAsync(() -> paymentClient.getPayment(id));
}
}
- Fallback cùng tham số + thêm
Throwableở cuối, cùng kiểu trả về. - Phải gọi qua Spring proxy: gọi nội bộ
this.createPayment()thì annotation không chạy. - Fallback
UNKNOWNchỉ minh họa. Production phải phân biệt: lỗi trước khi gửi, timeout không rõ kết quả, lỗi nghiệp vụ, Circuit OPEN. Không tự kết luận đã thanh toán hay thất bại; lưu trạng thái và đối soát. - Với Feign đồng bộ, timeout chính là
readTimeoutcủa Feign;@TimeLimiterchỉ áp cho kiểu async.
Bước 4: Test bằng curl. Cho Payment cố tình trả 500:
@PostMapping("/payments")
public ResponseEntity<String> createPayment() {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Payment service unavailable");
}
for i in $(seq 1 12); do curl -s -X POST http://localhost:8080/orders/1/pay; echo; done
# 10 lần đầu: gọi Payment, lỗi → fallback UNKNOWN
# lần 11, 12: Circuit OPEN, không gọi Payment, fallback trả ngay
curl http://localhost:8081/actuator/circuitbreakers
# "paymentService": { "state": "OPEN", "failureRate": "100.0%", ... }
5. Cách hoạt động
failure rate ≥ 50%
(sau ≥ 10 calls)
CLOSED ───────────────────────> OPEN
^ |
| | chờ 10s
| 3 probe call OK v
+──────────────────────── HALF_OPEN
|
probe call lỗi |
OPEN <───────────────────────────+
Timeline:
t=0s call 1..10 → Payment 500 → failure 100% → OPEN
t=1s call 11 → CallNotPermittedException → fallback (0ms, không HTTP)
t=10s → HALF_OPEN
t=11s 3 probe calls → Payment đã sống → CLOSED
(nếu probe lỗi → OPEN thêm 10s)
Thứ tự decorator mặc định của Resilience4j (ngoài → trong):
Retry( CircuitBreaker( RateLimiter( TimeLimiter( Bulkhead( call ) ) ) ) )
Retry nằm ngoài nên mỗi lần retry đều được Circuit Breaker ghi nhận. Số lỗi đếm được phụ thuộc thứ tự này.
6. So sánh
| Cơ chế | Giải quyết vấn đề | Khi Payment chậm |
|---|---|---|
| Timeout / TimeLimiter | Không chờ quá lâu | Dừng chờ khi vượt giới hạn |
| Retry | Lỗi tạm thời | Thử lại lỗi được phép retry |
| Circuit Breaker | Dependency lỗi kéo dài | Ngừng gọi khi tỷ lệ lỗi cao |
| Bulkhead | Một dependency ăn hết tài nguyên | Giới hạn số call đồng thời |
| Rate Limiter | Quá nhiều request | Giới hạn tốc độ request |
| Fallback | Trải nghiệm khi lỗi | Trả kết quả thay thế |
Retry có thể nguy hiểm:
Order Payment
|--- Pay $100 ------->| SUCCESS
|<--- Response X | (mất response)
| Timeout
|--- Retry Pay $100 ->| SUCCESS again?
Không có idempotency thì retry gây thanh toán trùng. Chỉ retry thao tác idempotent hoặc có idempotency key.
Bulkhead có hai loại: SemaphoreBulkhead (giới hạn số call đồng thời) và FixedThreadPoolBulkhead (thread pool + queue riêng). 100 request gọi Payment thì tối đa 20 chạy, còn lại bị từ chối ngay, Inventory và Notification vẫn còn thread.
7. Khi nào nên dùng / không nên dùng
- Nên: mọi lời gọi qua mạng tới dependency có thể chậm/lỗi (service khác, API bên thứ ba).
- Không nên: bọc logic local; retry thao tác không idempotent; dùng fallback để "giả vờ thành công" với nghiệp vụ tiền.
- Luôn có timeout trước; Circuit Breaker không thay timeout.
Trước khi qua phần mới: Resilience4j và Circuit Breaker khác nhau thế nào, trả lời sao cho đúng? Resilience4j là thư viện của Spring, còn Circuit Breaker là design pattern hay nguyên lý?
Bạn hiểu gần đúng rồi. Chỉ cần chỉnh một điểm: Resilience4j không phải thư viện thuộc Spring, mà là thư viện Java độc lập, có hỗ trợ tích hợp với Spring Boot.
| Khái niệm | Bản chất | Vai trò |
|---|---|---|
| Circuit Breaker | Design Pattern | Ngăn ứng dụng liên tục gọi một service đang gặp lỗi |
| Resilience4j | Java Library | Cung cấp implementation của Circuit Breaker, Retry, Bulkhead, RateLimiter... |
| Spring Cloud Circuit Breaker | Spring Cloud Abstraction | API thống nhất để tích hợp các implementation như Resilience4j |
1. Circuit Breaker là Design Pattern
Chính xác hơn, là Fault Tolerance Pattern trong distributed systems. Giống như Singleton là Creational pattern, Observer là Behavioral pattern. Nó mô tả cách giải quyết vấn đề, không quy định thư viện; bạn có thể tự viết bằng Java Core.
2. Resilience4j là thư viện Java
Resilience4j (Java Library)
|-- Circuit Breaker
|-- Retry
|-- Rate Limiter
|-- Bulkhead
|-- Time Limiter
|-- Cache
Không bắt buộc chạy trong Spring Boot.
3. Spring Cloud Circuit Breaker là abstraction
@Service
public class OrderService {
private final CircuitBreakerFactory<?, ?> cbFactory;
private final PaymentClient paymentClient;
public OrderService(CircuitBreakerFactory<?, ?> cbFactory, PaymentClient paymentClient) {
this.cbFactory = cbFactory;
this.paymentClient = paymentClient;
}
public PaymentResponse pay(PaymentRequest req) {
return cbFactory.create("paymentService")
.run(() -> paymentClient.createPayment(req),
ex -> new PaymentResponse("UNKNOWN", "Needs verification"));
}
}
// dependency: spring-cloud-starter-circuitbreaker-resilience4j
Code chỉ phụ thuộc API của Spring Cloud; bên dưới là Resilience4j (hoặc implementation khác).
Cách nhớ
- Circuit Breaker = Pattern (ý tưởng, cách giải quyết).
- Resilience4j = Library (công cụ hiện thực pattern).
- Spring Cloud Circuit Breaker = Abstraction (lớp tích hợp thống nhất).
Tương tự: Repository là pattern, Spring Data JPA là framework giúp triển khai truy cập dữ liệu theo hướng repository.
Payment phản hồi rất chậm nhưng vẫn trả HTTP 200 sau 20 giây, mình chỉ cấu hình failureRateThreshold = 50%, không timeout. Circuit chắc chắn OPEN đúng không?
Không. HTTP 200 được tính là success, nên chậm không tự làm Circuit OPEN. Trong lúc đó Tomcat thread vẫn bị chiếm 20 giây mỗi request, cascading failure vẫn xảy ra. Cần:
- Timeout (Feign
readTimeout: 3000) để biến chậm thành lỗi. slowCallDurationThreshold+slowCallRateThresholdđể Circuit đếm cả slow call.- Bulkhead để giới hạn số thread chờ Payment.
Circuit Breaker không có timeout mặc định 5 giây nào cả.
Phỏng vấn hỏi Circuit Breaker / Resilience4j thì trả lời sao?
Q1. Circuit Breaker là gì, có những trạng thái nào?
🇻🇳 Tiếng Việt:
Circuit Breaker là fault-tolerance pattern ngăn ứng dụng liên tục gọi một service đang lỗi, tránh cascading failure và cho service đó thời gian hồi phục. Có 3 trạng thái: CLOSED cho request đi qua và đếm lỗi; OPEN từ chối ngay, chạy fallback; HALF_OPEN cho vài request thử, thành công thì về CLOSED, lỗi thì OPEN lại.
🇬🇧 English:
A Circuit Breaker is a fault-tolerance pattern that prevents an application from repeatedly calling a failing service. It avoids cascading failures and gives the downstream service time to recover. It has three states: Closed allows requests and records failures; Open rejects calls immediately and uses a fallback; Half-Open lets a few trial calls through, closing again on success or reopening on failure.
Q2. Circuit Breaker và Resilience4j khác nhau thế nào?
🇻🇳 Tiếng Việt:
Circuit Breaker là design pattern. Resilience4j là thư viện Java nhẹ, độc lập với Spring, hiện thực các pattern như Circuit Breaker, Retry, Bulkhead, Rate Limiter, Time Limiter. Spring Cloud Circuit Breaker là lớp abstraction để dùng các implementation đó qua một API thống nhất.
🇬🇧 English:
Circuit Breaker is a fault-tolerance design pattern used in distributed systems. Resilience4j is a lightweight Java library, independent of Spring, that implements patterns such as Circuit Breaker, Retry, Bulkhead, Rate Limiter and Time Limiter. Spring Cloud Circuit Breaker is an abstraction that lets us use those implementations through one consistent API.
Q3. Retry, Timeout và Circuit Breaker khác nhau thế nào?
🇻🇳 Tiếng Việt:
Timeout giới hạn thời gian chờ một call. Retry thử lại lỗi tạm thời, chỉ nên dùng cho thao tác idempotent hoặc có idempotency key, nếu không có thể thanh toán trùng. Circuit Breaker tạm ngừng gọi dependency khi tỷ lệ lỗi vượt ngưỡng. Circuit Breaker không thay timeout: response 200 chậm vẫn tính là success nếu không cấu hình slow call.
🇬🇧 English:
Timeout limits how long a call can wait. Retry repeats a call after a transient failure and should only be used for idempotent operations or with an idempotency key, otherwise we risk double payments. Circuit Breaker temporarily stops calls when the failure rate exceeds a threshold. It does not replace timeouts: a slow 200 response still counts as success unless slow-call thresholds are configured.
Q4. Bulkhead là gì?
🇻🇳 Tiếng Việt:
Bulkhead là pattern cô lập tài nguyên: giới hạn số call đồng thời (semaphore) hoặc dùng thread pool riêng cho một dependency, để một service chậm không ăn hết thread của cả ứng dụng.
🇬🇧 English:
Bulkhead is a fault-isolation pattern. It limits concurrent calls with a semaphore, or uses a dedicated thread pool for one dependency, so a slow service cannot exhaust the resources of the whole application.
Cách nhớ ngắn gọn
- Pattern (Circuit Breaker) → Library (Resilience4j) → Abstraction (Spring Cloud Circuit Breaker).
- CLOSED → OPEN → HALF_OPEN → CLOSED/OPEN.
- Timeout trước, Circuit Breaker sau; 200 chậm không làm OPEN nếu thiếu slow call config.
- Retry chỉ cho idempotent; Bulkhead giữ thread cho dependency khác.
Tự kiểm tra
Circuit OPEN thì gọi method có chạy HTTP không?
Không. Ném CallNotPermittedException ngay và chạy fallback.
Vì sao gọi this.createPayment() trong cùng class thì @CircuitBreaker không có tác dụng?
Annotation hoạt động qua Spring AOP proxy; gọi nội bộ bỏ qua proxy.
minimumNumberOfCalls dùng để làm gì?
Chưa đủ số call tối thiểu thì không tính tỷ lệ lỗi, tránh OPEN chỉ vì 1–2 lỗi đầu tiên.
Vì sao fallback của thanh toán không nên trả "FAILED" cứng?
Timeout không có nghĩa là thất bại; tiền có thể đã bị trừ. Cần lưu trạng thái UNKNOWN và đối soát.
Resilience4j có phải thư viện của Spring không?
Không. Là thư viện Java độc lập, có module tích hợp Spring Boot.