天天看點

服務熔斷服務熔斷的了解

服務熔斷的了解

服務熔斷也稱服務隔離,來自于Michael Nygard 的《Release It》中的CircuitBreaker應用模式,Martin Fowler在博文CircuitBreaker中對此設計進行了比較詳細說明。

本文認為服務熔斷是服務降級的措施。服務熔斷對服務提供了proxy,防止服務不可能時,出現串聯故障(cascading failure),導緻雪崩效應。服務熔斷一般是某個服務(下遊服務)故障引起,而服務降級一般是從整體負荷考慮;

如下面來自Martin文中的圖示:

服務熔斷服務熔斷的了解

服務熔斷和服務降級所考慮關注的目标也不一樣,熔斷是每個微服務都需要的,是一個架構級的處理,而服務降級一般是關注業務,對業務進行考慮,抓住業務的層級,進而決定在哪一層上進行處理:比如在IO層,業務邏輯層,還是在外圍進行處理。

Martin設計一個狀态機,代表服務的不同狀态,

狀态機來實作,内部模拟以下幾種狀态。

  1. 閉合(closed)狀态:對應用程式的請求能夠直接引起方法的調用。代理類維護了最近調用失敗的次數,如果某次調用失敗,則使失敗次數加1。如果最近失敗次數超過了在給定時間内允許失敗的門檻值,則代理類切換到斷開(Open)狀态。此時代理開啟了一個逾時時鐘,當該時鐘超過了該時間,則切換到半斷開。
  2. 半開(Half-Open)狀态。該逾時時間的設定是給了系統一次機會來修正導緻調用失敗的錯誤。
  3. 斷開(Open)狀态: 在該狀态下,對應用程式的請求會立即傳回錯誤響應。

如圖:

服務熔斷服務熔斷的了解

說明: 半斷開(Half-Open)狀态,允許對服務一定數量的請求可以去調用服務,能夠有效防止正在恢複中的服務被突然而來的大量請求再次拖垮。也有稱為限流模式、預防模式。如果這些請求對服務的調用成功,那麼可以認為之前導緻調用失敗的錯誤已經修正,此時熔斷器切換到閉合狀态(并且将錯誤計數器重置);如果這一定數量的請求有調用失敗的情況,則認為導緻之前調用失敗的問題仍然存在,熔斷器切回到斷開方式,然後開始重置計時器來給系統一定的時間來修正錯誤。

部落客張善友認為對服務熔斷的設計中要考慮包括這些設計:

異常處理:調用受熔斷器保護的服務的時候,我們必須要處理當服務不可用時的異常情況。這些異常處理通常需要視具體的業務情況而定。比如,如果應用程式隻是暫時的功能降級,可能需要切換到其它的可替換的服務上來執行相同的任務或者擷取相同的資料,或者給使用者報告錯誤然後提示他們稍後重試。
    異常的類型:請求失敗的原因可能有很多種。一些原因可能會比其它原因更嚴重。比如,請求會失敗可能是由于遠端的服務崩潰,這可能需要花費數分鐘來恢複;也可能是由于伺服器暫時負載過重導緻逾時。熔斷器應該能夠檢查錯誤的類型,進而根據具體的錯誤情況來調整政策。比如,可能需要很多次逾時異常才可以斷定需要切換到斷開狀态,而隻需要幾次錯誤提示就可以判斷服務不可用而快速切換到斷開狀态。
    日志:熔斷器應該能夠記錄所有失敗的請求,以及一些可能會嘗試成功的請求,使得的管理者能夠監控使用熔斷器保護的服務的執行情況。
    測試服務是否可用:在斷開狀态下,熔斷器可以采用定期的ping遠端的服務或者資源,來判斷是否服務是否恢複,而不是使用計時器來自動切換到半斷開狀态。這種ping操作可以模拟之前那些失敗的請求,或者可以使用通過調用遠端服務提供的檢查服務是否可用的方法來判斷。
    手動重置:在系統中對于失敗操作的恢複時間是很難确定的,提供一個手動重置功能能夠使得管理者可以手動的強制将熔斷器切換到閉合狀态。同樣的,如果受熔斷器保護的服務暫時不可用的話,管理者能夠強制的将熔斷器設定為斷開狀态。
    并發問題:相同的熔斷器有可能被大量并發請求同時通路。熔斷器的實作不應該阻塞并發的請求或者增加每次請求調用的負擔。
    資源的差異性:使用單個熔斷器時,一個資源如果​​有分布在多個地方就需要小心。比如,一個資料可能存儲在多個磁盤分區上(shard),某個分區可以正常通路,而另一個可能存在暫時性的問題。在這種情況下,不同的錯誤響應如果混為一談,那麼應用程式通路的這些存在問題的分區的失敗的可能性就會高,而那些被認為是正常的分區,就有可能被阻塞。
    加快熔斷器的熔斷操作:有時候,服務傳回的錯誤資訊足夠讓熔斷器立即執行熔斷操作并且保持一段時間。比如,如果從一個分布式資源傳回的響應提示負載超重,那麼應該等待幾分鐘後再重試。(HTTP協定定義了"HTTP 503 Service Unavailable"來表示請求的服務目前不可用,他可以包含其他資訊比如,逾時等)
    重複失敗請求:當熔斷器在斷開狀态的時候,熔斷器可以記錄每一次請求的細節,而不是僅僅傳回失敗資訊,這樣當遠端服務恢複的時候,可以将這些失敗的請求再重新請求一次。
           

注意:

服務熔斷恢複重試問題:如果服務是幂等性的,則恢複重試不會有問題;而如果服務是非幂等性的,則重試會導緻資料出現問題。

網際網路公司Netflix,其Hystrix作為Netflix開源架構中的最受喜愛元件之一,可以用來處理依賴隔離,實作熔斷機制。

在Hystrix中,最重要的class是HystrixCommand和HystrixObservableCommand。

下面是Netflix的wiki:

You can support graceful degradation in a Hystrix command by adding a fallback method that Hystrix will call to obtain a default value or values in case the main command fails. You will want to implement a fallback for most Hystrix commands that might conceivably fail, with a couple of exceptions:

  • a command that performs a write operation

    If your Hystrix command is designed to do a write operation rather than to return a value (such a command might normally return a void in the case of a HystrixCommand or an empty Observable in the case of a HystrixObservableCommand), there isn’t much point in implementing a fallback. If the write fails, you probably want the failure to propagate back to the caller.

  • batch systems/offline compute

    If your Hystrix command is filling up a cache, or generating a report, or doing any sort of offline computation, it’s usually more appropriate to propagate the error back to the caller who can then retry the command later, rather than to send the caller a silently-degraded response.

    Whether or not your command has a fallback, all of the usual Hystrix state and circuit-breaker state/metrics are updated to indicate the command failure.

In an ordinary HystrixCommand you implement a fallback by means of a getFallback() implementation. Hystrix will execute this fallback for all types of failure such as run() failure, timeout, thread pool or semaphore rejection, and circuit-breaker short-circuiting. The following example includes such a fallback:

public class CommandHelloFailure extends HystrixCommand<String> {

    private final String name;

    public CommandHelloFailure(String name) {
        super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
        this.name = name;
    }

    @Override
    protected String run() {
        throw new RuntimeException("this command always fails");
    }

    @Override
    protected String getFallback() {
        return "Hello Failure " + name + "!";
    }
}   
           

繼續閱讀