Resilience Patterns trên .NET 10 — Polly, Circuit Breaker và Retry cho Microservices

Posted on: 4/17/2026 1:09:18 PM

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.

350M+ Lượt tải Polly trên NuGet
5 layers Standard Resilience Pipeline
v10.4 Microsoft.Extensions.Http.Resilience
< 3ms Overhead trung bình mỗi request

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.

Program.cs
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

PaymentClientResilience.cs
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:

Exponential Backoff — tăng dần khoảng cách giữa các lần retry
Jitter Thêm random để tránh thundering herd
Idempotent Chỉ retry safe/idempotent operations

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.

Program.cs — Hedging với A/B routing
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.

Program.cs — Observability
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.

appsettings.json
{
  "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"
    }
  }
}
Program.cs — Bind cấu hình
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

Nguyên tắc 1
Luôn đặt timeout. Mọi HTTP call đều phải có timeout. Không có ngoại lệ. HttpClient mặc định không có timeout (thực ra là 100s — quá dài). Đặt total timeout hợp lý theo SLA của downstream.
Nguyên tắc 2
Circuit breaker cho mọi dependency. Mỗi downstream service cần circuit breaker riêng. Dùng named HttpClient + AddResilienceHandler để tách biệt — circuit breaker của Payment không ảnh hưởng đến Inventory.
Nguyên tắc 3
Không retry write operations (trừ khi có idempotency key). POST, PUT, DELETE — mỗi retry có thể tạo side effect. Dùng DisableForUnsafeHttpMethods() là cách an toàn nhất.
Nguyên tắc 4
Monitor circuit breaker state. Circuit mở là dấu hiệu cảnh báo quan trọng. Tích hợp OpenTelemetry + alert khi circuit mở quá 5 phút — lúc đó vấn đề không còn là transient fault mà là incident thật sự.
Nguyên tắc 5
Test resilience behavior. Dùng chaos engineering tools (Simmy — Polly's fault injection library) để inject timeout và exception trong staging. Không đợi production sập mới biết circuit breaker hoạt động đúng không.
Nguyên tắc 6
Chỉ dùng 1 resilience handler per HttpClient. Không stack 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:

Program.cs — Production-grade setup
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: