天天看點

Spring Cloud:第四章:Hystrix斷路器

Hystrix “豪豬”,具有自我保護的能力。hystrix 通過如下機制來解決雪崩效應問題。

資源隔離:包括線程池隔離和信号量隔離,限制調用分布式服務的資源使用,某一個調用的服務出現問題不會影響其他服務調用。

(1)線程池隔離模式:使用一個線程池來存儲目前請求,線程池對請求作處理,設定任務傳回處理逾時時間,堆積的請求先入線程池隊列。這種方式要為每個依賴服務申請線程池,有一定的資源消耗,好處是可以應對突發流量(流量洪峰來臨時,處理不完可将資料存儲到線程池隊裡慢慢處理)

(2)信号量隔離模式:使用一個原子計數器(或信号量)記錄目前有多少個線程在運作,請求來先判斷計數器的數值,若超過設定的最大線程個數則丢棄該類型的新請求,若不超過則執行計數操作請求來計數器+1,請求傳回計數器-1。這種方式是嚴格的控制線程且立即傳回模式,無法應對突發流量(流量洪峰來臨時,處理的線程超過數量,其他的請求會直接傳回,不繼續去請求依賴的服務)

Spring Cloud:第四章:Hystrix斷路器

降級機制:逾時降級、資源不足時(線程或信号量)降級,降級後可以配合降級接口傳回托底資料。

Spring Cloud:第四章:Hystrix斷路器

熔斷:當失敗率達到閥值自動觸發降級(如因網絡故障/逾時造成的失敗率高),熔斷器觸發的快速失敗會進行快速恢複。

正常情況下,斷路器處于關閉狀态(Closed),

如果調用持續出錯或者逾時,電路被打開進入熔斷狀态(Open),後續一段時間内的所有調用都會被拒絕(Fail Fast),

一段時間以後,保護器會嘗試進入半熔斷狀态(Half-Open),允許少量請求進來嘗試,

        如果調用仍然失敗,則回到熔斷狀态

        如果調用成功,則回到電路閉合狀态;

 緩存:提供了請求緩存、請求合并實作。

斷路器開啟或者關閉的條件:

1、  當滿足一定的閥值的時候(預設10秒内超過20個請求次數)

2、  當失敗率達到一定的時候(預設10秒内超過50%的請求失敗)

3、  到達以上閥值,斷路器将會開啟

4、  當開啟的時候,所有請求都不會進行轉發

5、  一段時間之後(預設是5秒),這個時候斷路器是半開狀态,會讓其中一個請求進行轉發。如果成功,斷路器會關閉,若失敗,繼續開啟。重複4 

消費者服務ribbon-consumer

案例:

1首先在pom.xml檔案中增加spring-cloud-starter-hystrix依賴

    <dependency>

        <groupId>org.springframework.cloud</groupId>

        <artifactId>spring-cloud-starter-hystrix</artifactId>

    </dependency>

2 在ribbon-consumer主類中使用@EnableCircuitBreaker注解開啟斷路由器功能,在這裡還有一個小技巧,可以使用@SpringCloudApplicationd代替@EnableCircuitBreaker、@EnableEurekaClient、@SpringBootApplication這三個注解。

    package com.didispace;

    import org.springframework.boot.SpringApplication;

    import org.springframework.boot.autoconfigure.SpringBootApplication;

    import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;

    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

    import org.springframework.cloud.client.loadbalancer.LoadBalanced;

    import org.springframework.context.annotation.Bean;

    import org.springframework.web.client.RestTemplate;

    @EnableCircuitBreaker

    @EnableDiscoveryClient

    @SpringBootApplication

    public class ConsumerApplication {

        @Bean

        @LoadBalanced

        RestTemplate restTemplate() {

            return new RestTemplate();

        }

        public static void main(String[] args) {

            SpringApplication.run(ConsumerApplication.class, args);

        }

    }

3 增加HelloService類

    package com.didispace.web;

    import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;

    import org.apache.log4j.Logger;

    import org.springframework.beans.factory.annotation.Autowired;

    import org.springframework.http.ResponseEntity;

    import org.springframework.stereotype.Service;

    import org.springframework.web.client.RestTemplate;

    import org.springframework.web.util.UriComponents;

    import org.springframework.web.util.UriComponentsBuilder;

    import java.net.URI;

    import java.util.HashMap;

    import java.util.Map;

    @Service

    public class HelloService {

        @Autowired

        RestTemplate restTemplate;

        //使用@HystrixCommand注解指定回調方法

        @HystrixCommand(fallbackMethod = "helloFallback")

        public String hello() {

            return restTemplate.getForEntity("http://HELLO-SERVICE/hello", String.class).getBody();

        }

        public String helloFallback() {

            return "error";

        }

    }

4 控制器類編寫

    package com.didispace.web;

    import org.springframework.beans.factory.annotation.Autowired;

    import org.springframework.web.bind.annotation.RequestMapping;

    import org.springframework.web.bind.annotation.RequestMethod;

    import org.springframework.web.bind.annotation.RestController;

    import org.springframework.web.client.RestTemplate;

    @RestController

    public class ConsumerController {

        @Autowired

        HelloService helloService;

        @RequestMapping(value = "/ribbon-consumer", method = RequestMethod.GET)

        public String helloConsumer() {

            return helloService.hello();

        }

    }

測試

1 啟動Eureka、啟動2個Hello-Service服務,啟動1個ribbon-consumer服務

2 通路http://localhost:9000/ribbon-consumer

并且Hello-Service兩個服務輪詢

3 關閉其中一個服務

4 通路http://localhost:9000/ribbon-consumer

浏覽器結果為如下,結果輪詢,說明回退生效。

error

或者

Hello World

Hystrix是如何工作的

第1步 建立HystrixCommand或HystrixObservableCommand對象

首先,建構一個HystrixCommand或HystrixObservableCommand對象,用來表示對依賴服務的操作請求,同時傳遞所有需要的參數。這兩個對象都采用了指令模式來實作對服務調用操作的封裝,但是這兩個對象分别針對不同的應用場景。

  1. HystrixCommand: 用在依賴的服務傳回單個操作結果的時候
  2. HystrixObservableCommand: 用在依賴的服務傳回多個操作結果的時候

指令模式,将來自用戶端的請求封裝成一個對象,進而讓你可以使用不同的請求對用戶端進行參數化。它可以用于實作行為請求者和行為實作者的解耦,以便使兩者可以适應變化

指令模式的示例代碼在command子產品下

通過指令模式的示例代碼可以分析出指令模式的幾個關鍵點:

  1. Receiver: 接收者,處理具體的業務邏輯
  2. Command: 抽象指令,定義了一個對象應具備的一系列指令操作,如execute()、undo()、redo()等。當指令操作被調用的時候就會觸發接收者做具體指令對應的業務邏輯。
  3. ConcreteCommand: 具體的指令實作,在這裡要綁定指令操作和接收者之間的關系,execute()指令的實作轉交給了Receiver的action()方法
  4. Invoker: 調用者,它擁有一個指令對象,可以在需要時通過指令對象完成具體的業務邏輯

指令模式中Invoker和Receiver的關系非常類似于請求-響應模式,是以它比較适用于實作記錄日志、撤銷操作、隊列請求等。

以下情況我們可以考慮使用指令模式:

  1. 使用指令模式作為回調在面向對象系統中的替代。
  2. 需要在不同的時間指定請求、将請求排隊。一個指令對象和原先的請求發出者可以有不同的生命周期。換言之,原先的請求發出者可能已經不在了,但是指令本身仍然是活動的。這時指令的接收者可以是在本地,也可以在網絡的另一個位址。指令對象可以在序列化之後傳送到另一台機器上。
  3. 系統需要支援指令的撤銷。指令對象可以把狀态存儲起來,等到用戶端需要撤銷指令所産生的效果時,可以調用undo()方法,把指令所産生的效果撤銷掉。指令對象還提供redo()方法,以供用戶端在需要時再重新實施指令效果。
  4. 如果要将系統中所有的資料更新到日志裡,以便在系統崩潰時,可以根據日志讀回所有的資料更新指令,重新調用execute()方法一條一條執行這些指令,進而恢複系統在崩潰前所做的資料更新。

第2步 指令執行

從圖中我們可以看到一共存在4種指令的執行方式,Hystrix在執行時會根據建立的Command對象以及具體的情況來選擇一個執行。

HystrixCommand

HystrixCommand實作了兩個執行方式:

  1. execute(): 同步執行,從依賴的服務傳回一個單一的結果對象,或是在錯誤時抛出異常
  2. queue(): 異步執行,直接傳回一個Future對象,其中包含了服務執行結束時要傳回的單一結果對象。
R value = command.execute();
Future<R> fValue = command.queue();      

HystrixObservableCommand

HystrixObservableCommand實作了另兩種執行方式:

  1. observer(): 傳回Observable對象,它代表了操作的多個結果,是一個HotObservable
  2. toObservable(): 同樣傳回Observable對象,也代表操作的多個結果,傳回的是一個ColdObservable
Observable<R> ohvalue = command.observe();
Observable<R> ocvalue = command.toObservable();      

Hot Observable和Cold Observable,分别對應了上面command.observe()和command.toObservable的傳回對象。

Hot Observable,不論事件源是否有訂閱者,都會在建立後對事件進行釋出,是以對Hot Observable的每一個訂閱者都有可能是從事件源的中途開始的,并可能隻是看到了整個操作的局部過程。

Cold Observable在沒有訂閱者的時候不會釋出事件,而是進行等待,直到有訂閱者後才會釋出事件,是以對于Cold Observable的訂閱者,它可以保證從一開始看到整個操作的全部過程。

HystrixCommand也使用RxJava實作:

  1. execute():該方法是通過queue()傳回的異步對象Future<R>的get()方法來實作同步執行的。該方法會等待任務執行結束,然後獲得R類型的結果傳回。
  2. queue():通過toObservable()獲得一個Cold Observable,并且通過通過toBlocking()将該Observable轉換成BlockingObservable,它可以把資料以阻塞的方式發出來,toFuture方法則是把BlockingObservable轉換為一個Future,該方法隻是建立一個Future傳回,并不會阻塞,這使得消費者可以自己決定如何處理異步操作。execute()則是直接使用了queue()傳回的Future中的阻塞方法get()來實作同步操作的。
  3. 通過這種方式轉換的Future要求Observable隻發射一個資料,是以這兩個實作都隻能傳回單一結果。

RxJava觀察者-訂閱者模式入門介紹

在Hystrix的底層實作中大量使用了RxJava。上面提到的Observable對象就是RxJava的核心内容之一,可以把Observable對象了解為事件源或是被觀察者,與其對應的是Subscriber對象,可以了解為訂閱者或是觀察者。

  1. Observable用來向訂閱者Subscriber對象釋出事件,Subscriber對象在接收到事件後對其進行處理,這裡所指的事件通常就是對依賴服務的調用。
  2. 一個Observable可以發出多個事件,直到結束或是發生異常。
  3. Observable對象每發出一個事件,就會調用對應觀察者Subscriber對象的onNext()方法。
  4. 每一個Observable的執行,最後一定會通過調用Subscriber.onCompleted()或是Subscriber.onError()來結束該事件的操作流。

第3步 結果是否被緩存

若目前指令的請求緩存功能是被啟用的,并且該指令緩存命中,那麼緩存的結果會立即以Observable對象的形式傳回。

第4步 斷路器是否打開

在指令結果沒有緩存命中的時候,Hystrix在執行指令前需要檢查斷路器是否為打開狀态:

  1. 如果斷路器是打開的,Hystrix不會執行指令,而是直接賺到fallback處理邏輯(對應下面第8步)
  2. 如果斷路器是關閉的,那麼Hystrix會跳到第5步,檢查是否有可用資源來執行指令。

第5步 線程池/請求隊列/信号量是否占滿

如果與指令相關的線程池和請求隊列或者信号量(不使用線程池的時候)已被占滿,那麼Hystrix不會執行指令,轉接到fallback處理邏輯(對應下面第8步)

Hystrix所判斷的線程池并非容器的線程池,而是每個依賴服務的專有線程池。Hystrix為了保證不會因為某個依賴服務的問題影響到其他依賴服務而采用了艙壁模式來隔離每個依賴的服務。

第6步 HystrixObservableCommand.construct()或HystrixCommand.run()

Hystrix會根據我們編寫的方法來決定采取什麼樣的方式去請求依賴服務:

  1. HystrixCommand.run(): 傳回一個單一的結果,或者抛出異常
  2. HystrixObservableCommand.construct(): 傳回一個Observable對象來發射多個結果,或通過onError發送錯誤通知

如果run()或construct()方法的執行時間超過了指令設定的逾時閥值,目前處理線程會抛出一個TimeoutException(如果該指令不在其自身的線程中執行,則會通過單獨的計時線程抛出)。在這種情況下,Hystrix會轉到fallback邏輯去處理(第8步)。同時,如果目前指令沒有被取消或中斷,那麼它最終會忽略run()或construct()方法的傳回。

如果指令沒有抛出異常并傳回了結果,那麼Hystrix在記錄一些日志并采集監控報告之後将該結果傳回。在使用run()時,傳回一個Observable,它會發射單個結果并産生onCompleted的結束通知,在使用construct()時,會直接傳回該方法産生的Observable對象。

第7步 計算斷路器的健康度

Hystrix會将成功、失敗、拒絕、逾時等資訊報告給斷路器,斷路器會維護一組計數器來統計這些資料。

斷路器會使用這些統計資料來決定是否要将斷路器打開,來對某個依賴服務的請求進行熔斷/短路,直到恢複期結束。若在恢複期結束後,根據統計資料判斷如果還是未達到健康名額,就再次熔斷/短路。

第8步 fallback處理

當指令執行失敗時,Hystrix會進入fallback嘗試回退處理,我們通常也稱之為服務降級。能夠引起服務降級處理的情況主要有以下幾種:

  1. 第4步,目前指令處于熔斷/短路狀态,斷路器是打開的時候。
  2. 第5步,目前指令的線程池、請求隊列或者信号量被占滿的時候。
  3. 第6步,HystrixObservableCommand.construct()或HystrixCommand.run()抛出異常的時候。

在服務降級邏輯中,我們需要實作一個通用的響應結果,并且該結果的處理邏輯應當是從緩存或是根據一些靜态邏輯來擷取,而不是依賴網絡請求擷取。如果一定要在降級邏輯中包含網絡請求,那麼該請求也必須被包裝在HystrixCommand或是HystrixObservableCommand中,進而形成級聯的降級政策,而最終的降級邏輯一定不是一個依賴網絡請求的處理,而是一個能夠穩定傳回結果的處理邏輯。

HystrixCommand和HystrixObservableCommand中實作降級邏輯時有以下不同:

  1. 當使用HystrixCommand的時候,通過實作HystrixCommand.getFallback()來實作服務降級邏輯。
  2. 當使用HystrixObservableCommand的時候,通過HystrixObservableCommand.resumeWithFallback()實作服務降級邏輯,該方法會傳回一個Observable對象來發射一個或多個降級結果。

當指令的降級邏輯傳回結果之後,Hystrix就将該結果傳回給調用者。當使用HystrixCommand.getFallback()時候,它會傳回一個Observable對象,該對象會發射getFallback()的處理結果。而使用HystrixObservableCommand.resumeWithFallback()實作的時候,它會将Observable對象直接傳回。

如果我們沒有為指令實作降級邏輯或在降級進行中抛出了異常,Hystrix依然會傳回一個Observable對象,但是他不會發射任何結果資料,而是通過onError方法通知指令立即中斷請求,并通過onError()方法将引起指令失敗的異常發送給調用者。在降級政策的實作中我們應盡可能避免失敗的情況。

如果在執行降級時發生失敗,Hystrix會根據不同的執行方法作出不同的處理:

  1. execute(): 抛出異常
  2. queue(): 正常傳回Future對象,但是調用get()來擷取結果時會抛出異常
  3. observe(): 正常傳回Observable對象,當訂閱它的時候,将立即通過訂閱者的onError方法來通知中止請求
  4. toObservable(): 正常傳回Observable對象,當訂閱它的時候,将通過調用訂閱者的onError方法來通知中止請求

第9步 傳回成功的響應

當Hystrix指令執行成功之後,它會将處理結果直接傳回或是以Observable的形式傳回。具體的傳回形式取決于不同的指令執行方式。

Spring Cloud:第四章:Hystrix斷路器
  1. toObservable(): 傳回原始的Observable,必須通過訂閱它才會真正觸發指令的執行流程
  2. observe(): 在toObservable()産生原始Observable之後立即訂閱它,讓指令能夠馬上開始異步執行,并傳回一個Observable對象,當調用它的subscribe時,将重新産生結果和通知給訂閱者。
  3. queue(): 将toObservable()産生的原始Observable通過toBlocking()方法轉換成BlockingObservable對象,并調用它的toFuture()方法傳回異步的Future對象
  4. execute(): 在queue()産生異步結果Future對象之後,通過調用get()方法阻塞并等待結果的傳回。