Polly is a .NET resilience and transient-fault-handling library that allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner.
Polly是一個.NET彈性和瞬態故障處理庫,允許開發人員以流暢和線程安全的方式表達諸如重試,斷路器,超時,隔離和回退之類的策略。
支援平台
Polly targets .NET Standard 1.1 (coverage: .NET Core 1.0, Mono, Xamarin, UWP, WP8.1+) and .NET Standard 2.0+ (coverage: .NET Core 2.0+, .NET Core 3.0, and later Mono, Xamarin and UWP targets). The nuget package also includes direct targets for .NET Framework 4.6.1 and 4.7.2.
引用內容出自:https://github.com/App-vNext/Polly
前面幾篇有稍微介紹 Polly 的應用 Hangfire + Polly、HttpClientFactory + Polly,忽然發現 Polly 的草稿好像還沒使用。
開發環境
- VS 2019
- WinForm .NET 4.8
- Polly 7.2.1
在沒有使用 Polly 之前呢,Retry 你可能會這樣寫,這是一個很簡單的重試
private static void Retry(Action action, int retryCount = 3, int waitSecond = 10)
{
while (true)
{
try
{
action();
break;
}
catch
{
if (--retryCount == 0)
{
throw;
}
var seconds = TimeSpan.FromSeconds(waitSecond);
Thread.Sleep(seconds);
}
}
}
三個步驟
安裝以下套件 Install-Package Polly
Polly 提供了相當豐富的 Retry 機制,你就可以不需要自己造輪子囉,Polly 處理例外的寫法主要有三個步驟,風格是方法鏈(Fluent Interface Pattern)
1. 處理甚麼樣的例外,Handle:
Policy.Handle<Exception>
1-1. 或者是返回條件符合失敗,OrResult(非必要):
Policy.Handle<Exception>
也可以這樣寫 HandleResult:
Policy.HandleResult
2. 重試策略,包含重試次數,發生錯誤時回呼匿名方法:
.Retry(3, (reponse, retryCount, context) =>
{
//call back
//錯誤發生後要做的事
})
3. 執行內容,你要做的事,Execute:
.Execute(FakeRequest)
完整代碼如下:
private static void _01_標準用法()
{
Policy
// 1. 處理甚麼樣的例外
.Handle<HttpRequestException>()
// 或者返回條件(非必要)
.OrResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.BadGateway)
// 2. 重試策略,包含重試次數
.Retry(3, (reponse, retryCount, context) =>
{
var result = reponse.Result;
if (result != null)
{
var errorMsg = result.Content
.ReadAsStringAsync()
.GetAwaiter()
.GetResult();
Console.WriteLine($"標準用法,發生錯誤:{errorMsg},第 {retryCount} 次重試");
}
else
{
var exception = reponse.Exception;
Console.WriteLine($"標準用法,發生錯誤:{exception.Message},第 {retryCount} 次重試");
}
Thread.Sleep(3000);
})
// 3. 執行內容
.Execute(FailResponse);
Console.WriteLine("標準用法,完成");
}
七種重試策略
Polly 可以實現重試、斷路、超時、隔離、回退和緩存策略,下面列出一些使用場景和範例
重試 (Retry)
用在短暫發生很快就會恢復的故障,這個是很常見的場景,在一開始的使用步驟就是使用 Retry
永不放棄,失敗的時,等待 5 秒再重試
private static void _01_永不放棄()
{
var retryPolicy = Policy.HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.BadGateway)
.RetryForever((response, retryCount, context) =>
{
var errorMsg = response.Result
.Content
.ReadAsStringAsync()
.GetAwaiter()
.GetResult();
Console.WriteLine($"永不放棄,發生錯誤:{errorMsg},第 {retryCount} 次重試");
Thread.Sleep(5000);
})
;
retryPolicy.Execute(FailResponse);
Console.WriteLine("永不放棄,完成");
}
延遲重試
根據自訂頻率重試
private static void _02_延遲重試_固定周期()
{
var retryPolicy = Policy.HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.BadGateway)
.WaitAndRetry(new[]
{
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10),
TimeSpan.FromSeconds(15)
},
(response, retryTime, context) =>
{
var errorMsg = response.Result
.Content
.ReadAsStringAsync()
.GetAwaiter()
.GetResult();
Console.WriteLine($"延遲重試,發生錯誤:{errorMsg},延遲 {retryTime} 後重試");
});
retryPolicy.Execute(FailResponse);
Console.WriteLine("延遲重試,完成");
}
根據次方計算延遲重試時間,下面的例子,6 的次方,重試 6 次
private static void _01_延遲重試_計算週期_次方()
{
var retryPolicy = Policy.HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.BadGateway)
.WaitAndRetry(6,
retryAttempt => TimeSpan.FromSeconds(Math.Pow(6, retryAttempt)),
(response, retryTime, context) =>
{
var errorMsg = response.Result
.Content
.ReadAsStringAsync()
.GetAwaiter()
.GetResult();
Console.WriteLine($"延遲重試,發生錯誤:{errorMsg},延遲 {retryTime} 後重試");
})
;
retryPolicy.Execute(FailResponse);
Console.WriteLine("延遲重試,完成");
}
Jitter
/// <summary>
/// https://docs.microsoft.com/zh-tw/dotnet/architecture/microservices/implement-resilient-applications/implement-http-call-retries-exponential-backoff-polly
/// </summary>
private static void _01_延遲重試_計算週期_Jitter()
{
//抖動演算法
var jitterer = new Random();
var retryPolicy = Policy.Handle<Exception>()
.OrResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.BadGateway)
.WaitAndRetry(6,
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))
+ TimeSpan.FromMilliseconds(jitterer.Next(0, 100)),
(response, retryTime, context) =>
{
WaitAndRetryAction(response, retryTime);
})
;
try
{
var httpResponse = retryPolicy.Execute(RandomFailResponseOrException);
var content = httpResponse.Content
.ReadAsStringAsync()
.GetAwaiter()
.GetResult()
;
Console.WriteLine(content);
Console.WriteLine("延遲重試,完成");
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
參考
https://docs.microsoft.com/zh-tw/dotnet/architecture/microservices/implement-resilient-applications/implement-http-call-retries-exponential-backoff-polly#add-a-jitter-strategy-to-the-retry-policy
https://docs.microsoft.com/zh-tw/dotnet/architecture/microservices/implement-resilient-applications/implement-http-call-retries-exponential-backoff-polly
斷路 (Circuit-breaker)
當系統遇到嚴重問題時,快速回饋失敗比讓用戶/調用者等待要好,在復原之前先關閉請求,也有助於系統恢復。
比如,當我們去調一個第三方的 API,有很長一段時間 API 都沒有響應,可能對方服務器癱瘓了。如果我們的系統還不停地重試,不僅會加重系統的負擔,還會可能導致系統其它任務受影響。所以,當系統出錯的次數超過了指定的閾值,就要中斷當前線路,等待一段時間後再繼續。
下面是一個斷路策略的使用方式,假如出現兩次異常時就停下來,等待一分鐘後再繼續;除此之外,還可以在斷路時定義中斷的回呼(OnBreak)和重啟(OnReset)的回呼
Policy.Handle
斷路器的執行流程,下圖出自,https://github.com/App-vNext/Polly/wiki/Circuit-Breaker
為了演練斷路器模式,我找到了這篇文章 www.twilio.com/blog/using-polly-circuit-breakers-resilient-net-web-service-consumers,這篇詳細的介紹及演練斷路器的實現,需搭配 Microsoft.Extensions.Http.Polly,開始之前請先到 https://github.com/bryanjhogan/PollyCircuitBreakers/ 拉專案下來,再按照文章內容實作
或者使用我的演練代碼
定義重試策略,其中 onBreak、onRest、onHalfOpen 用來觀察斷路器模式的狀態,失敗兩次後要等 30 秒才能再執行,代碼如下:
private static AsyncCircuitBreakerPolicy<HttpResponseMessage> CreateAsyncCircuitBreakerPolicy()
{
Action<DelegateResult<HttpResponseMessage>, CircuitState, TimeSpan, Context> onBreak =
(response, state, retryTime, context) =>
{
var ex = response.Exception;
string msg = null;
if (ex != null)
{
msg = $"錯誤:{ex.Message}\r\n"
+ $"超過失敗上限了,先等等,過了 {retryTime} 再過來\r\n"
+ $"斷路器狀態:{state}"
;
}
else
{
var content = response.Result
.Content
.ReadAsStringAsync()
.GetAwaiter()
.GetResult();
msg = $"錯誤:{content}\r\n"
+ $"超過失敗上限了,先等等,過了 {retryTime} 再過來\r\n"
+ $"斷路器狀態:{state}"
;
}
Console.WriteLine(msg);
Console.WriteLine();
}
;
Action<Context> onReset = context =>
{
var state = s_asyncCircuitBreakerPolicy.CircuitState;
Console.WriteLine($"Reset 重設,斷路器狀態:{state}");
};
Action onHalfOpen = () =>
{
var state = s_asyncCircuitBreakerPolicy.CircuitState;
Console.WriteLine($"斷路器狀態:{state}");
};
var policy = Policy.Handle<Exception>()
.OrResult<HttpResponseMessage>(p => p.IsSuccessStatusCode == false)
.CircuitBreakerAsync(2, TimeSpan.FromSeconds(30), onBreak, onReset, onHalfOpen);
return policy;
}
初始化重試策略
private static AsyncCircuitBreakerPolicy<HttpResponseMessage> s_asyncCircuitBreakerPolicy;
public Form1()
{
this.InitializeComponent();
s_asyncCircuitBreakerPolicy = CreateAsyncCircuitBreakerPolicy();
}
模擬服務狀態,如下代碼:
private static HttpResponseMessage RandomFailResponseOrException()
{
Console.WriteLine("請求網路資源中...");
var random = new Random().Next(0, 10);
if (random <= 1)
{
throw new HttpRequestException("請求出現未知異常~");
}
var response = new HttpResponseMessage();
if (random > 2 & random <= 6)
{
response.StatusCode = HttpStatusCode.OK;
response.Content = new StringContent("對了,媽,我在這裡~!");
}
else if (random > 6)
{
response.StatusCode = HttpStatusCode.BadGateway;
response.Content = new StringContent("網路設備噴掉了啦!!!");
}
return response;
}
private static Task<HttpResponseMessage> RandomFailResponseOrExceptionAsync()
{
var response = RandomFailResponseOrException();
return Task.FromResult(response);
}
最後,用按鈕呼叫它
private static void _02_斷路器()
{
var state = s_asyncCircuitBreakerPolicy.CircuitState;
try
{
Console.WriteLine($"呼叫任務前的狀態:{state}");
var response = s_asyncCircuitBreakerPolicy.ExecuteAsync(RandomFailResponseOrExceptionAsync)
.GetAwaiter()
.GetResult();
var content = response.Content;
if (content != null)
{
var result = content.ReadAsStringAsync().GetAwaiter().GetResult();
Console.WriteLine($"取得服務內容:{result}\r\n"
+ $"斷路器狀態:{state}");
if (response.IsSuccessStatusCode)
{
Console.WriteLine("斷路器,正常完成");
}
}
}
catch (Exception e)
{
Console.WriteLine($"錯誤:{e.Message}\r\n"
+ $"斷路器狀態:{state}");
}
finally
{
Console.WriteLine("");
}
}
執行結果,當狀態是 Open 的時候,不允許執行任務,必須要等待 30 秒(前面設定的),斷路器變成 Close,才能再次執行任務,如下圖
超時 (Timeout)
當系統超過一定時間的等待,幾乎可以判斷不可能會有成功的結果。例如,平時一個網絡請求瞬間就完成了,如果有一次網絡請求超過了 30 秒還沒完成,大概就知道這次不會成功了,這時就可以使用超時策略,避免系統長時間做無所謂的等待。
兩種 TimeoutStrategy
樂觀 (Optimistic):預設為 Optimistic,支援取消 (CancellationToken),捕捉 OperationCanceledException 和 CancellationTokenSource.IsCancellationRequested 來表示超時,大多數的 .NET 類別都是使用這種方式。
悲觀 (Pessimistic):某些情況可能沒有支援取消,允許強制執行超時。
下面是一個超時策略的使用方式,超過 30 秒,發生錯誤會進入回呼。
Policy.Timeout(30, onTimeout: (context, timespan, task) =>
{
// call back
});
接下來演練觸發樂觀 (Optimistic) TimeOut,先使用 HttpClient 產生超時例外。
private static string TimeoutRequest()
{
Console.WriteLine("請求網路資源中...");
var baseAddress = "http://localhost:9527";
// 釋放掉 HttpClient 是不對的作法,這裡只是為了模擬超時所而產生的代碼
using (var client = new HttpClient {BaseAddress = new Uri(baseAddress)})
{
client.BaseAddress = new Uri(baseAddress);
client.Timeout = TimeSpan.FromMilliseconds(10);
var response = client.GetAsync("api/value")
.GetAwaiter()
.GetResult();
if (response.IsSuccessStatusCode)
{
var result = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
return result;
}
}
return null;
}
最後,用按鈕呼叫它,當超時例外出現時,會進入 Policy.Timeout,我在這裡印出錯誤訊息
private static void _03_樂觀超時()
{
var timeoutPolicy = Policy.Timeout(TimeSpan.FromMilliseconds(1),
(context, timespan, task, ex) =>
{
var errorMsg = $"錯誤訊息:{ex.Message}"
+ $"錯誤目標:{ex.TargetSite}";
Console.WriteLine($"逾時時間:{timespan},錯誤:{errorMsg}");
});
try
{
timeoutPolicy.Execute(TimeoutRequest);
Console.WriteLine("沒有超時,完成");
}
catch (Exception e)
{
Console.WriteLine($"超時錯誤:{e.Message}");
}
}
不透過 HttpClient 也可以這樣模擬超時,手動建立 TaskCanceledException 或 OperationCanceledException。
TaskCanceledException 實作了 OperationCanceledException
timeoutPolicy.Execute(() =>
{
var cancelSource = new CancellationTokenSource();
var task = new Task(() => { Console.WriteLine("模擬超時例外"); },
cancelSource.Token);
cancelSource.Cancel();
var ex = new TaskCanceledException(task);
throw ex;
});
timeoutPolicy.Execute(() =>
{
var cancelSource = new CancellationTokenSource();
cancelSource.Cancel();
var ex = new OperationCanceledException(cancelSource.Token);
throw ex;
});
Polly.TimeOut 攔截 OperationCanceledException 例外且判斷是否有觸發 Cancel,最後再重新引發 TimeoutRejectedException 例外
悲觀 (Pessimistic) TimeOut ,使用時間來決定是否超時
private static void _03_悲觀超時()
{
var timeoutPolicy = Policy.Timeout(TimeSpan.FromSeconds(3),
TimeoutStrategy.Pessimistic,
(context, time, task, ex) =>
{
var errorMsg = $"錯誤訊息:{ex.Message}"
+ $"錯誤目標:{ex.TargetSite}";
Console.WriteLine($"逾時時間:{time}\r\n錯誤:{errorMsg}");
});
try
{
timeoutPolicy.Execute(() =>
{
Console.WriteLine("請求網路資源中...");
Thread.Sleep(TimeSpan.FromSeconds(5));
});
Console.WriteLine("沒有超時,完成");
}
catch (Exception e)
{
Console.WriteLine($"超時錯誤:{e.Message}");
}
}
隔離 (Bulkhead Isolation)
當系統的一處出現故障時,可能引發多個錯誤,很容易耗盡主機的資源(如 CPU、RAM、執行緒),將操作限制在一個固定大小的資源池中,隔離有潛在可能相互影響的操作。
以下是隔離策略的用法,表示最多允許 12 個執行序執行,如果執行任務被拒絕,則執行回呼。
Policy.Bulkhead(12, context =>
{
// do something
});
完整範例如下:
private static void _04_隔離()
{
var bulkheadPolicy = Policy.Bulkhead(1, 1, context =>
{
var msg = $"Reject:{context.PolicyKey}";
Console.WriteLine(msg);
});
Console.WriteLine("請求網路資源中...");
Task.Factory
.StartNew(() =>
{
bulkheadPolicy.Execute(() =>
{
Console.WriteLine("1.Execute Task,休息一下");
Thread.Sleep(TimeSpan.FromSeconds(5));
});
});
Task.Factory
.StartNew(() =>
{
bulkheadPolicy.Execute(() =>
{
Console.WriteLine("2.Execute Task");
});
});
Task.Factory
.StartNew(() =>
{
bulkheadPolicy.Execute(() =>
{
Console.WriteLine("3.Execute Task");
});
});
Console.WriteLine("隔離,完成");
}
回退 (Fallback)
當錯誤發生時,執行備用方案。語法如下:
Policy.Handle<Whatever>()
.Fallback<UserAvatar>(() => UserAvatar.GetRandomAvatar())
參考:https://github.com/App-vNext/Polly/wiki/Fallback
完整範例
private static void _05_回退()
{
var policy = Policy.Handle<HttpRequestException>()
.Fallback(() =>
{
Console.WriteLine("回退策略,執行解決方案,請求另一個備援服務位置");
Console.WriteLine("請求網路資源 http://localhost:9528");
});
policy.Execute(() =>
{
Console.WriteLine("請求網路資源 http://localhost:9527,出現異常...");
throw new HttpRequestException("機房發生火災");
});
Console.WriteLine("回退策略,完成");
}
快取 (Cache)
一般我們會把頻繁使用且不會怎麽變化的資源快取起來,以提高系統的響應速度,一般常見的快取邏輯是先判斷快取中有沒有這些資料,有的話就回傳;沒有的話就從資料庫讀取,放到快取,回傳結果,Polly 的快取策略也有這樣的機制
Cache 需要依賴 Polly.Caching.Memory,也依賴 Microsoft.Extensions.Caching.Memory
完整範例如下:
需要安裝以下套件
Install-Package Polly.Caching.Memory
Install-Package Newtonsoft.Json
Install-Package FakeData
"datakey" 是存取快取的 key
private static void _06_緩存()
{
var memoryCache = new MemoryCache(new MemoryCacheOptions());
var memoryCacheProvider = new MemoryCacheProvider(memoryCache);
if (s_cachePolicy == null)
{
s_cachePolicy = Policy.Cache(memoryCacheProvider, TimeSpan.FromSeconds(5));
}
var result = s_cachePolicy.Execute(context =>
{
Console.WriteLine("快取過期,更新快取內容");
var names = new List<string>();
var number = FakeData.NumberData.GetNumber(1, 10);
for (var i = 0; i < number; i++)
{
names.Add(FakeData.NameData.GetFullName());
}
return names;
}, new Context("datakey"));
var json = JsonConvert.SerializeObject(result);
Console.WriteLine($"取得資料:{json}");
Console.WriteLine("隔離策略,完成");
}
策略包 (Policy Wrap)
一種操作可能會有多種不同的故障,而不同的故障處理需要不同的策略,Polly 可以將這些不同的策略包在一起,基本用法如下
var policyWrap = Policy.Wrap(fallback, cache, retry, breaker, timeout, bulkhead);
policyWrap.Execute(...);
完整範例如下
用起來蠻簡單的,比較值得注意的是策略的執行順序,比如 Policy.Wrap(fallbackPolicy, waitAndRetry),會先執行 waitAndRetry → fallbackPolicy
private static void _07_策略包()
{
var fallbackPolicy = Policy.Handle<HttpRequestException>()
.OrResult<HttpResponseMessage>(p => p.IsSuccessStatusCode == false)
.Fallback(() =>
{
Console.WriteLine("執行回退策略,請求另一個備援服務位置");
Console.WriteLine("請求網路資源 http://localhost:9528/api/values");
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("OK,換個位置就可以了")
};
return response;
});
var waitAndRetry = Policy.Handle<HttpRequestException>()
.OrResult<HttpResponseMessage>(r => r.IsSuccessStatusCode == false)
.WaitAndRetry(3,
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
(response, retryTime, context) =>
{
Console.WriteLine($"執行重試策略,重試時間 {retryTime}");
});
var policyWrap = Policy.Wrap(fallbackPolicy, waitAndRetry);
var wrapResponse = policyWrap.Execute(() =>
{
Console.WriteLine("向 http://localhost:9527/api/values 請求資源");
var response = new HttpResponseMessage(HttpStatusCode.BadGateway);
return response;
});
var result = wrapResponse.Content.ReadAsStringAsync().GetAwaiter().GetResult();
Console.WriteLine($"結果:{result}");
Console.WriteLine("策略包,完成");
}
範例位置
https://github.com/yaochangyu/sample.dotblog/tree/master/Retry/Polly/Lab.RetryPolly
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET