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
- Khi nào thêm code resilience bắt đầu có giá trị?
- Ngân sách số nào cho resilience?
- Pipeline phân tầng trông thế nào?
- Cấu hình .NET 10 với Microsoft.Extensions.Http.Resilience?
- Circuit breaker thực sự quyết định mở ra sao?
- Polly tự nó tạo failure mode nào?
- Khi nào nên bỏ qua handler resilience?
- Đ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
- Closed - traffic bình thường, đếm fail.
- Open - mọi request fail nhanh trong break duration; không call nào tới dependency.
- Half-open - một probe đi qua; thành công đóng breaker, fail mở lại.
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?
- Bão retry - retry đập dependency trong khi nó đang phục
hồi, đánh sập lại. Phòng: jitter, exponential backoff, và
MaxRetryAttemptsnhỏ (3 là đủ). - Retry trên call không idempotent -
POST /charge-cardretry hai lần trừ tiền hai lần. Phòng: chỉ retry thao tác idempotent, và ghép mọi retry với idempotency key chương 10. - Circuit breaker giấu - breaker đang mở mà bạn không biết.
Phòng: alert trên
circuit_breaker_state == open; emit log khi state đổi. - Bulkhead chật quá - cap concurrent dưới traffic bình thường, bóp request khoẻ. Phòng: size bulkhead bằng 2x đỉnh concurrent dưới tải bình thường.
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
- reliability ở mức workflow nghiệp vụ nhiều bước, nơi retry và circuit breaker trên call lẻ không đủ. Saga gắn outbox (chương 10) và handler resilience thành một hình nhất quán.