Skip to main content

最佳實踐


1. 區分 Transient vs Permanent Failure

不是所有錯誤都應該重試。 對永久性錯誤重試只是浪費時間和資源。

應該重試的(Transient)

  • TimeoutException — 網路暫時不穩
  • IOException — 連線瞬斷
  • HttpRequestException with 5xx — 伺服器暫時錯誤
  • ModbusException with 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 重用

延伸閱讀