核心策略詳解
Polly v8 提供六種內建策略,每種都對應不同的故障處理場景。本頁逐一說明並附上工業自動化範例。
策略選擇決策樹
在開始套策略之前,先從失敗類型與業務成本兩個維度判斷該選哪一種。下圖列出最常見的選擇路徑(可同時選多個並組合):
成本與失敗類型對照:
| 失敗類型 | 範例 | 推薦策略 | 不該用 |
|---|---|---|---|
| 暫時失敗 + 冪等讀取 | Modbus 偶發 timeout、HTTP 5xx | AddRetry + AddTimeout | 非冪等不要 retry |
| 暫時失敗 + 不可重試 | 扣款、SECS Remote Command | AddFallback 或 AddTimeout 不重試 | AddRetry(會重複執行副作用) |
| 持續失敗 (下游掛掉) | DB 完全斷線、PLC 失聯 | AddCircuitBreaker + AddFallback | 純 retry(會把資源燒光) |
| 自身過載 | 大量上游請求湧入 | AddRateLimiter(v8 取代 Bulkhead) | 純 retry / hedging |
| 對 P99 latency 敏感 | 看板讀取、Dashboard 查詢 | AddHedging(僅冪等) | 寫入類請求 |
註:Polly v8 已將舊版的
Bulkhead(信號量隔離)併入AddRateLimiter;組合策略時請改用AddRateLimiter而非AddBulkhead。
各策略的詳細 API 參數與工業自動化範例見以下章節。
Retry — 重試
遇到暫態錯誤時,等待一段時間後重新嘗試。
基本用法
var pipeline = new ResiliencePipelineBuilder()
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromSeconds(1),
BackoffType = DelayBackoffType.Exponential, // 1s, 2s, 4s
})
.Build();
退避策略
| BackoffType | 行為 | 適用場景 |
|---|---|---|
Constant | 每次間隔相同(預設) | 穩定的暫態錯誤 |
Linear | 間隔線性增長 | 逐漸恢復的場景 |
Exponential | 間隔指數增長(推薦) | 設備重啟、網路恢復 |
搭配 UseJitter = true 可在任何退避策略上加入隨機抖動(±25%),避免多客戶端同時重試(thundering herd)。
Modbus 讀取重試
var modbusRetry = new ResiliencePipelineBuilder<ushort[]>()
.AddRetry(new RetryStrategyOptions<ushort[]>
{
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
Delay = TimeSpan.FromMilliseconds(500),
ShouldHandle = new PredicateBuilder<ushort[]>()
.Handle<ModbusException>()
.Handle<TimeoutException>(),
OnRetry = args =>
{
Logger.Warn(
$"Modbus 讀取失敗 [{args.Outcome.Exception?.GetType().Name}]," +
$"第 {args.AttemptNumber + 1} 次重試,延遲 {args.RetryDelay.TotalMilliseconds}ms");
return ValueTask.CompletedTask;
}
})
.Build();
var registers = await modbusRetry.ExecuteAsync(
async ct => await modbusClient.ReadHoldingRegistersAsync(1, 0, 10, ct));
Circuit Breaker — 斷路器
當錯誤頻率超過閾值時「斷路」,停止嘗試一段時間。避免對已知故障的設備持續發送請求。
狀態機
Closed(正常) ──失敗率超標──▶ Open(斷路)
▲ │
│ 等待 BreakDuration
│ │
└──成功──── HalfOpen(半開) ◀──┘
(試探性放行一個請求)
基本用法
var pipeline = new ResiliencePipelineBuilder()
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.5, // 失敗率 50% 就斷路
SamplingDuration = TimeSpan.FromSeconds(30), // 取樣窗口 30 秒
MinimumThroughput = 5, // 至少 5 次請求才判斷
BreakDuration = TimeSpan.FromSeconds(30), // 斷路 30 秒
OnOpened = args =>
{
Logger.Error($"斷路器開啟!設備通訊中斷,{args.BreakDuration.TotalSeconds}s 後重試");
return ValueTask.CompletedTask;
},
OnClosed = args =>
{
Logger.Info("斷路器關閉,設備通訊恢復");
return ValueTask.CompletedTask;
},
OnHalfOpened = args =>
{
Logger.Info("斷路器半開,嘗試恢復通訊...");
return ValueTask.CompletedTask;
}
})
.Build();
設備通訊場景
// PLC 長時間無回應 → 斷路,避免阻塞其他設備的通訊
var plcCircuitBreaker = new ResiliencePipelineBuilder()
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.8, // 80% 失敗才斷路(設備偶爾忙碌是正常的)
SamplingDuration = TimeSpan.FromSeconds(60),
MinimumThroughput = 10,
BreakDuration = TimeSpan.FromSeconds(60),
ShouldHandle = new PredicateBuilder()
.Handle<TimeoutException>()
.Handle<CommunicationException>()
})
.Build();
Timeout — 逾時
限制操作的最大執行時間。設備通訊必須設定逾時,避免一個無回應的設備拖垮整個系統。
基本用法
var pipeline = new ResiliencePipelineBuilder()
.AddTimeout(new TimeoutStrategyOptions
{
Timeout = TimeSpan.FromSeconds(5),
OnTimeout = args =>
{
Logger.Warn($"操作逾時({args.Timeout.TotalSeconds}s)");
return ValueTask.CompletedTask;
}
})
.Build();
逾時會拋出 TimeoutRejectedException
try
{
await pipeline.ExecuteAsync(async ct =>
{
// ct 是 Polly 管理的 CancellationToken
// 逾時時 Polly 會取消這個 token
await plcClient.ReadRegisterAsync("D100", ct);
});
}
catch (TimeoutRejectedException)
{
Logger.Error("PLC 回應逾時");
}
Fallback — 降級
操作失敗時回傳替代值。適合「有最後已知值比沒有值好」的場景。
基本用法
var pipeline = new ResiliencePipelineBuilder<double>()
.AddFallback(new FallbackStrategyOptions<double>
{
ShouldHandle = new PredicateBuilder<double>()
.Handle<CommunicationException>()
.Handle<TimeoutRejectedException>(),
FallbackAction = args =>
{
Logger.Warn("通訊失敗,回傳最後已知溫度值");
return Outcome.FromResultAsValueTask(lastKnownTemperature);
}
})
.Build();
var temperature = await pipeline.ExecuteAsync(
async ct => await ReadTemperatureAsync(ct));
搭配快取的最後已知值
private double _lastKnownTemp = double.NaN;
var pipeline = new ResiliencePipelineBuilder<double>()
.AddFallback(new FallbackStrategyOptions<double>
{
ShouldHandle = new PredicateBuilder<double>()
.Handle<Exception>(),
FallbackAction = args =>
{
if (double.IsNaN(_lastKnownTemp))
return Outcome.FromExceptionAsValueTask<double>(
args.Outcome.Exception!); // 沒有快取值就還是拋錯
return Outcome.FromResultAsValueTask(_lastKnownTemp);
}
})
.Build();
Rate Limiter — 限流
限制對設備或 API 的請求頻率。某些 PLC 或設備有請求頻率限制。
基本用法
var pipeline = new ResiliencePipelineBuilder()
.AddRateLimiter(new SlidingWindowRateLimiterOptions
{
PermitLimit = 10, // 每個窗口最多 10 次
Window = TimeSpan.FromSeconds(1), // 窗口大小 1 秒
QueueLimit = 5, // 排隊上限 5 個
})
.Build();
設備請求頻率限制
// 某些 SECS/GEM 設備限制每秒最多 5 個 transaction
var secsRateLimiter = new ResiliencePipelineBuilder()
.AddRateLimiter(new SlidingWindowRateLimiterOptions
{
PermitLimit = 5,
Window = TimeSpan.FromSeconds(1),
QueueLimit = 20,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
})
.Build();
超過限制時拋出 RateLimiterRejectedException。
info
Rate Limiter 需要額外安裝 Polly.RateLimiting NuGet 套件。
Hedging — 對沖
同時發送多個請求,取最快回應的那個。適合有備援設備或多路徑的場景。
基本用法
var pipeline = new ResiliencePipelineBuilder<DeviceData>()
.AddHedging(new HedgingStrategyOptions<DeviceData>
{
MaxHedgedAttempts = 2,
Delay = TimeSpan.FromSeconds(1), // 1 秒後沒回應就發第二個請求
ActionGenerator = args =>
{
// 切換到備用設備
return () => ReadFromBackupDeviceAsync(args.PrimaryContext.CancellationToken);
}
})
.Build();
雙備援設備場景
// 主設備和備用設備,誰先回應用誰
var hedging = new ResiliencePipelineBuilder<SensorReading>()
.AddHedging(new HedgingStrategyOptions<SensorReading>
{
MaxHedgedAttempts = 1,
Delay = TimeSpan.FromSeconds(2), // 主設備 2 秒沒回就問備用
ActionGenerator = args =>
{
Logger.Info("主設備回應慢,嘗試備用設備");
return () => backupSensor.ReadAsync(args.PrimaryContext.CancellationToken);
}
})
.Build();
組合策略
使用 ResiliencePipelineBuilder 鏈式呼叫組合多個策略。策略的順序很重要——外層策略先執行。
推薦的組合順序
var pipeline = new ResiliencePipelineBuilder()
// 1. 最外層:總逾時(整體操作的時間上限)
.AddTimeout(new TimeoutStrategyOptions
{
Timeout = TimeSpan.FromSeconds(30),
Name = "TotalTimeout"
})
// 2. 重試(包含內層策略的重試)
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential,
Delay = TimeSpan.FromSeconds(1),
})
// 3. 斷路器
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(30),
MinimumThroughput = 5,
BreakDuration = TimeSpan.FromSeconds(30),
})
// 4. 最內層:單次操作逾時
.AddTimeout(new TimeoutStrategyOptions
{
Timeout = TimeSpan.FromSeconds(5),
Name = "AttemptTimeout"
})
.Build();
執行流程:
請求 → TotalTimeout(30s) → Retry(3次) → CircuitBreaker → AttemptTimeout(5s) → 實際操作
下一步:公司場景 Pattern — 完整的工業場景韌性管道範例