1. 程式人生 > >springcloud系列—Hystrix—第3章-3: Hystrix 服務降級(fallback)與異常處理,Hystrix依賴隔離(命令名稱-分組和執行緒池)、請求快取與清除快取、斷路器

springcloud系列—Hystrix—第3章-3: Hystrix 服務降級(fallback)與異常處理,Hystrix依賴隔離(命令名稱-分組和執行緒池)、請求快取與清除快取、斷路器

資料參考:《Spring Cloud 微服務實戰》

目錄

服務降級

在HystrixCommand中可以通過過載getFallback()方法來實現服務降級邏輯。

在 HystrixObservableCommand 實現得 Hystrix 命令中,我們可以通過過載 resumenWithFallback 方法來實現服務降級邏輯。

使用註解來定義服務降級邏輯

異常處理

異常傳播(就是不觸發fallback)

異常獲取

Hystrix依賴隔離

依賴隔離

如何使用

命令名稱、分組和執行緒池劃分

請求快取

清理失效快取功能

Hystrix斷路器


 

服務降級

fallbackMethod所描述的函式實際上是Hystrix命令執行失敗時使用得後備方法,用來實現服務降級處理邏輯。

  • 在HystrixCommand中可以通過過載getFallback()方法來實現服務降級邏輯。

Hystrix會在執行 run() 得過程中,通過過載 getFallback() 方法來實現服務降級邏輯,Hystrix會在執行 run() 得過程中出現錯誤、超時、執行緒池拒絕、斷路器熔斷等情況時,執行 getFallback() 方法內得邏輯,比如:

public class MyCommand extends HystrixCommand<String> {

    private RestTemplate restTemplate;

    protected MyCommand(Setter setter, RestTemplate restTemplate) {
        super(setter);
        this.restTemplate = restTemplate;
    }

    @Override
    protected String getFallback() {
        return "調用出異常啦,啟用熔斷器";
    }

    @Override
    protected String run() throws Exception {
        return restTemplate.getForEntity("http://PROVIDER-EUREKA/index",String.class).getBody();
    }
}
  • 在 HystrixObservableCommand 實現得 Hystrix 命令中,我們可以通過過載 resumenWithFallback 方法來實現服務降級邏輯。

該方法會返回一個 Onservable 物件,當命令執行失敗得時候, Hystrix 會將 Observable 中得結果通知給所有得訂閱者。

public class MyObservableCommand extends HystrixObservableCommand<String> {

    private RestTemplate restTemplate;

    protected MyObservableCommand(Setter setter, RestTemplate restTemplate) {
        super(setter);
        this.restTemplate = restTemplate;
    }

    @Override
    protected Observable<String> construct() {
        return Observable.create(new Observable.OnSubscribe<String>() {
            @Override
            public void call(Subscriber<? super String> subscriber) {
                try{
                    if (!subscriber.isUnsubscribed()){
                        String a = restTemplate.getForEntity("http://PROVIDER-EUREKA/index",String.class).getBody();
                        subscriber.onNext(a);
                        subscriber.onCompleted();
                    }
                }catch (Exception e){
                    subscriber.onError(e);
                }
            }
        });
    }

    @Override
    protected Observable<String> resumeWithFallback() {
        return super.resumeWithFallback();
    }
}
  • 使用註解來定義服務降級邏輯

我們需要將具體得 Hystrix 命令與 fallback 實現函式定義再同一個類中,並且 fallbackMethod 得值必須與實現得 fallback 方法得名字相同。由於必須定義在一個類中,所有對 fallback得訪問修飾符沒有要求。

@Service
public class HelloService {

    @Autowired
    RestTemplate restTemplate;

    @Autowired
    private LoadBalancerClient loadBalancerClient;

    @HystrixCommand(fallbackMethod = "helloFallback")
    public String helloService(){

        ServiceInstance serviceInstance = loadBalancerClient.choose("provider-eureka");
        StringBuilder sb = new StringBuilder();
        sb.append("host: ").append(serviceInstance.getHost()).append(", ");
        sb.append("port: ").append(serviceInstance.getPort()).append(", ");
        sb.append("uri: ").append(serviceInstance.getUri());
        System.out.println(sb.toString());

        return restTemplate.getForEntity("http://PROVIDER-EUREKA/hello",String.class).getBody();
    }

    public String helloFallback(){
        return "error";
    }
}

 

異常處理

異常傳播(就是不觸發fallback)

在HystrixCommand 實現得 run() 方法中丟擲異常時,除了 HystrixBadRequestException 之外,其他異常均會被 Hystyix 認為命令執行失敗並觸發服務降級邏輯。所有當需要在命令執行中丟擲不觸發服務降級得異常時使用它。

在使用註冊配置實現 Hystrix 命令時,它還支援忽略指定異常型別功能,只需要通過設定 @HystrixCommand 註解得 ignoreExceptions引數,比如:

@HystrixCommand(fallbackMethod = "helloFallback",ignoreExceptions = {NullPointerException.class})
    public String helloService(){

        ServiceInstance serviceInstance = loadBalancerClient.choose("provider-eureka");
        StringBuilder sb = new StringBuilder();
        sb.append("host: ").append(serviceInstance.getHost()).append(", ");
        sb.append("port: ").append(serviceInstance.getPort()).append(", ");
        sb.append("uri: ").append(serviceInstance.getUri());
        System.out.println(sb.toString());

        return restTemplate.getForEntity("http://PROVIDER-EUREKA/hello",String.class).getBody();
    }

    public String helloFallback(){
        return "error";
    }

如上面得程式碼,當丟擲得異常是NullPointException得時候,Hystrix會把它包裝到 HystrixBadRequest- Exception 中,這樣就不會觸發 fallback邏輯了。

異常獲取

當 Hystrix 因為異常進入到服務降級得邏輯後,我們有時候需要獲取到異常,對不同得異常進行鍼對性得處理,所以其實是可以獲取到異常得。

 

Hystrix依賴隔離

我們已經體驗瞭如何使用@HystrixCommand來為一個依賴資源定義服務降級邏輯。實現方式非常簡單,同時對於降級邏輯還能實現一些更加複雜的級聯降級等策略。之前對於使用Hystrix來實現服務容錯保護時,除了服務降級之外,我們還提到過執行緒隔離、斷路器等功能。那麼在本篇中我們就來具體說說執行緒隔離。

依賴隔離

“艙壁模式”對於熟悉Docker的讀者一定不陌生,Docker通過“艙壁模式”實現程序的隔離,使得容器與容器之間不會互相影響。而Hystrix則使用該模式實現執行緒池的隔離,它會為每一個Hystrix命令建立一個獨立的執行緒池,這樣就算某個在Hystrix命令包裝下的依賴服務出現延遲過高的情況,也只是對該依賴服務的呼叫產生影響,而不會拖慢其他的服務。

通過對依賴服務的執行緒池隔離實現,可以帶來如下優勢:

  • 應用自身得到完全的保護,不會受不可控的依賴服務影響。即便給依賴服務分配的執行緒池被填滿,也不會影響應用自身的額其餘部分。
  • 可以有效的降低接入新服務的風險。如果新服務接入後執行不穩定或存在問題,完全不會影響到應用其他的請求。
  • 當依賴的服務從失效恢復正常後,它的執行緒池會被清理並且能夠馬上恢復健康的服務,相比之下容器級別的清理恢復速度要慢得多。
  • 當依賴的服務出現配置錯誤的時候,執行緒池會快速的反應出此問題(通過失敗次數、延遲、超時、拒絕等指標的增加情況)。同時,我們可以在不影響應用功能的情況下通過實時的動態屬性重新整理(後續會通過Spring Cloud Config與Spring Cloud Bus的聯合使用來介紹)來處理它。
  • 當依賴的服務因實現機制調整等原因造成其效能出現很大變化的時候,此時執行緒池的監控指標資訊會反映出這樣的變化。同時,我們也可以通過實時動態重新整理自身應用對依賴服務的閾值進行調整以適應依賴方的改變。
  • 除了上面通過執行緒池隔離服務發揮的優點之外,每個專有執行緒池都提供了內建的併發實現,可以利用它為同步的依賴服務構建非同步的訪問。

總之,通過對依賴服務實現執行緒池隔離,讓我們的應用更加健壯,不會因為個別依賴服務出現問題而引起非相關服務的異常。同時,也使得我們的應用變得更加靈活,可以在不停止服務的情況下,配合動態配置重新整理實現效能配置上的調整。

如何使用

說了那麼多依賴隔離的好處,那麼我們如何使用Hystrix來實現依賴隔離呢?其實,我們在上一篇定義服務降級的時候,已經自動的實現了依賴隔離。

在上一篇的示例中,我們使用了@HystrixCommand來將某個函式包裝成了Hystrix命令,這裡除了定義服務降級之外,Hystrix框架就會自動的為這個函式實現呼叫的隔離。所以,依賴隔離、服務降級在使用時候都是一體化實現的,這樣利用Hystrix來實現服務容錯保護在程式設計模型上就非常方便的,並且考慮更為全面。除了依賴隔離、服務降級之外,還有一個重要元素:斷路器。我們將在下一篇做詳細的介紹,這三個重要利器構成了Hystrix實現服務容錯保護的強力組合拳。

 

命令名稱、分組和執行緒池劃分

以繼承方式實現的Hystrix命令使用類名作為預設的命令名稱,我們也可以在建構函式中通過Setter靜態類來設定,比如:

public HelloCommend() {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GroupName"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("CommandName")));
    }

從上面Setter的使用中可以看到,我們並沒有直接設定命令名稱,而是先呼叫了withGroupkey來設定命令組名,然後才通過呼叫andCommandkey來設定命令名。這是因為在Setter的定義中,只有withGroupkey靜態函式可以建立Setter的例項,聽以
Groupkey是每個setter必需的引數,而CommandKey則是一個可選引數。

    通過設定命令組, Hystrix會根據組來組織和統計命令的告警、儀表盤等資訊。那麼為什麼一定要設定命令組呢?因為除了根
據組能實現統計之外, Hystrix命令預設的執行緒劃分也是根據命令分組來實現的
預設情況下, Hystrix會讓相同組名的命令使用
同一個執行緒池,所以我們需要在建立Hystrix命令時為其指定命令組名來實現預設的執行緒池劃分。

    如果Hystrix的執行緒池分配僅僅依靠命令組來劃分,那麼它就顯得不夠靈活了,所以Hystrix還提供了HystrixThreadPoolKey來對
執行緒池進行設定,通過它我們可以實現更細粒度的執行緒池劃分,比如:
 

public HelloCommend() {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GroupName"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("CommandName"))
                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("ThreadPoolKey")));
    }

    如果在沒有特別指定HystrixThreadPoolkey的情況下,依然會使用命令組的方式來劃分執行緒池。通常情況下,儘量通過
HystrixThreadPoolKey的方式來指定執行緒池的劃分,而不是通過組名的預設方式實現劃分,因為多個不同的命令可能從業務
邏輯上來看屬於同一個組,但是往往從實現本身上需要跟其他命令進行隔離。
    上面已經介紹瞭如何為通過繼承實現的HystrixCommand設定命令名稱、分組以及執行緒池劃分,那麼當我們使用
@HystrixCommand註解的時候,又該如何設定呢?
只需設定@HystrixCommand註解的commandKey, groupkey以及
threadPoolKey屬性即可,它們分別表示了命令名稱、分組以及執行緒池劃分
,比如我們可以像下面這樣進行設定:

@HystrixCommand(fallbackMethod = "helloFeedback1",groupKey = "",threadPoolKey = "",commandKey = "")
    public String helloConsumer () {
        return restTemplate.getForEntity("http://EUREKA-CLIENT/hello",
                String.class).getBody();
    }

 

請求快取

高併發環境下如果能處理好快取就可以有效的減小伺服器的壓力,Java中有許多非常好用的快取工具,比如Redis、EHCache等,當然在Spring Cloud的Hystrix中也提供了請求快取的功能,我們可以通過一個註解或者一個方法來開啟快取,進而減輕高併發環境下系統的壓力。本文我們就來看看Hystrix中請求快取的使用。
 

在 Hystrix 請求快取的使用非常簡單,我們只需要在實現 HystrixCommand 和 HystrixObservableCommand 命令中過載 getCacheKey()方法,就能實現快取請求。

public class HelloCommend extends HystrixCommand<String>{

    private RestTemplate restTemplate;

    public HelloCommend() {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GroupName"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("CommandName"))
                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("ThreadPoolKey")));
    }
    public HelloCommend(Setter setter, RestTemplate restTemplate) {
        super(setter);
        this.restTemplate = restTemplate;
    }
    @Override
    protected String run() throws Exception {
        String forObject = restTemplate.getForObject("http://EUREKA-CLIENT/hello", String.class);
        return forObject;
    }

    @Override
    protected String getFallback() {
        return "error";
    }

    @Override
    protected String getCacheKey() {
        return super.getCacheKey();
    }
}

在上面得例子中,我們通過 getCacheKey 方法返回得請求快取 key 值,就能讓該請求命令具備快取功能。系統在執行時會根據getCacheKey方法的返回值來判斷這個請求是否和之前執行過的請求一樣,即被快取,如果被快取,則直接使用快取資料而不去請求服務提供者,那麼很明顯,getCacheKey方法將在run方法之前執行。

  • 減少重複得請求數,降低依賴服務得併發度。
  • 在同一使用者得請求中,相同依賴服務得返回資料始終保持一致。
  • 請求快取在 run() 和 construct() 執行之前生效,所以可以有效得減少不必要得現場開銷。

 

清理失效快取功能

清理快取,開啟請求快取之後,我們在讀的過程中沒有問題,但是我們如果是寫,那麼我們繼續讀之前的快取了 ,我們需要把之前的cache清掉 

說明 :

1.其中getInstance方法中的第一個引數的key名稱要與實際相同 

2.clear方法中的cacheKey要與getCacheKey方法生成的key方法相同 

3.注意我們用了commandKey是test,大家要注意之後new這個Command的時候要指定相同的commandKey,否則會清除不成功 

    /**
     * 清理快取
     * 開啟請求快取之後,我們在讀的過程中沒有問題,但是我們如果是寫,那麼我們繼續讀之前的快取了
     * 我們需要把之前的cache清掉
     * 說明 :   1.其中getInstance方法中的第一個引數的key名稱要與實際相同
     *          2.clear方法中的cacheKey要與getCacheKey方法生成的key方法相同
     *          3.注意我們用了commandKey是test,大家要注意之後new這個Command的時候要指定相同的commandKey,否則會清除不成功
     */
    public static void flushRequestCache(Long id){
        HystrixRequestCache.getInstance(
                HystrixCommandKey.Factory.asKey("test"), HystrixConcurrencyStrategyDefault.getInstance())
                .clear(String.valueOf(id));
    }

 

Hystrix斷路器

我們來說說斷路器的工作原理。當我們把服務提供者中加入了模擬的時間延遲之後,在服務消費端的服務降級邏輯因為hystrix命令呼叫依賴服務超時,觸發了降級邏輯,但是即使這樣,受限於Hystrix超時時間的問題,我們的呼叫依然很有可能產生堆積。

這個時候斷路器就會發揮作用,那麼斷路器是在什麼情況下開始起作用呢?這裡涉及到斷路器的三個重要引數:快照時間窗、請求總數下限、錯誤百分比下限。這個引數的作用分別是:

  • 快照時間窗:斷路器確定是否開啟需要統計一些請求和錯誤資料,而統計的時間範圍就是快照時間窗,預設為最近的10秒。
  • 請求總數下限:在快照時間窗內,必須滿足請求總數下限才有資格根據熔斷。預設為20,意味著在10秒內,如果該hystrix命令的呼叫此時不足20次,即時所有的請求都超時或其他原因失敗,斷路器都不會開啟。
  • 錯誤百分比下限:當請求總數在快照時間窗內超過了下限,比如發生了30次呼叫,如果在這30次呼叫中,有16次發生了超時異常,也就是超過50%的錯誤百分比,在預設設定50%下限情況下,這時候就會將斷路器開啟。

那麼當斷路器開啟之後會發生什麼呢?我們先來說說斷路器未開啟之前,對於之前那個示例的情況就是每個請求都會在當hystrix超時之後返回fallback,每個請求時間延遲就是近似hystrix的超時時間,如果設定為5秒,那麼每個請求就都要延遲5秒才會返回。當熔斷器在10秒內發現請求總數超過20,並且錯誤百分比超過50%,這個時候熔斷器開啟。開啟之後,再有請求呼叫的時候,將不會呼叫主邏輯,而是直接呼叫降級邏輯,這個時候就不會等待5秒之後才返回fallback。通過斷路器,實現了自動地發現錯誤並將降級邏輯切換為主邏輯,減少響應延遲的效果。

在斷路器開啟之後,處理邏輯並沒有結束,我們的降級邏輯已經被成了主邏輯,那麼原來的主邏輯要如何恢復呢?對於這一問題,hystrix也為我們實現了自動恢復功能。當斷路器開啟,對主邏輯進行熔斷之後,hystrix會啟動一個休眠時間窗,在這個時間窗內,降級邏輯是臨時的成為主邏輯,當休眠時間窗到期,斷路器將進入半開狀態,釋放一次請求到原來的主邏輯上,如果此次請求正常返回,那麼斷路器將繼續閉合,主邏輯恢復,如果這次請求依然有問題,斷路器繼續進入開啟狀態,休眠時間窗重新計時。

通過上面的一系列機制,hystrix的斷路器實現了對依賴資源故障的埠、對降級策略的自動切換以及對主邏輯的自動恢復機制。這使得我們的微服務在依賴外部服務或資源的時候得到了非常好的保護,同時對於一些具備降級邏輯的業務需求可以實現自動化的切換與恢復,相比於設定開關由監控和運維來進行切換的傳統實現方式顯得更為智慧和高效。