☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
microservices 中級

🛡️ 微服務韌性模式:Polly 與容錯設計

📌 為什麼微服務需要韌性?

在分散式系統中,網路是不可靠的。服務可能暫時不可用、回應緩慢或回傳錯誤。如果不處理這些情況,一個服務的故障會像骨牌效應般擴散到整個系統。

沒有韌性的微服務:
用戶 → Gateway → 訂單服務 → 庫存服務(掛了 💥)
                        ↓
                   訂單服務超時等待...
                        ↓
                   Gateway 超時等待...
                        ↓
                   用戶看到 500 錯誤 😡
有韌性的微服務:
用戶 → Gateway → 訂單服務 → 庫存服務(掛了 💥)
                        ↓
                   斷路器啟動:直接回傳降級回應
                        ↓
                   用戶看到:"庫存確認中,稍後通知"

📌 安裝 Polly

# .NET 8 推薦使用新版 Polly v8
dotnet add package Microsoft.Extensions.Http.Polly
dotnet add package Microsoft.Extensions.Http.Resilience

📌 重試 (Retry) 策略

暫時性故障(網路閃斷、服務重啟中)通常重試就能解決。

// ── 基本重試 ──
builder.Services.AddHttpClient("ProductService")
    .AddTransientHttpErrorPolicy(policy =>
        policy.WaitAndRetryAsync(
            retryCount: 3,
            sleepDurationProvider: attempt =>
                TimeSpan.FromSeconds(Math.Pow(2, attempt)), // 指數退避:2s, 4s, 8s
            onRetry: (outcome, delay, attempt, context) =>
            {
                Console.WriteLine($"重試第 {attempt} 次,等待 {delay.TotalSeconds}s");
            }));

// ── .NET 8 新版 Resilience Pipeline ──
builder.Services.AddHttpClient("ProductService")
    .AddStandardResilienceHandler(options =>
    {
        options.Retry.MaxRetryAttempts = 3;
        options.Retry.Delay = TimeSpan.FromSeconds(1);
        options.Retry.BackoffType = DelayBackoffType.Exponential;
        options.Retry.UseJitter = true; // 加入抖動,避免多個客戶端同時重試
    });

何時該重試?何時不該?

✅ 適合重試的場景:
├── HTTP 408 Request Timeout
├── HTTP 429 Too Many Requests
├── HTTP 500/502/503/504 伺服器錯誤
├── 網路連線逾時
└── 資料庫暫時性錯誤

❌ 不適合重試的場景:
├── HTTP 400 Bad Request(請求有誤,重試也沒用)
├── HTTP 401/403 認證失敗
├── HTTP 404 Not Found
├── 業務邏輯錯誤(庫存不足等)
└── 非冪等操作(要特別小心!)

📌 斷路器 (Circuit Breaker) 模式

當某個服務持續失敗時,斷路器會暫時切斷呼叫,避免浪費資源。

builder.Services.AddHttpClient("InventoryService")
    .AddTransientHttpErrorPolicy(policy =>
        policy.CircuitBreakerAsync(
            handledEventsAllowedBeforeBreaking: 5,  // 連續 5 次失敗後斷路
            durationOfBreak: TimeSpan.FromSeconds(30), // 斷路 30 秒
            onBreak: (outcome, duration) =>
                Console.WriteLine($"斷路器開啟!暫停 {duration.TotalSeconds}s"),
            onReset: () =>
                Console.WriteLine("斷路器關閉,恢復正常"),
            onHalfOpen: () =>
                Console.WriteLine("斷路器半開,嘗試一個請求...")
        ));

斷路器的三種狀態

┌──────────────────────────────────────────────┐
│              斷路器狀態機                      │
│                                              │
│  ┌────────┐  連續失敗  ┌────────┐            │
│  │ Closed │ ────────→ │  Open  │            │
│  │ (正常)  │           │ (斷路)  │            │
│  └───┬────┘  ←──────  └───┬────┘            │
│      ↑       成功重試      │                  │
│      │                    │ 等待超時           │
│      │       ┌────────┐   │                  │
│      └────── │HalfOpen│ ←─┘                  │
│       失敗   │(半開放) │                      │
│       →Open  └────────┘                      │
└──────────────────────────────────────────────┘

📌 超時 (Timeout) 與逾時處理

// 設定請求超時
builder.Services.AddHttpClient("PaymentService")
    .AddPolicyHandler(Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(5),
        TimeoutStrategy.Optimistic,
        onTimeoutAsync: (context, timespan, task) =>
        {
            Console.WriteLine($"請求超時!等待了 {timespan.TotalSeconds}s");
            return Task.CompletedTask;
        }));

📌 艙壁隔離 (Bulkhead) 模式

限制對某個服務的並行請求數,防止一個慢服務拖垮整個系統。

// 最多允許 10 個並行請求呼叫庫存服務
// 超過的最多排隊 5 個,其餘直接拒絕
builder.Services.AddHttpClient("InventoryService")
    .AddTransientHttpErrorPolicy(policy =>
        policy.BulkheadAsync(
            maxParallelization: 10,
            maxQueuingActions: 5,
            onBulkheadRejectedAsync: (context) =>
            {
                Console.WriteLine("艙壁隔離:請求被拒絕(並行數已滿)");
                return Task.CompletedTask;
            }));
艙壁隔離的概念(來自船艦設計):
┌─────────────────────────────────┐
│ 微服務系統                       │
│ ┌─────┐ ┌─────┐ ┌─────┐        │
│ │ 10  │ │ 20  │ │ 10  │ ← 最大並行數 │
│ │ 請求 │ │ 請求 │ │ 請求 │        │
│ │     │ │     │ │     │        │
│ │商品  │ │訂單  │ │庫存  │        │
│ └─────┘ └─────┘ └─────┘        │
│ 即使庫存服務很慢,也不會影響商品服務 │
└─────────────────────────────────┘

📌 完整範例:HttpClient + Polly 的韌性設定

// Program.cs — 完整的韌性配置
builder.Services.AddHttpClient<InventoryServiceClient>(client =>
{
    client.BaseAddress = new Uri("http://inventory-service");
})
.AddStandardResilienceHandler(options =>
{
    // 重試策略
    options.Retry.MaxRetryAttempts = 3;
    options.Retry.Delay = TimeSpan.FromMilliseconds(500);
    options.Retry.BackoffType = DelayBackoffType.Exponential;
    options.Retry.UseJitter = true;

    // 斷路器
    options.CircuitBreaker.FailureRatio = 0.5;  // 50% 失敗率觸發
    options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(10);
    options.CircuitBreaker.MinimumThroughput = 8;
    options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(30);

    // 總超時
    options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(15);

    // 單次請求超時
    options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(5);
});

// ── 搭配降級回應 ──
public class InventoryServiceClient
{
    private readonly HttpClient _client;
    private readonly ILogger<InventoryServiceClient> _logger;

    public InventoryServiceClient(HttpClient client,
        ILogger<InventoryServiceClient> logger)
    {
        _client = client;
        _logger = logger;
    }

    public async Task<StockInfo> GetStockAsync(int productId)
    {
        try
        {
            return await _client.GetFromJsonAsync<StockInfo>(
                $"/api/inventory/{productId}")
                ?? new StockInfo(productId, 0, false);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "無法取得庫存資訊,回傳降級回應");
            // 降級回應:假設有庫存,後續再非同步確認
            return new StockInfo(productId, -1, true);
        }
    }
}

public record StockInfo(int ProductId, int Quantity, bool IsDegraded);

下一章: 我們將學習微服務中最棘手的問題 — 分散式資料管理與 Saga 模式。

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊