NET Core微服務之路:彈性和瞬態故障處理庫Polly的介紹


前言

上一節中我們介紹了Ocelot的常見使用配置,通過json配置文件,實現API網關的請求處理。和一個使用DownStream擴展下游中間件,來實現Http轉RPC的簡單實現,功能不算強大,但可以作為一個思路,根據自己的RestFul或業務需求來規范下游中間件的處理功能,也有幸被張隊收錄,十分感謝。
我們知道,Consul、Etcd、Zookeeper等等這些注冊中心都有健康檢查的機制,用於檢查服務節點的狀態,是200,還是非200。但是,這種檢測是粗粒度的,她只能檢測節點的健康狀態,卻不能檢測接口的健康狀態,畢竟細粒度的控制太多由業務環境支配,無法統一化和標准化。本節我們介紹如何在接口(或方法)中如何實現健康狀態的檢測,其實也就是對某個接口的故障保護。
 
先來了解兩個重要的名詞定義。
 

瞬態故障:

一種僅短暫影響電氣設備的介電性能,且可在短時間內自行恢復的故障;電力系統中90%以上的故障都是瞬態故障或由瞬態故障擴大的--來自百度百科。
很難理解吧,沒關系,我們換一個解釋方法:
當你有一台服務器,服務上運行着一個“秒殺”系統,理論測試下承載的最大並發數是10W,而當實際運行時,瞬間涌入的請求量達到了1000W,如果你沒做前置消峰處理(比如使用隊列),服務器立馬宕機,甚至燒毀的可能性。
再或者,這台服務器的電壓是220V,運行電流是10A,如果服務器電源沒做瞬時電流保護處理,那么電源第一個被燒壞,然后是CPU,接着是主板,再然后是硬盤,等等不可恢復的災難。
這種故障是瞬間的,一般都在一秒以內,甚至微秒內發生,一旦出現,如果沒有保護錯誤,那么必將造成重大災難。
 

過載故障:

在我們的家用電箱中,都會有這么一個重要的器件,如下圖所示(它叫空開,也叫斷路器),當電流或電壓超過斷路器的額定工作范圍,便會自動斷開,從常閉狀態改變為常開狀態,阻斷整個線路的流通情況(同樣稱之為過載保護)
我們知道,我國規定的市電壓是AC220V,50HZ,工業電壓是AC380V,50HZ。當輸入電壓超過這個標准,斷路器(比如額定輸入AC220V,50HZ,60A,而實際輸入AC300V,50HZ,100A)將會斷開不合格的輸入,達到保護的目的。
切換到我們的軟件系統中,比如我們規定一台服務器的最大承載並發數是10W范圍,超過這個范圍服務器將會運行緩慢、甚至宕機。通過某個軟件、或中間件、或物理設備控制着這個輸入端,當輸入量超過這個范圍值,這個輸入端將自動斷開,達到保護服務的目的。
 
咋一看,瞬態故障和過載故障的意義都差不多,都是為了控制和防止過高或過大的輸入,其實不然
從時間點:瞬態故障的動作范圍極短,遠遠超過你眨眼睛的速度。而過載故障有可能是長時間的、超過這個額定范圍的輸入。
從發生點:瞬態故障發生在“秒殺”、“搶購”等瞬間存在的高並發系統中,而過載故障一般均是設計承載和實際承載不符造成的。
從輸入量:瞬態故障的輸入量一般是正常輸入量的十幾倍(電氣中甚至上百倍),而過載故障一般是正常范圍的幾倍,最高也就是幾十倍(這都很嚇人了)。
 
好了,理解了上面兩個主要故障,接下來我們看看有沒有什么技術可以減少這種故障的出現的幾率(或保護我們的整個系統)。
 

Polly介紹

我們來介紹一款強大的庫Polly,Polly是一種.NET 彈性瞬態故障處理庫,允許我們以非常順暢和線程安全的方式來執諸如行重試,斷路,超時,故障恢復等策略,並且已經被.NET基金會認可的彈性和瞬態處理庫。支持目標.NET Framework 4.5+和.NET Core1.0+以上的基礎框架庫。
 

安裝最新的Polly庫

目前最新的官方庫是7.0.3,如下所示。
(我喜歡在不同的操作系統上得到相同的開發體驗,jetbrains幫我實現了)
 

Polly故障策略(措施)

該庫目前按照官方的文檔,已經實現了七種彈性策略。戳我查看原文( https://github.com/App-vNext/Polly#resilience-policies
重試策略:許多故障是暫時性的、並且可以在短時間延遲后可以自行糾正:Maybe it's just a blip。
斷路器:斷路器策略針對當系統繁忙時(或者系統出現故障),快速響應失敗總比讓用戶一直等待更好:Stop doing it if it hurts。
超時:超時策略針對的條件是超過一定的等待時間,如果還未響應,視為超時,保證調用者不必等待到執行超時:Give that system a break。
隔離:隔離針對的條件是當進程出現故障時,多個失敗一直在主機中對資源(例如線程/ CPU)一直占用,下游系統故障也可能導致上游失敗,這些風險都將造成嚴重的后果:One fault shouldn't sink the whole ship。
緩存:緩存策略針對的條件是數據不會很頻繁的進行更新,為了避免系統過載,首次加載數據時將響應數據進行緩存,如果緩存中存在則直接從緩存中讀取:You've asked that one before
回退:不管重試多次,操作仍然會失敗,也就是說,當發生這樣的事情時我們打算做什么,撤銷之前的操作等等都可以:Degrade gracefully。
策略包裝:策略包裝針對的條件是不同的故障需要不同的策略,也就意味着,可以彈性靈活的組合不同的策略來實現不同的故障保護措施:Defence in depth。
 

一個簡單的異常

一般我們在處理異常的時候,都會習慣性用try.catch來處理,比如這樣一個不能除以0的數學異常
try
{
    var z = 0;
    var r = 1 / z;
}
catch (DivideByZeroException ex)
{
    throw ex;
}

很友好,也很直觀。但假如這不是一個除以0的數學異常,而只是一個其他異常,並且需要我們調用端通過重試或者等待一段時間后才能正常調用的處理呢,比如一個異步的操作方法,我不能將上面的代碼加上個for循環,或者調個線程阻塞一下吧,不管理論上為了目的和結果是可以這樣寫,我們重試10次,可以寫成這樣。

for (int i = 0; i < 10; i++)
{
    try
    {
        var z = 0;
        var r = 1 / z;
    }
    catch (DivideByZeroException ex)
    {
        throw ex;
    }    
}
這樣的代碼,確實滿足了我們對異常的友好處理,不過,有了Polly,我們還能更加友好,比如什么重試,超時,或者延時等等,都可以更好的實現。

 

Policy定義

故障定義

常見故障定義方式是指定委托執行過程中出現的特定異常
// 特定異常
Policy.Handle<DivideByZeroException>();
// 條件異常
Policy.Handle<ArgumentException>(ex => ex.HResult == 9999);
// 多重異常
Policy.Handle<DivideByZeroException>().Or<ArgumentException>();
// 聚合異常
Policy.Handle<ArgumentException>().Or<ArgumentException>();
通過Policy.Handle<TException>進行定義異常處理。
特定異常:正如上面“一個簡單的異常”中拋出的DivideByZeroException的異常一樣,自行指定一個異常類型。
注意:你也可以使用Exception來作為異常處理的類型,但這樣的范圍太大,所有.NET的異常都會經過這個處理,不推薦這樣寫。
條件異常:對某個異常指定一個過濾條件。
多重異常:對一個操作中出現多個不同異常的統一異常處理。
聚合異常:一個操作中,可能存在多個相同的異常,把他們合並為一個異常進行處理。
 

結果定義

指定要處理的返回結果
// 處理帶條件的返回值
Policy.HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.NotFound);
// 處理多個條件的返回值
Policy.HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.InternalServerError)
    .OrResult(r => r.StatusCode == HttpStatusCode.BadGateway);
// 結果判斷
Policy.HandleResult<int>(ret => ret <= 0);
通過Policy.HandleResult<TResult>進行定義結果處理。
上面這段代碼不用過多的解釋,相信都懂,將符合指定條件的結果過濾掉並返回。
 

故障處理策略定義

重試定義
指定策略應如何處理這些錯誤,常見的處理策略是重試
// 重試1次
Policy.Handle<TimeoutException>().Retry();
// 重試3次
Policy.Handle<TimeoutException>().Retry(3);
// 無限重試
Policy.Handle<TimeoutException>().RetryForever();
// 重試多次,每次重試都調用一個操作
Policy.Handle<TimeoutException>().Retry(3, (exception, retryCount) =>
{
    // do something
});
// 重試固定時間間隔 (1)
Policy.Handle<TimeoutException>().WaitAndRetry(3, _ => TimeSpan.FromSeconds(3));
// 重試指定時間時間 (2)
Policy.Handle<TimeoutException>().WaitAndRetry(new[]
{
    TimeSpan.FromSeconds(1),
    TimeSpan.FromSeconds(2),
    TimeSpan.FromSeconds(3)
}, (exception, timeSpan, retryCount, context) =>
{
    // do something
});
通過Policy.HandleResult<TResult>().Retry()進行定義重試處理次數和間隔時間。
(1):指定重試次數為3次,且每次重試的時間間隔是3秒
(2):指定重試次數為3次(數組大小),且每次重試間隔時間是1秒,2秒,3秒。
 

回退定義

Fallback策略是在遇到故障是指定一個默認的返回值,也是俗稱的降級操作。
// 返回一個值
Policy<int>.Handle<TimeoutException>().Fallback(99);
Policy<int>.Handle<TimeoutException>().Fallback(() => 99);
// 或將返回值定義為一個方法
Policy.Handle<TimeoutException>().Fallback(() => { });

 

斷路保護定義

Circuit Breaker也是一種比較常見的處理策略,它可以指定一定時間內最大的故障發生次數,當超過了該故障次數時,在該時間段內,不再執行Policy內的委托操作。
// 在指定的連續異常數后斷開,並在指定的持續時間內保持斷開。
Policy.Handle<TimeoutException>().CircuitBreaker(2, TimeSpan.FromMinutes(1));

// 在指定的連續異常數后斷開,並在規定的時間內保持電路斷開,且調用一個改變狀態的操作。
var circuitBreaker = Policy.Handle<TimeoutException>().CircuitBreaker(2, TimeSpan.FromMinutes(1),
    (exception, timespan) =>
    {
        // On Break
    },
    () =>
    {
        // On Reset
    });

// 獲取當前斷路器的狀態 (1)
var circuitState = circuitBreaker.CircuitState;

// 除了超時和策略執行失敗的這種自動方式外,也可以手動控制它的狀態:
// 手動打開(且保持)一個斷路器,例如手動隔離downstream服務
circuitBreaker.Isolate();

// 重置一個斷路器回closed的狀態,可再次接受actions的執行
circuitBreaker.Reset();
通過Policy.HandleResult<TResult>().CircuitBreaker()進行定義重試處理次數后的斷開操作處理。
Closed - 斷路器的常態,可執行后續Action,如同線路上的開關處於關閉狀態。
Open - 斷路器已打開,不允許執行Action,如果線路上的開關處於打開狀態。
HalfOpen - 在自動斷路時間到時,從斷開的狀態復原。
Isolated - 在斷開的狀態時手動hold住,不允許執行Action。
 

策略定義(彈性策略)

我們可以通過PWrap的方式(Polly的策略的封裝屬於彈性策略),封裝出一個更加強大的自定義策略。
// 一個簡單混合(彈性)策略:當重試2次后,自動回退100
var fallback = Policy<int>.Handle<TimeoutException>().Fallback(100);
var retry = Policy<int>.Handle<TimeoutException>().Retry(2);
var policyWrap = Policy.Wrap(fallback, retry);
policyWrap.Execute(() => { return 0; });

// 超時策略用於控制委托的運行時間,如果達到指定時間還沒有運行,則觸發超時異常。
Policy.Timeout(TimeSpan.FromSeconds(3), TimeoutStrategy.Pessimistic); (1// 無操作策略(NoOp),啥也不不干
Policy.NoOp();

// 艙壁隔離(Bulkhead Isolation)
// 艙壁隔離是一種並發控制的行為,並發控制是一個比較常見的模式,Polly也提供了這方面的支持
Policy.Bulkhead(12);

// 超過了並發數的任務會拋BulkheadRejectedException,如果要放在隊列中等待
// 這種方式下,有12個並發任務,每個任務維持着一個並發隊列,每個隊列可以自持最大100個任務。
Policy.Bulkhead(12, 100);
通過Policy.Wrap()可以混合多種策略進行彈性處理。
(1):Polly支持兩種超時策略:
Pessimistic: 悲觀模式
當委托到達指定時間沒有返回時,不繼續等待委托完成,並拋超時TimeoutRejectedException異常。
Optimistic:樂觀模式
這個模式依賴於CancellationToken,需要等待委托自行終止操作。
其中悲觀模式比較容易使用,因為它不需要在委托額外的操作,但由於它本身無法控制委托的運行,函數本身並不知道自己被外圍策略取消了,也無法在超時的時候中斷后續行為,因此用起來反而還不是那么實用。
 

重試操作

public static void Retry()
{
    var tick = 0;
    const int maxRetry = 6;
    var retry = Policy.Handle<Exception>().Retry(maxRetry);

    try
    {
        retry.Execute(() =>
        {
            Console.WriteLine($@"try {++tick}");
            if (tick >= 1)
                // 出現故障,開始重試Execute
                throw new Exception("throw the exception");
        });
    }
    catch (Exception ex)
    {
        Console.WriteLine(@"exception : " + ex.Message);
    }
}

 我們定義一個最大重試次數為6的常亮,和一個為Retry的Policy委托,通過委托執行retry.Execute()方法中,強制拋出一個異常,每拋出一次異常,將計數器+1(實際重試了7次),執行結果如下:

try 1
try 2
try 3
try 4
try 5
try 6
try 7
exception : throw the exception

 

回退操作

public static void Fallback()
{
    Policy.Handle<ArgumentException>().Fallback(() => { Console.WriteLine(@"error occured"); })
        .Execute(() =>
        {
            Console.WriteLine(@"try");
            // 出現故障,進行降級處理Fallback
            throw new ArgumentException(@"throw the exception");
        });
}

以上代碼解釋:當委托方法中出現異常,在回退操作的控制台中輸出一句話“error occured”,執行結果如下:

try
error occured

 

緩存操作

public static void Cache()
{
    const int ttl = 60;
    var policy = Policy.Cache(new MemoryCacheProvider(new MemoryCache(new MemoryCacheOptions())), TimeSpan.FromSeconds(ttl));
    var context = new Context(operationKey: "cache_key");
    for (var i = 0; i < 3; i++)
    {
        var cache = policy.Execute(_ =>
        {
            Console.WriteLine(@"get value");
            return 3;
        }, context);
        Console.WriteLine(cache);
    }
}

Polly支持Cache的操作,你可以使用Memory、Redis等緩存提供器來支持Polly的緩存處理。代碼中,我們使用Context來設置一個Key,通過委托執行3次,每次都通過這個緩存來獲取值,執行結果如下:

get value
3
3
3

 

斷路操作

public static void CircuitBreaker()
{
    var tick = 0;
    const int interval = 10;
    const int maxRetry = 6;
    var circuitBreaker = Policy.Handle<Exception>().CircuitBreaker(maxRetry, TimeSpan.FromSeconds(interval));

    while (true)
    {
        try
        {
            circuitBreaker.Execute(() =>
            {
                Console.WriteLine($@"try {++tick}");
                throw new Exception("throw the exception");
            });
        }
        catch (Exception ex)
        {
            Console.WriteLine(@"exception : " + ex.Message);

            // 當重試次數達到斷路器指定的次數時,Polly會拋出The circuit is now open and is not allowing calls. 斷路器已打開,不允許訪問
            // 為了演示,故意將下面語句寫上,可退出while循環
            // 實際環境中視情況,斷開絕不等於退出,或許20-30秒后,服務維護后變得可用了
            if (ex.Message.Contains("The circuit is now open and is not allowing calls"))
            {
                break;
            }
        }

        Thread.Sleep(300);
    }
}

這段代碼稍微有點多,我們先看看執行結果:

try 1
exception : throw the exception
try 2
exception : throw the exception
try 3
exception : throw the exception
try 4
exception : throw the exception
try 5
exception : throw the exception
try 6
exception : throw the exception
exception : The circuit is now open and is not allowing calls.
從執行結果中我們可以看到,一共嘗試重試了6次,超過6次后,直接返回The circuit is now open and is not allowing calls(斷路器已打開,不允許調用)。
 

彈性操作

我們用一個超時來處理策略組合的彈性操作。
public static void Timeout()
{
    const int timeoutSecond = 3;

    try
    {
        Policy.Wrap(
            Policy.Timeout(timeoutSecond, TimeoutStrategy.Pessimistic),
            Policy.Handle<TimeoutRejectedException>().Fallback(() => { })
        ).Execute(() =>
        {
            Console.WriteLine(@"try");
            Thread.Sleep(5000);
        });
    }
    catch (Exception ex)
    {
        // 當超時時間到,會拋出The delegate executed through TimeoutPolicy did not complete within the timeout.
        // 委托執行未在指定時間內完成
        Console.WriteLine($@"exception : {ex.GetType()} : {ex.Message}");
    }
}

 以上策略很好理解,組合一個超時和回退的策略組合,當超時已到,執行回退操作,執行結果如下:

try
exception : Polly.Timeout.TimeoutRejectedException : The delegate executed through TimeoutPolicy did not complete within the timeout.
策略組合是Polly中強大的彈性操作核心,我們可以結合實際業務場景,來組合多個策略以滿足我們對故障保護的需求,以下是Polly中Warp的方法源碼:
 

總結

本節只是簡單的介紹了一下Polly這個彈性框架庫的使用范圍,我們可以通過它的故障定義和委托來實現熔斷,降級,隔離,重試等操作,實現方式也非常簡單(.Net的語法糖確實很甜),更強大的是她的彈性策略,結合到我們實際的業務中,可以保護(或處理)很多實際業務場景。比如一個支付失敗的操作,可以嘗試指定次數后回滾之前的支付操作,保護支付合規性。再或者,服務節點中某個接口存在異常,立馬阻斷與調用端的連接,並隔離這個異常的接口,等等業務場景。並且,不僅在Ocelot中也集成了這強大的彈性庫,其他非常多的.Net開源框架也默認集成了Polly。

 

感謝閱讀!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM