Độ tin cậy Trung bình 5 phút đọc

Circuit breaker, retry, timeout: Polly trong ASP.NET Core

Cách cấu hình Polly và Microsoft.Extensions.Http.Resilience vào service .NET: retry với exponential backoff, circuit breaker, timeout, và bulkhead isolation.

Mục lục
  1. Khi nào thêm code resilience bắt đầu có giá trị?
  2. Ngân sách số nào cho resilience?
  3. Pipeline phân tầng trông thế nào?
  4. Cấu hình .NET 10 với Microsoft.Extensions.Http.Resilience?
  5. Circuit breaker thực sự quyết định mở ra sao?
  6. Polly tự nó tạo failure mode nào?
  7. Khi nào nên bỏ qua handler resilience?
  8. Đi tiếp đâu từ đây?

Lần đầu một service downstream chậm và service của bạn ngừng phản hồi vì mọi thread đang block chờ nó, bạn đã gặp ca Polly được thiết kế cho. Chương này hướng dẫn cấu hình bốn pattern resilience cốt lõi - timeout, retry, circuit breaker, bulkhead - vào ASP.NET Core theo cách sống sót outage không lường trước.

Khi nào thêm code resilience bắt đầu có giá trị?

Ba tín hiệu.

Service gọi service khác đồng bộ. HTTP client, gRPC client, API third-party. Mỗi cái có thể fail độc lập và service của bạn nên tiếp tục phản hồi cho phần còn lại của request.

Dependency có latency thay đổi. Third-party chậm thường đau hơn fail nhanh. Thread pool đầy waiter, request mới xếp hàng, cả service đổ. Timeout + bulkhead bảo vệ khỏi cái này.

Bạn có yêu cầu latency phía user. Budget p99 500 ms nghĩa downstream mất 30 giây để fail phải timeout nhanh hơn thế, retry một lần, rồi fallback. Polly điều phối tất cả bằng một pipeline.

Nếu network call duy nhất là tới Postgres trong cluster, resilience là tune connection pool, không phải Polly. Để Polly cho call ra ngoài mạng.

Ngân sách số nào cho resilience?

Pattern              Default                          Hiệu ứng
Timeout              500 ms - 5 s mỗi attempt         chặn 1 call
Retry                3 attempt, 200 ms-1.5 s backoff  xử lý lỗi tạm
Circuit breaker      mở sau 5/10 fail, 30 s           dừng cascading
Bulkhead             10-20 call đồng thời             chặn blast radius
Tổng budget          ~3x timeout 1 attempt            cận trên

Tổng wall-clock budget cho pipeline Polly xấp xỉ timeout 1 attempt nhân số retry (kèm backoff). Timeout 500 ms với 3 retry mất ~3 giây trong tệ nhất; đặt deadline service-level theo đó.

Pipeline phân tầng trông thế nào?

flowchart LR
    Req[HTTP request] --> Timeout[Timeout 5 s]
    Timeout --> Retry[Retry 3x backoff]
    Retry --> Breaker[Circuit breaker]
    Breaker --> Bulkhead[Bulkhead 20 concurrent]
    Bulkhead --> Downstream[Service downstream]
    Downstream --> Out[Response]

Ngoài vào trong: Timeout (chặn 1 attempt), Retry (xử lý lỗi tạm), Circuit Breaker (dừng gọi dependency chết), Bulkhead (giới hạn call đồng thời), rồi call thật. Mỗi tầng độc lập và cấu hình theo từng dependency.

Cấu hình .NET 10 với Microsoft.Extensions.Http.Resilience?

.NET hiện đại ship pipeline resilience chuẩn dưới dạng extension một dòng. Dây một lần mỗi HttpClient:

// Program.cs
builder.Services.AddHttpClient<IPaymentClient, PaymentClient>(c =>
{
    c.BaseAddress = new Uri(builder.Configuration["Payment:BaseUrl"]!);
    c.Timeout = TimeSpan.FromSeconds(10);
})
.AddStandardResilienceHandler(opt =>
{
    // Giá trị default cho rõ - chỉ override cái bạn đổi.
    opt.AttemptTimeout.Timeout = TimeSpan.FromSeconds(2);
    opt.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(10);
    opt.Retry.MaxRetryAttempts = 3;
    opt.Retry.BackoffType = DelayBackoffType.Exponential;
    opt.Retry.UseJitter = true;
    opt.CircuitBreaker.FailureRatio = 0.5;
    opt.CircuitBreaker.MinimumThroughput = 10;
    opt.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(30);
    opt.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(30);
});

// Sử dụng - không có code Polly ở chỗ gọi.
public class CheckoutService(IPaymentClient payment)
{
    public Task<PaymentResult> ChargeAsync(Order order, CancellationToken ct)
        => payment.ChargeAsync(order.Id, order.Amount, ct);  // resilience vô hình
}

Ba chi tiết. Handler chuẩn gói cả bốn pattern với default hợp lý. Cấu hình theo từng client, không global - service recommendation và service payment xứng đáng budget khác nhau. Handler emit metric OpenTelemetry để bạn thấy retry và break trong Grafana (chương 13).

Circuit breaker thực sự quyết định mở ra sao?

Ba state với ngưỡng:

stateDiagram-v2
    [*] --> Closed
    Closed --> Open: fail vượt ngưỡng
    Open --> HalfOpen: hết break duration
    HalfOpen --> Closed: probe thành công
    HalfOpen --> Open: probe fail

Ngưỡng quan trọng. Tỉ lệ default là "hơn 50% fail trong 30 giây với ít nhất 10 request". Hạ ngưỡng thì breaker mở vì trục trặc nhỏ; nâng thì breaker quá chậm. Default ổn cho phần lớn service; tune theo metric, không đoán.

Polly tự nó tạo failure mode nào?

Khi nào nên bỏ qua handler resilience?

Ba ca.

Một, dependency nội bộ tin được với replication đồng bộ và cùng failure domain - Postgres của chính bạn trong cùng VPC. Retry connection pool xử lý lỗi tạm; thêm Polly là code trùng.

Hai, best-effort fire-and-forget qua queue (chương 6). Queue đã retry sẵn; Polly phía publish là quá liều.

Ba, kiến trúc đã hỏng. Nếu call bạn đang bọc có hình sai - vòng lặp đồng bộ trong khi messaging async sẽ chạy - resilience che vấn đề thiết kế thay vì sửa.

Đi tiếp đâu từ đây?

Chương kế tiếp: pattern saga

Câu hỏi thường gặp

Retry trước hay circuit breaker trước?
Cả hai, nhưng đúng thứ tự. Pipeline chuẩn là timeout → retry → circuit breaker → request. Timeout chặn một lần thử; retry xử lý lần kế tiếp; circuit breaker dừng các lần thử khi dependency rõ ràng đang down. Đảo lại (breaker ngoài retry), breaker mở quá quá nhạy vì mỗi retry tính là một failure.
Exponential backoff thực sự đem lại gì?
Tránh thundering herd. Nếu 1000 client cùng retry call lỗi sau 100 ms, bạn có 1000 retry đồng thời đập dependency đang hồi phục cùng lúc và đánh sập lại. Exponential backoff (100 ms, 200 ms, 400 ms, 800 ms) cộng jitter (50% đến 150% delay random) trải retry theo thời gian.
Khi nào circuit breaker hại thay vì lợi?
Khi dependency là quan trọng và xuống cấp êm tệ hơn fail nhanh. Breaker trên auth service sẽ khoá mọi user khi service trục trặc; tốt hơn là retry mạnh. Breaker đúng cho dependency không quan trọng (recommendation, analytics, feed third-party) nơi 'không có câu trả lời' tốt hơn 'treo 5 giây'.
Khác gì health check của load balancer?
Khác tầng. Load balancer loại instance hỏng khỏi pool. Circuit breaker dừng call tới dependency từ một client. Cả hai đều cần: LB giữ instance khoẻ sau một IP, breaker giữ service của bạn còn phản hồi khi mọi instance đều chậm. Chúng lắp ghép.