最佳實踐
1. 區分 Transient vs Permanent Failure
不是所有錯誤都應該重試。 對永久性錯誤重試只是浪費時間和資源。
應該重試的(Transient)
TimeoutException— 網路暫時不穩IOException— 連線瞬斷HttpRequestExceptionwith 5xx — 伺服器暫時錯誤ModbusExceptionwith timeout — PLC 忙碌- SECS/GEM T3 Timeout — Transaction 逾時
不應該重試的(Permanent)
ArgumentException— 參數錯誤,再試也一樣UnauthorizedAccessException— 權限問題- HTTP 4xx(400 Bad Request、401、403、404)— 客戶端錯誤
FormatException— 資料格式錯誤- SECS/GEM SACK 錯誤 — 設備拒絕命令
使用 ShouldHandle 精確過濾
var pipeline = new ResiliencePipelineBuilder()
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
// 只對特定例外重試
ShouldHandle = new PredicateBuilder()
.Handle<TimeoutException>()
.Handle<IOException>()
.Handle<HttpRequestException>(ex =>
ex.StatusCode >= System.Net.HttpStatusCode.InternalServerError)
})
.Build();
2. Circuit Breaker 閾值設定
閾值應根據設備特性和業務需求來設定。
PLC 通訊(高頻、可容忍偶爾失敗)
new CircuitBreakerStrategyOptions
{
FailureRatio = 0.8, // 80% 失敗才斷路(偶爾逾時正常)
SamplingDuration = TimeSpan.FromSeconds(60),
MinimumThroughput = 10, // 至少 10 次才判斷
BreakDuration = TimeSpan.FromSeconds(30),
}
MES/ERP API 呼叫(低頻、重要性高)
new CircuitBreakerStrategyOptions
{
FailureRatio = 0.5, // 50% 就斷路
SamplingDuration = TimeSpan.FromSeconds(30),
MinimumThroughput = 3, // 3 次失敗就夠了
BreakDuration = TimeSpan.FromSeconds(60),
}
設定原則
| 參數 | 設高 | 設低 |
|---|---|---|
| FailureRatio | 容忍更多失敗(PLC 通訊) | 快速斷路(關鍵 API) |
| MinimumThroughput | 需要更多樣本才判斷(高頻) | 快速反應(低頻) |
| BreakDuration | 設備恢復慢(硬體重啟) | 網路問題通常很快恢復 |
| SamplingDuration | 觀察較長的趨勢 | 快速反應近期狀態 |
3. Logging 整合
每次重試和熔斷都應該記錄,方便除錯和監控。
使用事件回呼
services.AddResiliencePipeline("plc", (builder, context) =>
{
var logger = context.ServiceProvider
.GetRequiredService<ILogger<PlcCommunication>>();
builder
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
OnRetry = args =>
{
logger.LogWarning(
"PLC 操作重試 {Attempt}/{Max},延遲 {Delay}ms,原因: {Error}",
args.AttemptNumber + 1, 3,
args.RetryDelay.TotalMilliseconds,
args.Outcome.Exception?.Message ?? "unknown");
return ValueTask.CompletedTask;
}
})
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
OnOpened = args =>
{
logger.LogError(
"PLC 斷路器開啟!中斷 {Duration}s。原因: {Error}",
args.BreakDuration.TotalSeconds,
args.Outcome.Exception?.Message);
return ValueTask.CompletedTask;
},
OnClosed = _ =>
{
logger.LogInformation("PLC 斷路器關閉,通訊恢復正常");
return ValueTask.CompletedTask;
}
});
});
結構化日誌建議
[14:23:15 WRN] PLC 操作重試 1/3,延遲 500ms,原因: Modbus read timeout
[14:23:16 WRN] PLC 操作重試 2/3,延遲 1000ms,原因: Modbus read timeout
[14:23:17 WRN] PLC 操作重試 3/3,延遲 2000ms,原因: Modbus read timeout
[14:23:19 ERR] PLC 斷路器開啟!中斷 30s。原因: Modbus read timeout
...
[14:23:49 INF] PLC 斷路器關閉,通訊恢復正常
4. 測試韌性策略
用 DI 替換測試用 Pipeline
// 測試時註冊一個不做任何韌性處理的 Pipeline
services.AddResiliencePipeline("plc-communication", builder =>
{
// 空的 pipeline = 直接執行,不重試、不熔斷
// 用於測試業務邏輯,不測試韌性行為
});
直接測試 Pipeline 行為
[TestMethod]
public async Task Retry_ShouldRetryOnModbusException()
{
int callCount = 0;
var pipeline = new ResiliencePipelineBuilder<int>()
.AddRetry(new RetryStrategyOptions<int>
{
MaxRetryAttempts = 2,
Delay = TimeSpan.Zero, // 測試時不等待
ShouldHandle = new PredicateBuilder<int>()
.Handle<ModbusException>()
})
.Build();
var result = await pipeline.ExecuteAsync(async _ =>
{
callCount++;
if (callCount < 3)
throw new ModbusException("timeout");
return 42;
});
Assert.AreEqual(42, result);
Assert.AreEqual(3, callCount); // 1 原始 + 2 重試
}
測試 Circuit Breaker 狀態
[TestMethod]
public async Task CircuitBreaker_ShouldOpenAfterFailures()
{
var options = new CircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(30),
MinimumThroughput = 2,
BreakDuration = TimeSpan.FromSeconds(5),
};
var pipeline = new ResiliencePipelineBuilder()
.AddCircuitBreaker(options)
.Build();
// 觸發 2 次失敗
for (int i = 0; i < 2; i++)
{
try
{
await pipeline.ExecuteAsync(_ =>
throw new InvalidOperationException());
}
catch { }
}
// 第 3 次應該被斷路器攔截
await Assert.ThrowsExceptionAsync<BrokenCircuitException>(
() => pipeline.ExecuteAsync(_ => ValueTask.CompletedTask));
}
5. 常見錯誤
| 錯誤 | 問題 | 修正 |
|---|---|---|
| 對所有 Exception 重試 | 永久性錯誤也重試,浪費時間 | 用 ShouldHandle 指定例外類型 |
| 無限重試 | 卡死流程 | 設定 MaxRetryAttempts |
| 重試間隔太短 | 打爆設備 | 用 Exponential 退避 |
| 沒有 CircuitBreaker | 設備離線時持續嘗試 | 加入 CircuitBreaker 斷路 |
| 沒有 Timeout | 一個請求卡住拖垮全部 | 每個操作都加 Timeout |
| 只加外層 Timeout | 重試內的單次操作沒有時間限制 | 加兩層 Timeout(Total + Attempt) |
| Pipeline 重複建立 | 效能浪費 | 用 DI 註冊,singleton 重用 |
延伸閱讀:
- 概觀 — v8 架構與安裝
- 核心策略詳解 — 每個策略的完整參數
- 場景 Pattern — 完整的工業場景範例