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

浙公網(wǎng)安備 33010602011771號(hào)