在進入SOA之后,我們的代碼從本地方法調用變成了跨機器的通信。任何一個新技術的引入都會為我們解決特定的問題,都會帶來一些新的問題。比如網絡故障、依賴服務崩潰、超時、服務器內存與CPU等其它問題。正是因為這些問題無法避免,所以我們在進行系統設計、特別是進行分布式系統設計的時候以“Design For Failure”(為失敗而設計)為指導原則。把一些邊緣場景以及服務之間的調用發生的異常和超時當成一定會發生的情況來預先進行處理。
Design For Failure
1. 一個依賴服務的故障不會嚴重破壞用戶的體驗。
2. 系統能自動或半自動處理故障,具備自我恢復能力。
以下是一些經驗的服務容錯模式
- 超時與重試(Timeout and Retry)
- 限流(Rate Limiting)
- 熔斷器(Circuit Breaker)
- 艙壁隔離(Bulkhead Isolation)
- 回退(Fallback)
如果想詳細了解這幾種模式可以參考美團技術團隊的總結:服務容錯模式。我們今天要講的是,thanks to the community 多謝社區, Polly已經為我們實現了以上全部的功能。Polly是一個C#實現的彈性瞬時錯誤處理庫(resilience and transient-fault-handling library一直覺得這個英文翻譯不是很好) 。在Polly中,對這些服務容錯模式分為兩類:
- 錯誤處理fault handling :重試、熔斷、回退
- 彈性應變resilience:超時、艙壁、緩存
可以說錯誤處理是當錯誤已經發生時,防止由於該錯誤對整個系統造成更壞的影響而設置。而彈性應變,則在是錯誤發生前,針對有可能發生錯誤的地方進行預先處理,從而達到保護整個系統的目地。
Polly 錯誤處理使用三步曲
- 定義條件: 定義你要處理的 錯誤異常/返回結果
- 定義處理方式 : 重試,熔斷,回退
- 執行
先看一個簡單的例子
// 這個例子展示了當DoSomething方法執行的時候如果遇到SomeExceptionType的異常則會進行重試調用。
var policy = Policy .Handle<SomeExceptionType>() // 定義條件 .Retry(); // 定義處理方式 // 執行 policy.Execute(() => DoSomething());
定義條件
我們可以針對兩種情況來定義條件:錯誤異常和返回結果。
// 單個異常類型 Policy .Handle<HttpRequestException>() // 限定條件的單個異常 Policy .Handle<SqlException>(ex => ex.Number == 1205) // 多個異常類型 Policy .Handle<HttpRequestException>() .Or<OperationCanceledException>() // 限定條件的多個異常 Policy .Handle<SqlException>(ex => ex.Number == 1205) .Or<ArgumentException>(ex => ex.ParamName == "example") // Inner Exception 異常里面的異常類型 Policy .HandleInner<HttpRequestException>() .OrInner<OperationCanceledException>(ex => ex.CancellationToken != myToken)
以及用返回結果來限定
// 返回結果加限定條件 Policy .HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.NotFound) // 處理多個返回結果 Policy .HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.InternalServerError) .OrResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.BadGateway) // 處理元類型結果 (用.Equals) Policy .HandleResult<HttpStatusCode>(HttpStatusCode.InternalServerError) .OrResult<HttpStatusCode>(HttpStatusCode.BadGateway) // 在一個policy里面同時處理異常和返回結果。 HttpStatusCode[] httpStatusCodesWorthRetrying = { HttpStatusCode.RequestTimeout, // 408 HttpStatusCode.InternalServerError, // 500 HttpStatusCode.BadGateway, // 502 HttpStatusCode.ServiceUnavailable, // 503 HttpStatusCode.GatewayTimeout // 504 }; HttpResponseMessage result = Policy .Handle<HttpRequestException>() .OrResult<HttpResponseMessage>(r => httpStatusCodesWorthRetrying.Contains(r.StatusCode)) .RetryAsync(...) .ExecuteAsync( /* some Func<Task<HttpResponseMessage>> */ )
定義處理方式
在這里使用的處理方式就是我們最開始說的服務容錯模式,我們將介紹以下三種:重試、熔斷、回退。
重試
重試很好理解,當發生某種錯誤或者返回某種結果的時候進行重試。Polly里面提供了以下幾種重試機制
- 按次數重試
- 不斷重試(直到成功)
- 等待之后按次數重試
- 等待之后不斷重試(直到成功)
按次數重試
// 重試1次 Policy .Handle<SomeExceptionType>() .Retry() // 重試3(N)次 Policy .Handle<SomeExceptionType>() .Retry(3) // 重試多次,加上重試時的action參數 Policy .Handle<SomeExceptionType>() .Retry(3, (exception, retryCount) => { // 干點什么,比如記個日志之類的 });
不斷重試
// 不斷重試,直到成功 Policy .Handle<SomeExceptionType>() .RetryForever() // 不斷重試,帶action參數在每次重試的時候執行 Policy .Handle<SomeExceptionType>() .RetryForever(exception => { // do something });
等待之后重試
// 重試3次,分別等待1、2、3秒。 Policy .Handle<SomeExceptionType>() .WaitAndRetry(new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(3) });
當然也可以在每次重試的時候添加一些處理,這里我們可以從上下文中獲取一些數據,這些數據在policy啟動執行的時候可以傳進來。
Policy .Handle<SomeExceptionType>() .WaitAndRetry(new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(3) }, (exception, timeSpan, context) => { // do something });
把WiatAndRetry抱成WaitAndRetryForever()則可以實現重試直到成功。
熔斷
熔斷也可以被作為當遇到某種錯誤場景下的一個操作。以下代碼展示了當發生2次SomeExceptionType的異常的時候則會熔斷1分鍾,該操作后續如果繼續嘗試執行則會直接返回錯誤 。
Policy .Handle<SomeExceptionType>() .CircuitBreaker(2, TimeSpan.FromMinutes(1));
可以在熔斷和恢復的時候定義委托來做一些額外的處理。onBreak會在被熔斷時執行,而onReset則會在恢復時執行。
熔斷器狀態
我們的CircuitBreakPolicy的State定義了當前熔斷器的狀態,我們也可能調用它的Is
Action<Exception, TimeSpan> onBreak = (exception, timespan) => { ... }; Action onReset = () => { ... }; CircuitBreakerPolicy breaker = Policy .Handle<SomeExceptionType>() .CircuitBreaker(2, TimeSpan.FromMinutes(1), onBreak, onReset);
olate和Reset方法來手動熔斷和恢復 。
CircuitState state = breaker.CircuitState;
- Closed 關閉狀態,允許執行
- Open 自動打開,執行會被阻斷
- Isolate 手動打開,執行會被阻斷
- HalfOpen 從自動打開狀態恢復中,在熔斷時間到了之后從Open狀態切換到Closed
// 手動打開熔斷器,阻止執行 breaker.Isolate(); // 恢復操作,啟動執行 breaker.Reset();
回退(Fallback)
// 如果執行失敗則返回UserAvatar.Blank Policy .Handle<Whatever>() .Fallback<UserAvatar>(UserAvatar.Blank) // 發起另外一個請求去獲取值 Policy .Handle<Whatever>() .Fallback<UserAvatar>(() => UserAvatar.GetRandomAvatar()) // where: public UserAvatar GetRandomAvatar() { ... } // 返回一個指定的值,添加額外的處理操作。onFallback Policy .Handle<Whatever>() .Fallback<UserAvatar>(UserAvatar.Blank, onFallback: (exception, context) => { // do something });
執行polly policy
為我聲明了一個Policy,並定義了它的異常條件和處理方式,那么接下來就是執行它。執行是把我們具體要運行的代碼放到Policy里面。
// 執行一個Action var policy = Policy .Handle<SomeExceptionType>() .Retry(); policy.Execute(() => DoSomething());
這就是我們最開始的例子,還記得我們在異常處理的時候有一個context上下文嗎?我們可以在執行的時候帶一些參數進去
// 看我們在retry重試時被調用的一個委托,它可以從context中拿到我們在execute的時候傳進來的參數 。 var policy = Policy .Handle<SomeExceptionType>() .Retry(3, (exception, retryCount, context) => { var methodThatRaisedException = context["methodName"]; Log(exception, methodThatRaisedException); }); policy.Execute( () => DoSomething(), new Dictionary<string, object>() {{ "methodName", "some method" }} );
當然,我們也可以將Handle,Retry, Execute 這三個階段都串起來寫。
Policy .Handle<SqlException>(ex => ex.Number == 1205) .Or<ArgumentException>(ex => ex.ParamName == "example") .Retry() .Execute(() => DoSomething());
Polly 彈性應變處理Resilience
我們在上面講了Polly在錯誤處理方面的使用,接下來我們介紹Polly在彈性應變這塊的三個應用: 超時、艙壁和緩存。
超時
Policy .Timeout(TimeSpan.FromMilliseconds(2500))
支持傳入action回調
Policy .Timeout(30, onTimeout: (context, timespan, task) => { // do something });
超時分為樂觀超時與悲觀超時,樂觀超時依賴於CancellationToken ,它假設我們的具體執行的任務都支持CancellationToken。那么在進行timeout的時候,它會通知執行線程取消並終止執行線程,避免額外的開銷。下面的樂觀超時的具體用法 。
// 聲明 Policy Policy timeoutPolicy = Policy.TimeoutAsync(30); HttpResponseMessage httpResponse = await timeoutPolicy .ExecuteAsync( async ct => await httpClient.GetAsync(endpoint, ct), CancellationToken.None // 最后可以把外部的 CacellationToken附加到 timeoutPollcy的 CT上,在這里我們沒有附加 );
悲觀超時與樂觀超時的區別在於,如果執行的代碼不支持取消CancellationToken,它還會繼續執行,這會是一個比較大的開銷。
Policy .Timeout(30, TimeoutStrategy.Pessimistic)
上面的代碼也有悲觀sad...的寫法
Policy timeoutPolicy = Policy.TimeoutAsync(30, TimeoutStrategy.Pessimistic); var response = await timeoutPolicy .ExecuteAsync( async () => await FooNotHonoringCancellationAsync(), );// 在這里我們沒有 任何與CancllationToken相關的處理
艙壁
在開頭的那篇文章中詳細解釋了艙壁這種模式,它用來限制某一個操作的最大並發執行數量 。比如限制為12
Policy .Bulkhead(12)
同時,我們還可以控制一個等待處理的隊列長度
Policy .Bulkhead(12, 2)
以及當請求執行操作被拒絕的時候,執行回調
Policy .Bulkhead(12, context => { // do something });
緩存
Polly的緩存需要依賴於一個外部的Provider。
var memoryCacheProvider = new Polly.Caching.MemoryCache.MemoryCacheProvider(MemoryCache.Default); var cachePolicy = Policy.Cache(memoryCacheProvider, TimeSpan.FromMinutes(5)); // 設置一個絕對的過期時間 var cachePolicy = Policy.Cache(memoryCacheProvider, new AbsoluteTtl(DateTimeOffset.Now.Date.AddDays(1)); // 設置一個滑動的過期時間,即每次使用緩存的時候,過期時間會更新 var cachePolicy = Policy.Cache(memoryCacheProvider, new SlidingTtl(TimeSpan.FromMinutes(5)); // 我們用Policy的緩存機制來實現從緩存中讀取一個值,如果該值在緩存中不存在則從提供的函數中取出這個值放到緩存中。 // 借且於Polly Cache 這個操作只需要一行代碼即可。 TResult result = cachePolicy.Execute(() => getFoo(), new Context("FooKey")); // "FooKey" is the cache key used in this execution. // Define a cache Policy, and catch any cache provider errors for logging. var cachePolicy = Policy.Cache(myCacheProvider, TimeSpan.FromMinutes(5), (context, key, ex) => { logger.Error($"Cache provider, for key {key}, threw exception: {ex}."); // (for example) } );
組合Policy
最后我們要說的是如何將多個policy組合起來。大致的操作是定義多個policy,然后用Wrap方法即可。
var policyWrap = Policy .Wrap(fallback, cache, retry, breaker, timeout, bulkhead); policyWrap.Execute(...)
在另一個Policy聲明時組合使用其它外部聲明的Policy。
PolicyWrap commonResilience = Policy.Wrap(retry, breaker, timeout); Avatar avatar = Policy .Handle<Whatever>() .Fallback<Avatar>(Avatar.Blank) .Wrap(commonResilience) .Execute(() => { /* get avatar */ });
寫在后面
上一篇我們介紹了《asp.net core開源api 網關Ocelot的中文使用文檔》,Ocelot里面的一些關於Qos服務質量的處理就是用Polly來實現的。當然在沒有網關介入的情況 下,我們也可以單獨來使用Polly做彈性應對和瞬時錯誤處理。關於分布式架構,這是一個很大的話題,我們后面繼續展示,歡迎關注 。