🛡️ 微服務韌性模式: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 模式。