Circuit Breaker + Resilience4j: chặn lỗi lan truyền

4 câu hỏi~25 phútTừ khóa: cascading failure, CLOSED/OPEN/HALF_OPEN, fallback, Retry, TimeLimiter, Bulkhead, idempotency

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

2. Vấn đề: Cascading Failure

Order Service
     |
     v
Payment Service
     |
     X  DOWN / chậm 30s

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:

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: 10Theo dõi tối đa 10 call gần nhất
minimumNumberOfCalls: 10Cần ít nhất 10 call mới đánh giá tỷ lệ lỗi
failureRateThreshold: 50Tỷ lệ lỗi ≥ 50% thì OPEN
slowCallDurationThresholdCall > 2s bị tính là slow call
waitDurationInOpenState: 10sChờ 10 giây trước khi thử phục hồi
permittedNumberOfCallsInHalfOpenState: 3Cho 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));
    }
}

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 / TimeLimiterKhông chờ quá lâuDừng chờ khi vượt giới hạn
RetryLỗi tạm thờiThử lại lỗi được phép retry
Circuit BreakerDependency lỗi kéo dàiNgừng gọi khi tỷ lệ lỗi cao
BulkheadMột dependency ăn hết tài nguyênGiới hạn số call đồng thời
Rate LimiterQuá nhiều requestGiới hạn tốc độ request
FallbackTrải nghiệm khi lỗiTrả 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

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ệmBản chấtVai trò
Circuit BreakerDesign PatternNgăn ứng dụng liên tục gọi một service đang gặp lỗi
Resilience4jJava LibraryCung cấp implementation của Circuit Breaker, Retry, Bulkhead, RateLimiter...
Spring Cloud Circuit BreakerSpring Cloud AbstractionAPI 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ớ

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:

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


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.