Skip to main content

核心策略詳解

Polly v8 提供六種內建策略,每種都對應不同的故障處理場景。本頁逐一說明並附上工業自動化範例。


策略選擇決策樹

在開始套策略之前,先從失敗類型與業務成本兩個維度判斷該選哪一種。下圖列出最常見的選擇路徑(可同時選多個並組合):

成本與失敗類型對照:

失敗類型範例推薦策略不該用
暫時失敗 + 冪等讀取Modbus 偶發 timeout、HTTP 5xxAddRetry + AddTimeout非冪等不要 retry
暫時失敗 + 不可重試扣款、SECS Remote CommandAddFallbackAddTimeout 不重試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 — 完整的工業場景韌性管道範例