Polly 是 .Net 下的一套瞬時故障處理及恢復的函式庫,可讓開發者以fluent及線程安全的方式來應用諸如Retry、Circuit Breaker、Timeout、Bulkhead Isolation及Fallback等策略。
安裝
可以通過Nuget實現快速安裝: Install-Package Polly
基本用法
一個簡單的示例如下:
var policy = Policy
.Handle<DivideByZeroException>() //定義所處理的故障
.Retry(); //故障的處理方法
policy.Execute(() => DoSomething()); //應用策略
從上面的例子中我們可以看出,使用該策略一般包括三個步驟:
- 定義所處理的故障
- 定義故障的處理方法
- 應用策略
上述代碼在功能上和下面的代碼等價:
for (int i = 0; i < 2; i++)
{
try
{
DoSomething();
}
catch (DivideByZeroException)
{
if (i > 1)
throw;
}
}
雖然這個例子比較簡單,帶來的優越性並不明顯,但它以一種比較規范的方式定義了異常的處理策略,一來帶來了更好的體驗,帶來了更好的代碼可讀性,另外,隨着異常策略的復雜,它所帶來的對代碼的簡化就更加明顯了。下面就稍微詳細一點的深入介紹一下:
定義錯誤(故障)
常見故障定義方式是指定委托執行過程中出現的特定異常,Polly中支持異常處理方式如下:
// 處理指定異常
Policy.Handle<DivideByZeroException>();
// 處理有條件的指定異常
Policy.Handle<SqlException>(ex => ex.Number == 1205);
// 處理多種異常
Policy.Handle<DivideByZeroException>()
.Or<ArgumentException>();
// 處理多種有條件的異常
Policy.Handle<SqlException>(ex => ex.Number == 1205)
.Or<ArgumentException>(ex => ex.ParamName == "example");
也支持異常的聚合:
Policy.Handle<ArgumentException>()
.Or<ArgumentException>();
另外,也支持通過返回值判斷是否故障:
// 指定錯誤的返回值
Policy.HandleResult<int>(ret => ret <= 0);
故障處理策略:重試
常見的處理策略是重試,Polly庫中內置了各種常用的重試策略:
// 重試1次
Policy.Handle<TimeoutException>().Retry();
// 重試多次
Policy.Handle<TimeoutException>().Retry(3);
// 無限重試
Policy.Handle<TimeoutException>().RetryForever();
也支持retry時增加一些額外的行為:
Policy.Handle<TimeoutException>().Retry(3, (err, countdown, context) =>
{
// log retry
});
也支持等待並重試:
// 等待並重試
Policy.Handle<TimeoutException>().WaitAndRetry(3, _ => TimeSpan.FromSeconds(3));
故障處理策略:回退(Fallback)
Fallback策略是在遇到故障是指定一個默認的返回值,
Policy<int>.Handle<TimeoutException>().Fallback(3);
Policy<int>.Handle<TimeoutException>().Fallback(() => 3);
當然,遇到沒有返回值的也可以指定故障時的處理方法,
Policy.Handle<TimeoutException>().Fallback(() => { });
使用Fallback時,異常被捕獲,返回默認的返回結果。
PS: 帶參數的Fallback處理方式貌似在5.0之后發生了變化,成了本文所示的方式,以前是Fallback<T>
故障處理策略:斷路保護(Circuit Breaker)
Circuit Breaker也是一種比較常見的處理策略,它可以指定一定時間內最大的故障發生次數,當超過了該故障次數時,在該時間段內,不再執行Policy內的委托。下面以一個簡單的示例演示下該策略的功能:
static void testPolicy()
{
var circuitBreaker = Policy.Handle<TimeoutException>()
.CircuitBreaker(3, TimeSpan.FromMinutes(1));
for (int i = 0; i < 5; i++)
{
try
{
circuitBreaker.Execute(DoSomething);
}
catch (Polly.CircuitBreaker.BrokenCircuitException e)
{
Console.WriteLine(e.Message);
}
catch (TimeoutException)
{
Console.WriteLine("timeout");
}
}
}
static int index = 0;
static int DoSomething()
{
Console.WriteLine($"DoSomething {index++}");
throw new TimeoutException();
}
執行結果如下:
DoSomething 0
timeout
DoSomething 1
timeout
DoSomething 2
timeout
The circuit is now open and is not allowing calls.
The circuit is now open and is not allowing calls.
可以看到,前面3次都能執行委托DoSomething,但出錯次數到達3次后,已經進入斷路保護章台,后面兩次調用直接返回BrokenCircuitException。直到達到保護時間超時后,對策略的調用才會再次執行DoSomething委托。
這種策略在調用遠程服務時非常實用,當一定時間內的調用都出錯時,往往可以認為服務提供者已經不可用,后續調用完全可以直接失敗,以避免重試的開銷。直到一定時間后才需要再次重試。
相對其它處理策略,CircuitBreaker是一個比較復雜的策略,它是有狀態的,可以通過CircuitState屬性獲取:
var state = circuitBreaker.CircuitState;
它有四種狀態:
- CircuitState.Closed - 常態,可執行actions。
- CircuitState.Open - 自動控制器已斷開電路,不允許執行actions。
- CircuitState.HalfOpen - 在自動斷路時間到時,從斷開的狀態復原。可執行actions,后續的action/s或控制的完成,會讓狀態轉至Open或Closed。
- CircuitState.Isolated - 在電路開路的狀態時手動hold住,不允許執行actions。
除了超時和策略執行失敗的這種自動方式外,也可以手動控制它的狀態:
// 手動打開(且保持)一個斷路器–例如手動隔離downstream的服務
circuitBreaker.Isolate();
// 重置一個斷路器回closed的狀態,可再次接受actions的執行
circuitBreaker.Reset();
更多的介紹可以參看官方文檔:Circuit Breaker
故障封裝策略(PolicyWrap)
我們可以通過PolicyWrap的方式,封裝出一個更加強大的策略:
var fallback = Policy<int>.Handle<TimeoutException>().Fallback(100);
var retry = Policy<int>.Handle<TimeoutException>().Retry(2);
var retryAndFallback = fallback.Wrap(retry);
這個策略就是將Retry和Fallback組合起來,形成一個retry and fallback的策略,也可以寫成如下形式:
Policy.Wrap(fallback, retry);
當執行這個新策略時:
retryAndFallback.Execute(DoSomething);
等價於執行:
fallback.Execute(()=> retry.Execute(DoSomething));
封裝策略本身是屬於彈性策略的范疇,這里只是提及一下,以演示Polly模塊強大的功能,關於彈性策略更多內容在下文中再做更詳細的介紹。