當我們從單體架構遷移到微服務模式時,其中一個比較大的變化就是模塊(業務,服務等)間的調用方式。在以前,一個業務流程的執行在一個進程中就完成了,但是在微服務模式下可能會分散到2到10個,甚至更多的機器(微服務)上,這必然就要使用網路進行通信。而網路本身就是不可靠的,並隨著每個服務都根據自身的情況進行的 ...
當我們從單體架構遷移到微服務模式時,其中一個比較大的變化就是模塊(業務,服務等)間的調用方式。在以前,一個業務流程的執行在一個進程中就完成了,但是在微服務模式下可能會分散到2到10個,甚至更多的機器(微服務)上,這必然就要使用網路進行通信。而網路本身就是不可靠的,並隨著每個服務都根據自身的情況進行的動態擴容,以及機器漂移等等。可以說,在微服務中,網路連接緩慢,資源繁忙,暫時不可用,服務離線等異常情況已然變成了一種常態。因此我們必須要有一種機制來保證服務整體的穩定性,而本文要介紹的熔斷降級就是一種很好的應對方案。
服務熔斷
在介紹熔斷之前,我們先來談談微服務中的雪崩效應。在微服務中,服務A調用服務B,服務B可能會調用服務C,服務C又可能調用服務D等等,這種情況非常常見。如果服務D出現不可用或響應時間過長,就會導致服務C原來越多的線程處於網路調用等待狀態,進而影響到服務B,再到服務A等,最後會耗盡整個系統的資源,導致整體的崩潰,這就是微服務中的“雪崩效應”。
而熔斷機制就是應對雪崩效應的一種鏈路保護機制。其實,對於熔斷這個詞我們並不陌生,在日常生活中經常會接觸到,比如:家用電力過載保護器,一旦電壓過高(發生漏電等),就會立即斷電,有些還會自動重試,以便在電壓正常時恢復供電。再比如:股票交易中,如果股票指數過高,也會採用熔斷機制,暫停股票的交易。同樣,在微服務中,熔斷機制就是對超時的服務進行短路,直接返回錯誤的響應信息,而不再浪費時間去等待不可用的服務,防止故障擴展到整個系統,併在檢測到該服務正常時恢復調用鏈路。
服務降級
當我們談到服務熔斷時,經常會提到服務降級,它可以看成是熔斷器的一部分,因為在熔斷器框架中,通常也會包含服務降級功能。
降級的目的是當某個服務提供者發生故障的時候,向調用方返回一個錯誤響應或者替代響應。從整體負荷來考慮,某個服務熔斷後,伺服器將不再被調用,此時客戶端可以自己準備一個本地的fallback回調,這樣,雖然服務水平下降,但總比直接掛掉的要好。比如:調用聯通介面伺服器發送簡訊失敗之後,改用移動簡訊伺服器發送,如果移動簡訊伺服器也失敗,則改用電信簡訊伺服器,如果還失敗,則返回“失敗”響應;再比如:在從推薦商品伺服器載入數據的時候,如果失敗,則改用從緩存中載入,如果緩存也載入失敗,則返回一些本地替代數據。
在某些情況下,我們也會採取主動降級的機制,比如雙十一活動等,由於資源的有限,我們也可以把少部分不重要的服務進行降級,以保證重要服務的穩定,待度過難關,再重新開啟。
Polly基本使用
在.Net Core中有一個被.Net基金會認可的庫Polly,它一種彈性和瞬態故障處理庫,可以用來簡化對服務熔斷降級的處理。主要包含以下功能:重試(Retry),斷路器(Circuit-breaker),超時檢測(Timeout),艙壁隔離(Bulkhead Isolation), 緩存(Cache),回退(FallBack)。
該項目作者現已成為.NET基金會一員,一直在不停的迭代和更新,項目地址: https://github.com/App-vNext/Polly。
策略
在Polly中,有一個重要的概念:Policy,策略有“故障定義”和“故障恢復”兩部分組成。故障是指異常、非預期的返回值等情況,而動作則包括重試(Retry)、熔斷(Circuit-Breaker)、Fallback(降級)等。
故障定義
故障也可以說是觸發條件,它使用Handle<T>
來定義,表示在什麼情況下,才對其進行處理(熔斷,降級,重試等)。
一個簡單的異常故障定義如下:
Policy.Handle<HttpRequestException>()
如上,表示當我們的代碼觸發HttpRequestException
異常時,才進行處理。
我們也可以對異常的信息進行過濾:
Policy.Handle<SqlException>(ex => ex.Number == 1205)
如上,只有觸發SqlException
異常,並且其異常號為1205
的時候才進行處理。
如果我們希望同時處理多種異常,可以使用Or<T>
來實現:
Policy.Handle<HttpRequestException>().Or<OperationCanceledException>()
Policy.Handle<SqlException>(ex => ex.Number == 1205).Or<ArgumentException>(ex => ex.ParamName == "example")
除此之外,我們還可以根據返回結果進行故障定義:
Policy.HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.NotFound)
如上,當返回值為HttpResponseMessage
,並且其StatusCode
為NotFound
時,才對其進行處理。更多用法參考:usage--fault-handling-policies。
故障恢復
當定義了故障後,要考慮便是如何對故障進行恢復了,Polly中常用的有以下幾種恢復策略:
重試(Retry)策略
重試就是指Polly在調用失敗時捕獲我們指定的異常,並重新發起調用,如果重試成功,那麼對於調用者來說,就像沒有發生過異常一樣。在網路調用中經常出現瞬時故障,那麼重試機制就非常重要。
一個簡單的重試策略定義如下:
// 當發生HttpRequestException異常時,重試3次
var retryPolicy = Policy.Handle<HttpRequestException>().Retry(3);
有些情況下,如果故障恢復的太慢,我們重試的過快是沒有任何任何意義的,這時可以指定重試的時間間隔:
Policy.Handle<HttpRequestException>().WaitAndRetryAsync(5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt - 1)));
如上,重試五次,並且重試時間指數級增加。
超時(Timeout)策略
超時是我們比較常見的,比如HttpClient
就可以設置超時時間,如果在指定的時間內還沒有返回,就觸發一個TimeoutException
異常,而Polly的超時機制與其類似,只不過超時時觸發的是一個TimeoutRejectedException
異常。
// 如果30秒種內沒有執行完成,就觸發`TimeoutRejectedException`異常
Policy.TimeoutAsync(30);
// 設置超時回調
Policy.TimeoutAsync(30, onTimeout: (context, timespan, task) =>
{
// do something
});
由於超時策略本身就是拋出一個超時異常,所以不需要設置觸發條件。
回退(FallBack)策略
回退也稱服務降級,用來指定發生故障時的備用方案。
Policy<string>.Handle<HttpRequestException>().FallbackAsync("substitute data", (exception, context) =>
{
// do something
});
如上,如果觸發HttpRequestException
異常時,就返回固定的substitute data
。
熔斷(Circuit-breaker)策略
斷路器用於在服務多次不可用時,快速響應失敗,保護系統故障免受過載。
Policy.Handle<HttpRequestException>().Or<TimeoutException>()
.CircuitBreakerAsync(
// 熔斷前允許出現幾次錯誤
exceptionsAllowedBeforeBreaking: 3,
// 熔斷時間
durationOfBreak: TimeSpan.FromSeconds(100),
// 熔斷時觸發
onBreak: (ex, breakDelay) =>
{
// do something
},
// 熔斷恢復時觸發
onReset: () =>
{
// do something
},
// 在熔斷時間到了之後觸發
onHalfOpen: () =>
{
// do something
}
);
如上,如果我們的業務代碼連續失敗3次,就觸發熔斷(onBreak),就不會再調用我們的業務代碼,而是直接拋出BrokenCircuitException
異常。當熔斷時間(100s)過後,切換為HalfOpen
狀態,觸發onHalfOpen
事件,此時會再調用一次我們的業務代碼,如果調用成功,則觸發onReset
事件,並解除熔斷,恢復初始狀態,否則立即切回熔斷狀態。
更多策略的用法查看:usage--general-resilience-policie。
執行
在上面的示例中,我們熟悉了各種策略的定義,那麼接下來就是執行它。也就是使用Polly
包裹我們的業務代碼,Polly
會攔截業務代碼中的故障,並根據指定的策略進行恢復。
最簡單的策略執行方式如下:
var policy = /*策略定義*/;
var res = await policy.ExecuteAsync(/*業務代碼*/);
如果需要同時指定多個策略,可以使用Policy.Wrap
來完成:
Policy.Wrap(retry, breaker, timeout).ExecuteAsync(/*業務代碼*/);
其實Warp
本質就是多個策略的嵌套執行,使用如下寫法效果是一樣的:
fallback.Execute(() => waitAndRetry.Execute(() => breaker.Execute(action)));
關於Polly
更詳細的用法可以查看Polly Github上的https://github.com/App-vNext/Polly/wiki,本文就不再過多介紹。
Polly熔斷降級實戰
場景:輪詢調用服務A和服務B,單次調用時間不得超過1s,調用失敗時自動切換到另外一個服務重試一次,如果都失敗,進行優雅的降級,返回模擬數據,併在2個服務都多次失敗後進行熔斷。
首先創建一個ASP.NET Core Console程式,命名為PollyDemo。
然後引入Polly的官方Nuge包:
dotnet add package Polly
在我們首先定義一個超時策略:
var timeoutPolicy = Policy.TimeoutAsync(1, (context, timespan, task) =>
{
Console.WriteLine("It's Timeout, throw TimeoutRejectedException.");
return Task.CompletedTask;
});
可以根據實際情況來設置超時時間,我這裡為了方便測試,就設置為1s。
然後定義重試策略:
var retryPolicy = Policy.Handle<HttpRequestException>().Or<TimeoutException>().Or<TimeoutRejectedException>()
.WaitAndRetryAsync(
retryCount: 2,
sleepDurationProvider: retryAttempt =>
{
var waitSeconds = TimeSpan.FromSeconds(Math.Pow(2, retryAttempt - 1));
Console.WriteLine(DateTime.Now.ToString() + "-Retry:[" + retryAttempt + "], wait " + waitSeconds + "s!");
return waitSeconds;
});
再定義一個熔斷策略:
var circuitBreakerPolicy = Policy.Handle<HttpRequestException>().Or<TimeoutException>().Or<TimeoutRejectedException>()
.CircuitBreakerAsync(
// 熔斷前允許出現幾次錯誤
exceptionsAllowedBeforeBreaking: 2,
// 熔斷時間
durationOfBreak: TimeSpan.FromSeconds(3),
// 熔斷時觸發
onBreak: (ex, breakDelay) =>
{
Console.WriteLine(DateTime.Now.ToString() + "Breaker->Breaking the circuit for " + breakDelay.TotalMilliseconds + "ms! Exception: ", ex.Message);
},
// 熔斷恢復時觸發
onReset: () =>
{
Console.WriteLine(DateTime.Now.ToString() + "Breaker->Call ok! Closed the circuit again.");
},
// 在熔斷時間到了之後觸發
onHalfOpen: () =>
{
Console.WriteLine(DateTime.Now.ToString() + "Breaker->Half-open, next call is a trial.");
}
);
如上,連續錯誤2次就熔斷3秒。
最後,再定義一個回退策略:
var fallbackPolicy = Policy<string>.Handle<Exception>()
.FallbackAsync(
fallbackValue: "substitute data",
onFallbackAsync: (exception, context) =>
{
Console.WriteLine("It's Fallback, Exception->" + exception.Exception.Message + ", return substitute data.");
return Task.CompletedTask;
});
我們的業務代碼如下:
private List<string> services = new List<string> { "localhost:5001", "localhost:5002" };
private int serviceIndex = 0;
private HttpClient client = new HttpClient();
private Task<string> HttpInvokeAsync()
{
if (serviceIndex >= services.Count)
{
serviceIndex = 0;
}
var service = services[serviceIndex++];
Console.WriteLine(DateTime.Now.ToString() + "-Begin Http Invoke->" + service);
return client.GetStringAsync("http://" + service + "/api/values");
}
這裡方便測試,直接寫死了兩個服務,對其輪詢調用,在生產環境中可以參考上一篇《服務發現之Consul》來實現服務發現和負載均衡。
現在,我們組合這些策略來調用我們的業務代碼:
for (int i = 0; i < 100; i++)
{
Console.WriteLine(DateTime.Now.ToString() + "-Run[" + i + "]-----------------------------");
var res = await fallbackPolicy.WrapAsync(Policy.WrapAsync(circuitBreakerPolicy, retryPolicy, timeoutPolicy)).ExecuteAsync(HttpInvokeAsync);
Console.WriteLine(DateTime.Now.ToString() + "-Run[" + i + "]->Response" + ": Ok->" + res);
await Task.Delay(1000);
Console.WriteLine("--------------------------------------------------------------------------------------------------------------------");
}
如上,迴圈執行100次,策略的執行是非常簡單的,唯一需要註意的就是調用的順序:如上是依次從右到左進行調用,首先是進行超時的判斷,一旦超時就觸發TimeoutRejectedException
異常,然後就進入到了重試策略中,如果重試了一次就成功了,那就直接返回,不再觸發其他策略,否則就進入到熔斷策略中:
如上圖,服務A(localhost:5001)和服務B(localhost:5001),都沒有啟動,所以會一直調用失敗,最後熔斷器開啟,並最終被降級策略攔截,返回substitute data
。
現在我們啟動服務A,可以看到服務會自動恢復,解除熔斷狀態:
總結
本篇首先講解了一下微服務中熔斷、降級的基本概念,然後對.Net Core中的Polly
框架做了一個基本介紹,最後基於Polly
演示瞭如何在.NET Core中實現熔斷降級來提高服務質量。而熔斷本質上只是一個保護殼,在周圍出現異常的時候保全自身,從長遠來看,平時定期做好壓力測試才能防範於未然,降低觸發熔斷的次數。如果清楚的知道每個服務的承載量,並做好服務限流的控制,就能將“高壓”下觸發熔斷的概率降到最低了。那下一篇就來介紹一下速率限制(Rate Limiting),敬請期待!