Resilience Patterns trên .NET 10 — Polly, Circuit Breaker và Retry cho Microservices
Posted on: 4/17/2026 1:09:18 PM
Table of contents
- 1. Vì sao cần Resilience Patterns?
- 2. Polly v8 — Kiến trúc mới hoàn toàn
- 3. Tích hợp nhanh với AddStandardResilienceHandler
- 4. Circuit Breaker — Cơ chế cầu dao thông minh
- 5. Retry Pattern — Nghệ thuật thử lại đúng cách
- 6. Hedging — Song song hóa request để giảm latency
- 7. Timeout Strategy — Hai tầng bảo vệ
- 8. Kết hợp với OpenTelemetry
- 9. Dynamic Reload — Thay đổi cấu hình runtime
- 10. Thực hành tốt nhất cho Production
- 11. Ví dụ thực tế — E-commerce Resilient Architecture
- 12. Anti-patterns cần tránh
- Kết luận
Trong kiến trúc microservices, việc một service gọi đến service khác qua HTTP là chuyện thường ngày. Nhưng network không bao giờ đáng tin — timeout, server quá tải, DNS flap, hay đơn giản là deployment đang rolling update. Nếu không có chiến lược xử lý lỗi tạm thời (transient fault), một service chậm có thể kéo sập cả hệ thống theo hiệu ứng domino. Bài viết này đi sâu vào các Resilience Patterns trên .NET 10 với thư viện Polly và package Microsoft.Extensions.Http.Resilience — bộ công cụ chuẩn công nghiệp giúp ứng dụng tự phục hồi trước lỗi.
1. Vì sao cần Resilience Patterns?
Hãy tưởng tượng hệ thống e-commerce với 20 microservices. Service Order gọi Payment, Payment gọi Fraud Detection, Fraud Detection gọi ML Scoring. Khi ML Scoring chậm 10 giây thay vì 200ms bình thường, điều gì xảy ra?
graph LR
A[Order Service] -->|HTTP| B[Payment Service]
B -->|HTTP| C[Fraud Detection]
C -->|HTTP| D["ML Scoring (chậm ⚠️)"]
D -.->|10s timeout| C
C -.->|thread blocked| B
B -.->|thread pool cạn| A
A -.->|503 cho user| E[Client]
style D fill:#ff9800,stroke:#e65100,color:#fff
style A fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style B fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style C fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style E fill:#e94560,stroke:#fff,color:#fff
Cascading failure — một service chậm kéo sập toàn bộ chuỗi
Không có resilience patterns, thread pool của mỗi service bị chiếm bởi các request đang chờ downstream. Khi thread pool cạn, service không thể phục vụ bất kỳ request nào — kể cả những request không liên quan đến ML Scoring. Đây là cascading failure, và nó có thể đánh sập toàn bộ hệ thống trong vài phút.
Resilience ≠ Chỉ retry
Nhiều developer nghĩ resilience đơn giản là "thử lại khi lỗi". Thực tế, retry không đúng cách có thể khiến tình hình tệ hơn — hàng nghìn client đồng loạt retry tạo thành retry storm, đè bẹp service vốn đang quá tải. Resilience patterns là sự kết hợp thông minh giữa retry, circuit breaker, timeout, rate limiter và fallback.
2. Polly v8 — Kiến trúc mới hoàn toàn
Polly v8 (hiện tại trên .NET 10) đã được viết lại từ đầu với kiến trúc Resilience Pipeline — thay thế hoàn toàn API cũ dựa trên Policy. Pipeline cho phép xếp chồng nhiều strategy theo thứ tự xác định, và mỗi strategy hoạt động độc lập.
graph TB
subgraph Pipeline["Resilience Pipeline"]
direction TB
RL["1. Rate Limiter"] --> TT["2. Total Timeout (30s)"]
TT --> RT["3. Retry (3 lần, exponential)"]
RT --> CB["4. Circuit Breaker"]
CB --> AT["5. Attempt Timeout (10s)"]
end
REQ["HTTP Request"] --> RL
AT --> SVC["Downstream Service"]
style Pipeline fill:#f8f9fa,stroke:#e0e0e0
style REQ fill:#e94560,stroke:#fff,color:#fff
style SVC fill:#2c3e50,stroke:#fff,color:#fff
style RL fill:#fff,stroke:#e94560,color:#2c3e50
style TT fill:#fff,stroke:#e94560,color:#2c3e50
style RT fill:#fff,stroke:#e94560,color:#2c3e50
style CB fill:#fff,stroke:#e94560,color:#2c3e50
style AT fill:#fff,stroke:#e94560,color:#2c3e50
Thứ tự 5 strategy trong Standard Resilience Pipeline (outermost → innermost)
3. Tích hợp nhanh với AddStandardResilienceHandler
Cách nhanh nhất để thêm resilience vào HttpClient trên .NET 10 là dùng AddStandardResilienceHandler() từ package Microsoft.Extensions.Http.Resilience. Chỉ một dòng code, bạn có ngay 5 layers bảo vệ với cấu hình mặc định đã được tinh chỉnh cho production.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
// Đăng ký HttpClient với standard resilience pipeline
builder.Services
.AddHttpClient<PaymentClient>(client =>
{
client.BaseAddress = new Uri("https://payment-api.internal");
})
.AddStandardResilienceHandler();
var app = builder.Build();
await app.RunAsync();
Với cấu hình mặc định, pipeline sẽ:
| Thứ tự | Strategy | Mặc định | Tác dụng |
|---|---|---|---|
| 1 | Rate Limiter | 1.000 concurrent permits | Ngăn client gửi quá nhiều request đồng thời |
| 2 | Total Timeout | 30 giây | Giới hạn tổng thời gian bao gồm retry |
| 3 | Retry | 3 lần, exponential backoff + jitter | Tự động thử lại khi gặp lỗi tạm thời |
| 4 | Circuit Breaker | 10% failure ratio, 100 min throughput | Ngắt mạch khi downstream liên tục lỗi |
| 5 | Attempt Timeout | 10 giây | Giới hạn thời gian từng request đơn lẻ |
Tại sao có 2 timeout?
Attempt Timeout (10s) giới hạn từng lần thử — nếu 1 request quá 10s, nó bị cancel để nhường chỗ cho retry tiếp theo. Total Timeout (30s) là "ngân sách thời gian" tổng — dù retry bao nhiêu lần, tổng thời gian không vượt 30s. Thiếu total timeout, 3 lần retry × 10s = 30s + backoff delay có thể kéo đến > 40s.
4. Circuit Breaker — Cơ chế cầu dao thông minh
Circuit Breaker là pattern quan trọng nhất trong resilience. Thay vì cứ gửi request đến service đang lỗi (gây thêm tải), circuit breaker tự động "ngắt mạch" và fail ngay lập tức — giống cầu dao điện ngắt khi quá tải để bảo vệ thiết bị.
stateDiagram-v2
[*] --> Closed
Closed --> Open : Failure ratio vượt ngưỡng
Open --> HalfOpen : Sau break duration
HalfOpen --> Closed : Probe request thành công
HalfOpen --> Open : Probe request thất bại
Open --> Isolated : Manual isolate
Isolated --> Closed : Manual close
note right of Closed : Request đi qua bình thường\nĐang sampling failure ratio
note right of Open : Request bị reject ngay\nThrow BrokenCircuitException
note right of HalfOpen : Cho 1 request qua để test\nQuyết định đóng/mở lại
State machine của Circuit Breaker — 4 trạng thái
4.1. Cấu hình Circuit Breaker chi tiết
builder.Services
.AddHttpClient<PaymentClient>(client =>
{
client.BaseAddress = new Uri("https://payment-api.internal");
})
.AddResilienceHandler("PaymentPipeline", static pipelineBuilder =>
{
// Circuit Breaker: ngắt mạch khi 20% request thất bại
pipelineBuilder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
{
FailureRatio = 0.2, // 20% failure → mở circuit
SamplingDuration = TimeSpan.FromSeconds(10), // Cửa sổ sampling 10 giây
MinimumThroughput = 8, // Cần ít nhất 8 request để tính
BreakDuration = TimeSpan.FromSeconds(30), // Mở circuit trong 30s
ShouldHandle = static args => ValueTask.FromResult(args is
{
Outcome.Result.StatusCode:
HttpStatusCode.RequestTimeout or
HttpStatusCode.TooManyRequests or
HttpStatusCode.InternalServerError or
HttpStatusCode.ServiceUnavailable
})
});
// Retry: 5 lần với exponential backoff
pipelineBuilder.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 5,
BackoffType = DelayBackoffType.Exponential,
Delay = TimeSpan.FromMilliseconds(500),
UseJitter = true
});
// Timeout: 5 giây cho mỗi attempt
pipelineBuilder.AddTimeout(TimeSpan.FromSeconds(5));
});
Cẩn thận với MinimumThroughput
Nếu MinimumThroughput chưa đạt trong SamplingDuration, circuit breaker sẽ bỏ qua failure ratio. Điều này ngăn circuit mở khi traffic thấp (ví dụ chỉ 2 request lỗi trong 10s nhưng tổng cũng chỉ có 2 request). Đặt giá trị phù hợp với traffic thực tế của service.
4.2. Dynamic Break Duration
Thay vì break cố định 30s, bạn có thể tăng dần thời gian break theo số lần circuit mở liên tiếp — giống exponential backoff nhưng ở cấp circuit breaker:
pipelineBuilder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
{
BreakDurationGenerator = static args =>
{
// Lần 1: 15s, lần 2: 30s, lần 3: 60s, tối đa 120s
var duration = TimeSpan.FromSeconds(
Math.Min(15 * Math.Pow(2, args.FailureCount - 1), 120));
return ValueTask.FromResult(duration);
}
});
5. Retry Pattern — Nghệ thuật thử lại đúng cách
Retry nghe đơn giản nhưng sai cách có thể gây thảm họa. Ba nguyên tắc vàng:
5.1. Exponential Backoff + Jitter
Không jitter, 1.000 client retry cùng lúc tại giây thứ 1, 2, 4, 8 — tạo traffic spike tuần hoàn. Jitter phân tán các retry ngẫu nhiên, giảm áp lực lên server.
graph LR
subgraph Without_Jitter["Không có Jitter"]
A1["t=1s: 1000 req"] --> A2["t=2s: 1000 req"] --> A3["t=4s: 1000 req"]
end
subgraph With_Jitter["Có Jitter"]
B1["t=0.8-1.2s: ~330 req"] --> B2["t=1.6-2.4s: ~330 req"] --> B3["t=3.2-4.8s: ~340 req"]
end
style Without_Jitter fill:#ffebee,stroke:#c62828
style With_Jitter fill:#e8f5e9,stroke:#2e7d32
style A1 fill:#ffcdd2,stroke:#c62828,color:#2c3e50
style A2 fill:#ffcdd2,stroke:#c62828,color:#2c3e50
style A3 fill:#ffcdd2,stroke:#c62828,color:#2c3e50
style B1 fill:#c8e6c9,stroke:#2e7d32,color:#2c3e50
style B2 fill:#c8e6c9,stroke:#2e7d32,color:#2c3e50
style B3 fill:#c8e6c9,stroke:#2e7d32,color:#2c3e50
Jitter phân tán retry requests, tránh retry storm
5.2. Disable Retry cho Unsafe Methods
POST tạo đơn hàng, nếu retry 3 lần có thể tạo 3 đơn hàng. .NET 10 cung cấp API rõ ràng để xử lý:
builder.Services
.AddHttpClient<OrderClient>()
.AddStandardResilienceHandler(options =>
{
// Tắt retry cho POST, PUT, DELETE — chỉ retry GET/HEAD
options.Retry.DisableForUnsafeHttpMethods();
});
Khi nào ĐƯỢC retry POST?
Nếu downstream API hỗ trợ idempotency key (ví dụ Stripe, PayPal), bạn có thể retry POST an toàn vì server sẽ deduplicate dựa trên key. Trong trường hợp đó, đừng disable retry cho POST mà hãy đảm bảo mỗi request mang header Idempotency-Key duy nhất.
6. Hedging — Song song hóa request để giảm latency
Hedging là strategy nâng cao: khi request đầu tiên chậm, gửi thêm request song song đến endpoint khác (hoặc cùng endpoint), lấy response nào về trước. Đặc biệt hữu ích khi bạn có nhiều replica hoặc multi-region deployment.
builder.Services
.AddHttpClient<SearchClient>()
.AddStandardHedgingHandler(routingBuilder =>
{
routingBuilder.ConfigureWeightedGroups(options =>
{
options.SelectionMode = WeightedGroupSelectionMode.EveryAttempt;
options.Groups.Add(new WeightedUriEndpointGroup
{
Endpoints =
{
new() { Uri = new("https://search-primary.internal"), Weight = 70 },
new() { Uri = new("https://search-secondary.internal"), Weight = 30 }
}
});
});
});
| Đặc điểm | Retry | Hedging |
|---|---|---|
| Khi nào gửi request tiếp? | Sau khi request trước thất bại | Sau delay (mặc định 2s), bất kể request trước |
| Số request đồng thời | 1 | Nhiều (tối đa 10 mặc định) |
| Use case chính | Lỗi tạm thời, server trả 500/408/429 | Tail latency cao, multi-region, read-heavy |
| Chi phí | Thấp — request tuần tự | Cao hơn — nhiều request song song tiêu tốn tài nguyên |
7. Timeout Strategy — Hai tầng bảo vệ
Timeout nghe đơn giản nhưng là một trong những bug phổ biến nhất. Không timeout → thread bị block vô hạn. Timeout quá ngắn → request hợp lệ bị cancel. .NET 10 + Polly giải quyết bằng 2 tầng timeout:
builder.Services
.AddHttpClient<ReportClient>()
.AddResilienceHandler("ReportPipeline", builder =>
{
// Total timeout: toàn bộ operation (bao gồm retry) tối đa 60s
builder.AddTimeout(new TimeoutStrategyOptions
{
Timeout = TimeSpan.FromSeconds(60),
Name = "TotalTimeout"
});
// Retry 3 lần
builder.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential,
Delay = TimeSpan.FromSeconds(1)
});
// Per-attempt timeout: mỗi request đơn lẻ tối đa 15s
builder.AddTimeout(new TimeoutStrategyOptions
{
Timeout = TimeSpan.FromSeconds(15),
Name = "AttemptTimeout"
});
});
Thứ tự quan trọng!
Polly áp dụng strategy từ ngoài vào trong. Total Timeout phải ở ngoài cùng (trước Retry), Attempt Timeout ở trong cùng (sau Retry). Nếu đặt ngược, total timeout sẽ chỉ áp dụng cho 1 attempt thay vì toàn bộ operation.
8. Kết hợp với OpenTelemetry
Resilience patterns không có ý nghĩa nếu bạn không biết chúng đang hoạt động thế nào. Polly v8 tích hợp sẵn với OpenTelemetry, export metrics và traces cho mọi strategy.
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics.AddMeter("Polly"); // Polly metrics: retry count, circuit state, etc.
metrics.AddMeter("System.Net.Http"); // HttpClient metrics
metrics.AddPrometheusExporter();
})
.WithTracing(tracing =>
{
tracing.AddHttpClientInstrumentation();
tracing.AddOtlpExporter();
});
Các metrics quan trọng từ Polly:
| Metric | Ý nghĩa | Alert khi |
|---|---|---|
polly_strategy_attempt_count |
Tổng số lần thực thi (bao gồm retry) | Tăng đột biến → downstream có vấn đề |
polly_strategy_attempt_duration |
Thời gian từng attempt | p99 > attempt timeout |
polly_circuit_breaker_state |
Trạng thái circuit (0=Closed, 1=Open, 2=HalfOpen) | State = 1 kéo dài > 5 phút |
polly_rate_limiter_queue_duration |
Thời gian request chờ trong rate limiter queue | > 500ms |
9. Dynamic Reload — Thay đổi cấu hình runtime
Một tính năng mạnh mẽ của Polly trên .NET 10: bạn có thể thay đổi cấu hình resilience mà không cần restart ứng dụng. Cấu hình được bind từ appsettings.json và tự động reload khi file thay đổi.
{
"PaymentResilience": {
"Retry": {
"BackoffType": "Exponential",
"UseJitter": true,
"MaxRetryAttempts": 3,
"Delay": "00:00:01"
},
"CircuitBreaker": {
"FailureRatio": 0.1,
"SamplingDuration": "00:00:30",
"MinimumThroughput": 100,
"BreakDuration": "00:00:05"
},
"TotalRequestTimeout": {
"Timeout": "00:00:30"
}
}
}
var resilienceSection = builder.Configuration.GetSection("PaymentResilience");
builder.Services.Configure<HttpStandardResilienceOptions>(resilienceSection);
builder.Services
.AddHttpClient<PaymentClient>()
.AddStandardResilienceHandler();
Khi ops team cần tăng retry từ 3 lên 5, hoặc nới timeout từ 30s lên 60s, họ chỉ cần update config và deploy — không cần dev thay đổi code, không cần rebuild.
10. Thực hành tốt nhất cho Production
AddResilienceHandler để tách biệt — circuit breaker của Payment không ảnh hưởng đến Inventory.DisableForUnsafeHttpMethods() là cách an toàn nhất.AddStandardResilienceHandler() với AddResilienceHandler() — dùng RemoveAllResilienceHandlers() nếu cần custom pipeline thay thế standard.11. Ví dụ thực tế — E-commerce Resilient Architecture
Dưới đây là kiến trúc resilience hoàn chỉnh cho một hệ thống e-commerce với 3 downstream services, mỗi service có pipeline riêng phù hợp với đặc tính traffic:
var builder = WebApplication.CreateBuilder(args);
// Payment: critical, không retry POST, circuit breaker chặt
builder.Services
.AddHttpClient<PaymentClient>(c => c.BaseAddress = new("https://payment.internal"))
.AddResilienceHandler("Payment", pipeline =>
{
pipeline.AddTimeout(TimeSpan.FromSeconds(20));
pipeline.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 2,
BackoffType = DelayBackoffType.Exponential,
Delay = TimeSpan.FromMilliseconds(500),
UseJitter = true,
DisableFor = [HttpMethod.Post] // Không retry payment POST
});
pipeline.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
{
FailureRatio = 0.15,
MinimumThroughput = 20,
SamplingDuration = TimeSpan.FromSeconds(15),
BreakDuration = TimeSpan.FromSeconds(60)
});
pipeline.AddTimeout(TimeSpan.FromSeconds(8));
});
// Inventory: read-heavy, có thể retry thoải mái
builder.Services
.AddHttpClient<InventoryClient>(c => c.BaseAddress = new("https://inventory.internal"))
.AddStandardResilienceHandler();
// Notification: non-critical, timeout ngắn, fail silently
builder.Services
.AddHttpClient<NotificationClient>(c => c.BaseAddress = new("https://notify.internal"))
.AddResilienceHandler("Notification", pipeline =>
{
pipeline.AddTimeout(TimeSpan.FromSeconds(5));
pipeline.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 1,
Delay = TimeSpan.FromMilliseconds(200)
});
pipeline.AddTimeout(TimeSpan.FromSeconds(3));
});
Fallback cho Notification
Notification là non-critical — nếu gửi email thất bại, đơn hàng vẫn phải thành công. Trong thực tế, bạn nên wrap notification call trong try-catch và log lỗi thay vì để exception bubble up. Hoặc tốt hơn: đẩy notification vào message queue (RabbitMQ, Azure Service Bus) để xử lý async.
12. Anti-patterns cần tránh
| Anti-pattern | Vấn đề | Giải pháp |
|---|---|---|
| Retry without backoff | Retry ngay lập tức → retry storm đè bẹp server | Luôn dùng exponential backoff + jitter |
| Retry mọi thứ | Retry 404, 401 — những lỗi không bao giờ tự khỏi | Chỉ retry 408, 429, 500, 502, 503, TimeoutException |
| Shared circuit breaker | 1 circuit breaker cho mọi downstream → Payment lỗi ngắt luôn Inventory | Mỗi downstream service có named HttpClient + circuit riêng |
| Check circuit state trước Execute | Race condition + ngăn HalfOpen transition | Luôn gọi Execute, để Polly xử lý state |
| Quá nhiều retry | 10 retry × 5s timeout = 50s — user đã rời đi từ lâu | Tối đa 3-5 retry, total timeout ≤ SLA |
Kết luận
Resilience patterns không phải thứ "nice-to-have" — chúng là yêu cầu bắt buộc cho bất kỳ hệ thống microservices nào chạy production. Với Polly v8 và Microsoft.Extensions.Http.Resilience trên .NET 10, việc tích hợp đã đơn giản hơn bao giờ hết — chỉ 1 dòng AddStandardResilienceHandler() cho 90% use case, và full customization cho 10% còn lại. Kết hợp với OpenTelemetry để monitor, bạn có một hệ thống tự phục hồi, observable, và sẵn sàng cho mọi sự cố.
Tài liệu tham khảo:
Multi-Tenancy trên .NET 10 — Chiến lược Cô lập Dữ liệu, Row-Level Security và Caching cho SaaS Production 2026
AWS Lambda Serverless 2026: Kiến trúc, SnapStart, Event-Driven Patterns và Free Tier cho Production
Disclaimer: The opinions expressed in this blog are solely my own and do not reflect the views or opinions of my employer or any affiliated organizations. The content provided is for informational and educational purposes only and should not be taken as professional advice. While I strive to provide accurate and up-to-date information, I make no warranties or guarantees about the completeness, reliability, or accuracy of the content. Readers are encouraged to verify the information and seek independent advice as needed. I disclaim any liability for decisions or actions taken based on the content of this blog.