Chuyển đến nội dung chính

第 18 課:斷路器和重試模式

斷路器狀態(閉合、開啟、半開)、使用指數退避和抖動重試、逾時模式、回退策略、使用 Resilience4j/Polly 實作、級聯故障預防。

🏗️ 建築 — 第 18 課 第 18 課:斷路器和重試模式

雲端原生微服務架構

第 6 部分:彈性模式

亞洲開發網

第 18 課:斷路器和重試模式

簡介

在微服務中,失敗是正常的-服務會逾時、崩潰、過載。問題不是「會失敗嗎?」但是「如何處理失敗?」。

如果沒有保護機制,失敗的服務可能會透過級聯故障導致整個系統癱瘓。本文介紹了建構彈性系統的基本模式。


1. 級聯故障-核心問題

1.1 典型級聯故障場景

Normal flow:
API Gateway → Order Service → Payment Service → DB

Payment Service chậm (DB overloaded):
        ↓
Order Service gọi Payment, chờ... timeout sau 30s
Threads của Order Service bị block 30s mỗi request
        ↓
New requests vào Order Service không có thread để xử lý
Order Service trở nên không phản hồi
        ↓
API Gateway không nhận response từ Order Service
API Gateway threads bị block
        ↓
Toàn bộ hệ thống đóng băng

發生級聯故障的原因:呼叫失敗的下游服務所佔用的資源(執行緒、連線、記憶體)。

1.2 整體解決方案

┌─────────────────────────────────────────────────────┐
│              Resiliency Patterns                    │
│                                                     │
│  Timeout         — Không chờ vô thời hạn            │
│  Retry           — Thử lại khi lỗi tạm thời         │
│  Circuit Breaker — Ngừng gọi khi downstream lỗi    │
│  Bulkhead        — Cô lập resources theo group      │
│  Fallback        — Trả về giá trị mặc định          │
└─────────────────────────────────────────────────────┘

2. 逾時模式

2.1 所有網路呼叫逾時

規則:每個出站網路呼叫都必須有逾時時間。

// Không có timeout — NGUY HIỂM
HttpClient client = HttpClient.newHttpClient();
HttpResponse resp = client.send(request, BodyHandlers.ofString());

// Có timeout — ĐÚNG
HttpClient client = HttpClient.newBuilder()
    .connectTimeout(Duration.ofSeconds(2))
    .build();

HttpResponse resp = client.send(
    request.timeout(Duration.ofSeconds(5)),
    BodyHandlers.ofString()
);

2.2 逾時層次結構

User Request Timeout:     30s  (tổng budget cho request)
API Gateway Timeout:      25s  (dư 5s buffer)
Service Call Timeout:     10s  (mỗi downstream call)
DB Query Timeout:          5s
Cache Timeout:             1s

Luật: Mỗi layer phải có timeout nhỏ hơn layer cha
để không bao giờ "child đã timeout trước khi parent biết"

2.3 我應該設定多少超時?

Phân tích baseline:
1. Đo p99 latency của operation khi bình thường: 150ms
2. Thêm buffer: 150ms × 3 = 450ms
3. Set timeout: 500ms

Không đặt quá lớn: timeout = 30s → thread bị block 30s
Không đặt quá nhỏ: timeout = 50ms → false positives khi GC pause

3. 重試模式

3.1 我該什麼時候重試?

Nên retry:
✅ Network timeout (tạm thời)
✅ 503 Service Unavailable
✅ 429 Too Many Requests
✅ Connection reset

Không retry:
❌ 400 Bad Request (lỗi input, retry cũng vô ích)
❌ 401/403 Unauthorized (cần fix auth, không phải retry)
❌ 404 Not Found
❌ Operation gây side effects (đừng retry POST payment!)

3.2 指數退避

Không có backoff — THUNDERING HERD:
  Attempt 1: T+0ms   → Fail
  Attempt 2: T+0ms   → Fail (tất cả clients retry cùng lúc → server càng quá tải)
  Attempt 3: T+0ms   → Fail

Exponential Backoff:
  Attempt 1: T+0ms      → Fail
  Attempt 2: T+100ms    → Fail
  Attempt 3: T+400ms    → Fail
  Attempt 4: T+1600ms   → Success ✓

  delay(n) = baseDelay × 2^(n-1)

3.3 抖動-重試分散

Không có Jitter — vẫn thundering herd:
  10.000 clients đều retry tại T+100ms → spike

Với Jitter (Full Jitter):
  delay(n) = random(0, baseDelay × 2^(n-1))
  Clients retry tại các thời điểm khác nhau → hệ thống phục hồi dần

3.4 使用 Resilience4j 進行部署 (Java)

// Config
RetryConfig retryConfig = RetryConfig.custom()
    .maxAttempts(4)
    .waitDuration(Duration.ofMillis(100))
    .intervalFunction(IntervalFunction.ofExponentialRandomBackoff(
        Duration.ofMillis(100),  // initial delay
        2.0,                     // multiplier
        Duration.ofSeconds(10)   // max delay
    ))
    // Chỉ retry với các exception phù hợp
    .retryOnException(e ->
        e instanceof ConnectTimeoutException ||
        (e instanceof HttpStatusCodeException se && se.getStatusCode() == 503)
    )
    // Không retry nếu đây là non-retryable error
    .ignoreExceptions(BadRequestException.class, UnauthorizedException.class)
    .build();

RetryRegistry registry = RetryRegistry.of(retryConfig);
Retry retry = registry.retry("payment-service");

// Sử dụng
Supplier<PaymentResponse> supplier = Retry.decorateSupplier(
    retry,
    () -> paymentClient.charge(request)
);

// Log retry attempts
retry.getEventPublisher().onRetry(event ->
    log.warn("Retry attempt {} for payment-service: {}",
        event.getNumberOfRetryAttempts(),
        event.getLastThrowable().getMessage())
);

4. 斷路器模式

4.1 三種狀態

  ┌─────────────────────────────────────────────────────────────────┐
  │                                                                 │
  │         CLOSED                                                  │
  │    (Normal operation)                                           │
  │    - Requests flow through                                      │
  │    - Count failures                                             │
  │         │                                                       │
  │         │ failures > threshold (e.g., 50% in 10s window)       │
  │         ▼                                                       │
  │          OPEN                                                   │
  │    (Fail fast)                                                   │
  │    - Requests rejected immediately                              │
  │    - Return fallback response                                   │
  │    - No calls to downstream                                     │
  │         │                                                       │
  │         │ after wait duration (e.g., 30s)                      │
  │         ▼                                                       │
  │       HALF-OPEN                                                 │
  │    (Testing recovery)                                           │
  │    - Allow N probe requests through                             │
  │    - If success → CLOSED                                        │
  │    - If failure → OPEN again                                    │
  │                                                                 │
  └─────────────────────────────────────────────────────────────────┘

4.2 使用 Resilience4j 實作

CircuitBreakerConfig cbConfig = CircuitBreakerConfig.custom()
    // Sliding window: đánh giá trên 10 calls gần nhất
    .slidingWindowType(SlidingWindowType.COUNT_BASED)
    .slidingWindowSize(10)
    // Mở circuit khi > 50% calls fail
    .failureRateThreshold(50)
    // Mở circuit khi > 50% calls slow (> 2s)
    .slowCallRateThreshold(50)
    .slowCallDurationThreshold(Duration.ofSeconds(2))
    // Ở trạng thái OPEN trong 30s trước khi thử lại
    .waitDurationInOpenState(Duration.ofSeconds(30))
    // Cho 5 probe requests khi HALF-OPEN
    .permittedNumberOfCallsInHalfOpenState(5)
    // Cần ít nhất 10 calls trước khi đánh giá
    .minimumNumberOfCalls(10)
    .build();

CircuitBreakerRegistry registry = CircuitBreakerRegistry.of(cbConfig);
CircuitBreaker cb = registry.circuitBreaker("payment-service");

// Kết hợp Circuit Breaker + Retry + Timeout
CircuitBreaker circuitBreaker = registry.circuitBreaker("payment-service");
TimeLimiter timeLimiter = TimeLimiter.of(Duration.ofSeconds(2));
Retry retry = RetryRegistry.ofDefaults().retry("payment-service");

// Thứ tự quan trọng: Retry → CircuitBreaker → TimeLimiter
Supplier<CompletableFuture<PaymentResponse>> supplier = TimeLimiter
    .decorateFutureSupplier(timeLimiter,
        CircuitBreaker.decorateSupplier(circuitBreaker,
            () -> CompletableFuture.supplyAsync(
                () -> paymentClient.charge(request)
            )
        )
    );

// Fallback
Try<PaymentResponse> result = Try.ofSupplier(
    Retry.decorateSupplier(retry, supplier.get()::get)
).recover(throwable -> {
    log.error("Payment service unavailable, using fallback", throwable);
    return PaymentResponse.pending(request.getOrderId());
});

4.3 基於註解(更簡單)

@Service
public class PaymentServiceClient {

    @CircuitBreaker(name = "payment-service", fallbackMethod = "paymentFallback")
    @Retry(name = "payment-service")
    @TimeLimiter(name = "payment-service")
    public CompletableFuture<PaymentResponse> charge(ChargeRequest request) {
        return CompletableFuture.supplyAsync(
            () -> paymentClient.post("/charge", request, PaymentResponse.class)
        );
    }

    // Fallback phải có cùng signature + Throwable parameter
    public CompletableFuture<PaymentResponse> paymentFallback(
            ChargeRequest request, CallNotPermittedException e) {
        log.warn("Circuit OPEN for payment-service, returning pending response");
        return CompletableFuture.completedFuture(
            PaymentResponse.pending(request.getOrderId())
        );
    }

    public CompletableFuture<PaymentResponse> paymentFallback(
            ChargeRequest request, TimeoutException e) {
        log.error("Payment service timeout", e);
        return CompletableFuture.completedFuture(
            PaymentResponse.timeout(request.getOrderId())
        );
    }
}
# application.yml
resilience4j:
  circuitbreaker:
    instances:
      payment-service:
        sliding-window-size: 10
        failure-rate-threshold: 50
        wait-duration-in-open-state: 30s
        permitted-number-of-calls-in-half-open-state: 5
        minimum-number-of-calls: 10
        register-health-indicator: true  # Expose vào /actuator/health
  retry:
    instances:
      payment-service:
        max-attempts: 3
        wait-duration: 100ms
        exponential-backoff-multiplier: 2
        enable-exponential-backoff: true
  timelimiter:
    instances:
      payment-service:
        timeout-duration: 2s

5. 後備策略

5.1 後備類型

1. Static Fallback
   → Trả về hardcoded giá trị mặc định
   → Ví dụ: Recommendation service down → trả top 10 sản phẩm best-seller cố định

2. Cache Fallback
   → Trả về data từ cache (có thể stale)
   → Ví dụ: Product catalog down → trả data từ Redis cache (refresh mỗi 1 phút)

3. Graceful Degradation
   → Disable tính năng, hiển thị thông báo
   → Ví dụ: Review service down → ẩn phần reviews, vẫn cho phép đặt hàng

4. Pending/Async Fallback
   → Nhận request, lưu vào queue xử lý sau
   → Ví dụ: Notification service down → lưu vào database, retry sau

5.2 實際例子

@Service
public class ProductRecommendationService {

    @CircuitBreaker(name = "recommendation", fallbackMethod = "getCachedRecommendations")
    public List<Product> getRecommendations(String userId) {
        return recommendationEngine.getPersonalized(userId);
    }

    // Cache fallback — trả data cũ hơn thay vì lỗi
    public List<Product> getCachedRecommendations(String userId, Exception e) {
        log.warn("Recommendation engine unavailable, using cached data");

        List<Product> cached = cache.get("recommendations:" + userId);
        if (cached != null) return cached;

        // Nếu không có cache cá nhân hóa → dùng best-sellers
        return cache.get("best-sellers:top-10");
    }
}

6. 測試彈性

6.1 使用 Resilience4j 進行單元測試

@Test
void whenPaymentServiceDown_shouldRetryAndFallback() {
    // Mock payment service liên tục fail
    when(paymentClient.charge(any()))
        .thenThrow(new ConnectTimeoutException("Connection refused"))
        .thenThrow(new ConnectTimeoutException("Connection refused"))
        .thenReturn(PaymentResponse.success());

    PaymentResponse result = subject.charge(request);

    // Verify retry happened
    verify(paymentClient, times(3)).charge(any());

    // Verify result is correct
    assertThat(result.getStatus()).isEqualTo("SUCCESS");
}

@Test
void whenCircuitIsOpen_shouldReturnFallbackImmediately() {
    // Force circuit OPEN
    CircuitBreaker cb = CircuitBreakerRegistry.ofDefaults()
        .circuitBreaker("payment-service");
    cb.transitionToOpenState();

    long start = System.currentTimeMillis();
    PaymentResponse result = subject.charge(request);
    long duration = System.currentTimeMillis() - start;

    // Phải return ngay lập tức (fail fast)
    assertThat(duration).isLessThan(50);
    // Phải là fallback response
    assertThat(result.getStatus()).isEqualTo("PENDING");
}

7. 監控斷路器

透過 Prometheus 公開斷路器狀態:

# Spring Boot Actuator + Micrometer tự động export
# Metrics có sẵn:
resilience4j_circuitbreaker_state{name="payment-service"}  # 0=CLOSED, 1=OPEN, 2=HALF_OPEN
resilience4j_circuitbreaker_calls_total{name, kind, result}
resilience4j_circuitbreaker_failure_rate{name}
# Alert khi circuit OPEN
resilience4j_circuitbreaker_state{name="payment-service"} == 1
# Grafana panel: Circuit Breaker Status
- CLOSED: ✅ Xanh lá
- HALF-OPEN: ⚠️ Vàng
- OPEN: 🔴 Đỏ + Alert

總結

圖案問題已解決何時使用
逾時線程飢餓所有網絡通話
重試+退避+抖動瞬時故障冪等操作
斷路器級聯故障外部服務電話
後備體驗降級失敗無法避免時

套用順序:逾時→重試→斷路器→回退

下一篇文章:艙壁、速率限制和健康檢查模式