Spring Cloud:第四章:Hystrix斷路器
Hystrix “豪豬”,具有自我保護(hù)的能力。hystrix 通過如下機(jī)制來解決雪崩效應(yīng)問題。
資源隔離:包括線程池隔離和信號量隔離,限制調(diào)用分布式服務(wù)的資源使用,某一個(gè)調(diào)用的服務(wù)出現(xiàn)問題不會影響其他服務(wù)調(diào)用。
(1)線程池隔離模式:使用一個(gè)線程池來存儲當(dāng)前請求,線程池對請求作處理,設(shè)置任務(wù)返回處理超時(shí)時(shí)間,堆積的請求先入線程池隊(duì)列。這種方式要為每個(gè)依賴服務(wù)申請線程池,有一定的資源消耗,好處是可以應(yīng)對突發(fā)流量(流量洪峰來臨時(shí),處理不完可將數(shù)據(jù)存儲到線程池隊(duì)里慢慢處理)
(2)信號量隔離模式:使用一個(gè)原子計(jì)數(shù)器(或信號量)記錄當(dāng)前有多少個(gè)線程在運(yùn)行,請求來先判斷計(jì)數(shù)器的數(shù)值,若超過設(shè)置的最大線程個(gè)數(shù)則丟棄該類型的新請求,若不超過則執(zhí)行計(jì)數(shù)操作請求來計(jì)數(shù)器+1,請求返回計(jì)數(shù)器-1。這種方式是嚴(yán)格的控制線程且立即返回模式,無法應(yīng)對突發(fā)流量(流量洪峰來臨時(shí),處理的線程超過數(shù)量,其他的請求會直接返回,不繼續(xù)去請求依賴的服務(wù))
降級機(jī)制:超時(shí)降級、資源不足時(shí)(線程或信號量)降級,降級后可以配合降級接口返回托底數(shù)據(jù)。
熔斷:當(dāng)失敗率達(dá)到閥值自動(dòng)觸發(fā)降級(如因網(wǎng)絡(luò)故障/超時(shí)造成的失敗率高),熔斷器觸發(fā)的快速失敗會進(jìn)行快速恢復(fù)。
正常情況下,斷路器處于關(guān)閉狀態(tài)(Closed),
如果調(diào)用持續(xù)出錯(cuò)或者超時(shí),電路被打開進(jìn)入熔斷狀態(tài)(Open),后續(xù)一段時(shí)間內(nèi)的所有調(diào)用都會被拒絕(Fail Fast),
一段時(shí)間以后,保護(hù)器會嘗試進(jìn)入半熔斷狀態(tài)(Half-Open),允許少量請求進(jìn)來嘗試,
如果調(diào)用仍然失敗,則回到熔斷狀態(tài)
如果調(diào)用成功,則回到電路閉合狀態(tài);
緩存:提供了請求緩存、請求合并實(shí)現(xiàn)。
斷路器開啟或者關(guān)閉的條件:
1、 當(dāng)滿足一定的閥值的時(shí)候(默認(rèn)10秒內(nèi)超過20個(gè)請求次數(shù))
2、 當(dāng)失敗率達(dá)到一定的時(shí)候(默認(rèn)10秒內(nèi)超過50%的請求失?。?/p>
3、 到達(dá)以上閥值,斷路器將會開啟
4、 當(dāng)開啟的時(shí)候,所有請求都不會進(jìn)行轉(zhuǎn)發(fā)
5、 一段時(shí)間之后(默認(rèn)是5秒),這個(gè)時(shí)候斷路器是半開狀態(tài),會讓其中一個(gè)請求進(jìn)行轉(zhuǎn)發(fā)。如果成功,斷路器會關(guān)閉,若失敗,繼續(xù)開啟。重復(fù)4
消費(fèi)者服務(wù)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注解開啟斷路由器功能,在這里還有一個(gè)小技巧,可以使用@SpringCloudApplicationd代替@EnableCircuitBreaker、@EnableEurekaClient、@SpringBootApplication這三個(gè)注解。
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注解指定回調(diào)方法
@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 啟動(dòng)Eureka、啟動(dòng)2個(gè)Hello-Service服務(wù),啟動(dòng)1個(gè)ribbon-consumer服務(wù)
2 訪問http://localhost:9000/ribbon-consumer
并且Hello-Service兩個(gè)服務(wù)輪詢
3 關(guān)閉其中一個(gè)服務(wù)
4 訪問http://localhost:9000/ribbon-consumer
瀏覽器結(jié)果為如下,結(jié)果輪詢,說明回退生效。
error
或者
Hello World
Hystrix是如何工作的
第1步 創(chuàng)建HystrixCommand或HystrixObservableCommand對象
首先,構(gòu)建一個(gè)HystrixCommand或HystrixObservableCommand對象,用來表示對依賴服務(wù)的操作請求,同時(shí)傳遞所有需要的參數(shù)。這兩個(gè)對象都采用了命令模式來實(shí)現(xiàn)對服務(wù)調(diào)用操作的封裝,但是這兩個(gè)對象分別針對不同的應(yīng)用場景。
HystrixCommand: 用在依賴的服務(wù)返回單個(gè)操作結(jié)果的時(shí)候
HystrixObservableCommand: 用在依賴的服務(wù)返回多個(gè)操作結(jié)果的時(shí)候
命令模式,將來自客戶端的請求封裝成一個(gè)對象,從而讓你可以使用不同的請求對客戶端進(jìn)行參數(shù)化。它可以用于實(shí)現(xiàn)行為請求者和行為實(shí)現(xiàn)者的解耦,以便使兩者可以適應(yīng)變化
命令模式的示例代碼在command模塊下
通過命令模式的示例代碼可以分析出命令模式的幾個(gè)關(guān)鍵點(diǎn):
Receiver: 接收者,處理具體的業(yè)務(wù)邏輯
Command: 抽象命令,定義了一個(gè)對象應(yīng)具備的一系列命令操作,如execute()、undo()、redo()等。當(dāng)命令操作被調(diào)用的時(shí)候就會觸發(fā)接收者做具體命令對應(yīng)的業(yè)務(wù)邏輯。
ConcreteCommand: 具體的命令實(shí)現(xiàn),在這里要綁定命令操作和接收者之間的關(guān)系,execute()命令的實(shí)現(xiàn)轉(zhuǎn)交給了Receiver的action()方法
Invoker: 調(diào)用者,它擁有一個(gè)命令對象,可以在需要時(shí)通過命令對象完成具體的業(yè)務(wù)邏輯
命令模式中Invoker和Receiver的關(guān)系非常類似于請求-響應(yīng)模式,所以它比較適用于實(shí)現(xiàn)記錄日志、撤銷操作、隊(duì)列請求等。
以下情況我們可以考慮使用命令模式:
使用命令模式作為回調(diào)在面向?qū)ο笙到y(tǒng)中的替代。
需要在不同的時(shí)間指定請求、將請求排隊(duì)。一個(gè)命令對象和原先的請求發(fā)出者可以有不同的生命周期。換言之,原先的請求發(fā)出者可能已經(jīng)不在了,但是命令本身仍然是活動(dòng)的。這時(shí)命令的接收者可以是在本地,也可以在網(wǎng)絡(luò)的另一個(gè)地址。命令對象可以在序列化之后傳送到另一臺機(jī)器上。
系統(tǒng)需要支持命令的撤銷。命令對象可以把狀態(tài)存儲起來,等到客戶端需要撤銷命令所產(chǎn)生的效果時(shí),可以調(diào)用undo()方法,把命令所產(chǎn)生的效果撤銷掉。命令對象還提供redo()方法,以供客戶端在需要時(shí)再重新實(shí)施命令效果。
如果要將系統(tǒng)中所有的數(shù)據(jù)更新到日志里,以便在系統(tǒng)崩潰時(shí),可以根據(jù)日志讀回所有的數(shù)據(jù)更新命令,重新調(diào)用execute()方法一條一條執(zhí)行這些命令,從而恢復(fù)系統(tǒng)在崩潰前所做的數(shù)據(jù)更新。
第2步 命令執(zhí)行
從圖中我們可以看到一共存在4種命令的執(zhí)行方式,Hystrix在執(zhí)行時(shí)會根據(jù)創(chuàng)建的Command對象以及具體的情況來選擇一個(gè)執(zhí)行。
HystrixCommand
HystrixCommand實(shí)現(xiàn)了兩個(gè)執(zhí)行方式:
execute(): 同步執(zhí)行,從依賴的服務(wù)返回一個(gè)單一的結(jié)果對象,或是在錯(cuò)誤時(shí)拋出異常
queue(): 異步執(zhí)行,直接返回一個(gè)Future對象,其中包含了服務(wù)執(zhí)行結(jié)束時(shí)要返回的單一結(jié)果對象。
R value = command.execute();
Future<R> fValue = command.queue();
HystrixObservableCommand
HystrixObservableCommand實(shí)現(xiàn)了另兩種執(zhí)行方式:
observer(): 返回Observable對象,它代表了操作的多個(gè)結(jié)果,是一個(gè)HotObservable
toObservable(): 同樣返回Observable對象,也代表操作的多個(gè)結(jié)果,返回的是一個(gè)ColdObservable
Observable<R> ohvalue = command.observe();
Observable<R> ocvalue = command.toObservable();
Hot Observable和Cold Observable,分別對應(yīng)了上面command.observe()和command.toObservable的返回對象。
Hot Observable,不論事件源是否有訂閱者,都會在創(chuàng)建后對事件進(jìn)行發(fā)布,所以對Hot Observable的每一個(gè)訂閱者都有可能是從事件源的中途開始的,并可能只是看到了整個(gè)操作的局部過程。
Cold Observable在沒有訂閱者的時(shí)候不會發(fā)布事件,而是進(jìn)行等待,直到有訂閱者后才會發(fā)布事件,所以對于Cold Observable的訂閱者,它可以保證從一開始看到整個(gè)操作的全部過程。
HystrixCommand也使用RxJava實(shí)現(xiàn):
execute():該方法是通過queue()返回的異步對象Future<R>的get()方法來實(shí)現(xiàn)同步執(zhí)行的。該方法會等待任務(wù)執(zhí)行結(jié)束,然后獲得R類型的結(jié)果返回。
queue():通過toObservable()獲得一個(gè)Cold Observable,并且通過通過toBlocking()將該Observable轉(zhuǎn)換成BlockingObservable,它可以把數(shù)據(jù)以阻塞的方式發(fā)出來,toFuture方法則是把BlockingObservable轉(zhuǎn)換為一個(gè)Future,該方法只是創(chuàng)建一個(gè)Future返回,并不會阻塞,這使得消費(fèi)者可以自己決定如何處理異步操作。execute()則是直接使用了queue()返回的Future中的阻塞方法get()來實(shí)現(xiàn)同步操作的。
通過這種方式轉(zhuǎn)換的Future要求Observable只發(fā)射一個(gè)數(shù)據(jù),所以這兩個(gè)實(shí)現(xiàn)都只能返回單一結(jié)果。
RxJava觀察者-訂閱者模式入門介紹
在Hystrix的底層實(shí)現(xiàn)中大量使用了RxJava。上面提到的Observable對象就是RxJava的核心內(nèi)容之一,可以把Observable對象理解為事件源或是被觀察者,與其對應(yīng)的是Subscriber對象,可以理解為訂閱者或是觀察者。
Observable用來向訂閱者Subscriber對象發(fā)布事件,Subscriber對象在接收到事件后對其進(jìn)行處理,這里所指的事件通常就是對依賴服務(wù)的調(diào)用。
一個(gè)Observable可以發(fā)出多個(gè)事件,直到結(jié)束或是發(fā)生異常。
Observable對象每發(fā)出一個(gè)事件,就會調(diào)用對應(yīng)觀察者Subscriber對象的onNext()方法。
每一個(gè)Observable的執(zhí)行,最后一定會通過調(diào)用Subscriber.onCompleted()或是Subscriber.onError()來結(jié)束該事件的操作流。
第3步 結(jié)果是否被緩存
若當(dāng)前命令的請求緩存功能是被啟用的,并且該命令緩存命中,那么緩存的結(jié)果會立即以O(shè)bservable對象的形式返回。
第4步 斷路器是否打開
在命令結(jié)果沒有緩存命中的時(shí)候,Hystrix在執(zhí)行命令前需要檢查斷路器是否為打開狀態(tài):
如果斷路器是打開的,Hystrix不會執(zhí)行命令,而是直接賺到fallback處理邏輯(對應(yīng)下面第8步)
如果斷路器是關(guān)閉的,那么Hystrix會跳到第5步,檢查是否有可用資源來執(zhí)行命令。
第5步 線程池/請求隊(duì)列/信號量是否占滿
如果與命令相關(guān)的線程池和請求隊(duì)列或者信號量(不使用線程池的時(shí)候)已被占滿,那么Hystrix不會執(zhí)行命令,轉(zhuǎn)接到fallback處理邏輯(對應(yīng)下面第8步)
Hystrix所判斷的線程池并非容器的線程池,而是每個(gè)依賴服務(wù)的專有線程池。Hystrix為了保證不會因?yàn)槟硞€(gè)依賴服務(wù)的問題影響到其他依賴服務(wù)而采用了艙壁模式來隔離每個(gè)依賴的服務(wù)。
第6步 HystrixObservableCommand.construct()或HystrixCommand.run()
Hystrix會根據(jù)我們編寫的方法來決定采取什么樣的方式去請求依賴服務(wù):
HystrixCommand.run(): 返回一個(gè)單一的結(jié)果,或者拋出異常
HystrixObservableCommand.construct(): 返回一個(gè)Observable對象來發(fā)射多個(gè)結(jié)果,或通過onError發(fā)送錯(cuò)誤通知
如果run()或construct()方法的執(zhí)行時(shí)間超過了命令設(shè)置的超時(shí)閥值,當(dāng)前處理線程會拋出一個(gè)TimeoutException(如果該命令不在其自身的線程中執(zhí)行,則會通過單獨(dú)的計(jì)時(shí)線程拋出)。在這種情況下,Hystrix會轉(zhuǎn)到fallback邏輯去處理(第8步)。同時(shí),如果當(dāng)前命令沒有被取消或中斷,那么它最終會忽略run()或construct()方法的返回。
如果命令沒有拋出異常并返回了結(jié)果,那么Hystrix在記錄一些日志并采集監(jiān)控報(bào)告之后將該結(jié)果返回。在使用run()時(shí),返回一個(gè)Observable,它會發(fā)射單個(gè)結(jié)果并產(chǎn)生onCompleted的結(jié)束通知,在使用construct()時(shí),會直接返回該方法產(chǎn)生的Observable對象。
第7步 計(jì)算斷路器的健康度
Hystrix會將成功、失敗、拒絕、超時(shí)等信息報(bào)告給斷路器,斷路器會維護(hù)一組計(jì)數(shù)器來統(tǒng)計(jì)這些數(shù)據(jù)。
斷路器會使用這些統(tǒng)計(jì)數(shù)據(jù)來決定是否要將斷路器打開,來對某個(gè)依賴服務(wù)的請求進(jìn)行熔斷/短路,直到恢復(fù)期結(jié)束。若在恢復(fù)期結(jié)束后,根據(jù)統(tǒng)計(jì)數(shù)據(jù)判斷如果還是未達(dá)到健康指標(biāo),就再次熔斷/短路。
第8步 fallback處理
當(dāng)命令執(zhí)行失敗時(shí),Hystrix會進(jìn)入fallback嘗試回退處理,我們通常也稱之為服務(wù)降級。能夠引起服務(wù)降級處理的情況主要有以下幾種:
第4步,當(dāng)前命令處于熔斷/短路狀態(tài),斷路器是打開的時(shí)候。
第5步,當(dāng)前命令的線程池、請求隊(duì)列或者信號量被占滿的時(shí)候。
第6步,HystrixObservableCommand.construct()或HystrixCommand.run()拋出異常的時(shí)候。
在服務(wù)降級邏輯中,我們需要實(shí)現(xiàn)一個(gè)通用的響應(yīng)結(jié)果,并且該結(jié)果的處理邏輯應(yīng)當(dāng)是從緩存或是根據(jù)一些靜態(tài)邏輯來獲取,而不是依賴網(wǎng)絡(luò)請求獲取。如果一定要在降級邏輯中包含網(wǎng)絡(luò)請求,那么該請求也必須被包裝在HystrixCommand或是HystrixObservableCommand中,從而形成級聯(lián)的降級策略,而最終的降級邏輯一定不是一個(gè)依賴網(wǎng)絡(luò)請求的處理,而是一個(gè)能夠穩(wěn)定返回結(jié)果的處理邏輯。
HystrixCommand和HystrixObservableCommand中實(shí)現(xiàn)降級邏輯時(shí)有以下不同:
當(dāng)使用HystrixCommand的時(shí)候,通過實(shí)現(xiàn)HystrixCommand.getFallback()來實(shí)現(xiàn)服務(wù)降級邏輯。
當(dāng)使用HystrixObservableCommand的時(shí)候,通過HystrixObservableCommand.resumeWithFallback()實(shí)現(xiàn)服務(wù)降級邏輯,該方法會返回一個(gè)Observable對象來發(fā)射一個(gè)或多個(gè)降級結(jié)果。
當(dāng)命令的降級邏輯返回結(jié)果之后,Hystrix就將該結(jié)果返回給調(diào)用者。當(dāng)使用HystrixCommand.getFallback()時(shí)候,它會返回一個(gè)Observable對象,該對象會發(fā)射getFallback()的處理結(jié)果。而使用HystrixObservableCommand.resumeWithFallback()實(shí)現(xiàn)的時(shí)候,它會將Observable對象直接返回。
如果我們沒有為命令實(shí)現(xiàn)降級邏輯或在降級處理中拋出了異常,Hystrix依然會返回一個(gè)Observable對象,但是他不會發(fā)射任何結(jié)果數(shù)據(jù),而是通過onError方法通知命令立即中斷請求,并通過onError()方法將引起命令失敗的異常發(fā)送給調(diào)用者。在降級策略的實(shí)現(xiàn)中我們應(yīng)盡可能避免失敗的情況。
如果在執(zhí)行降級時(shí)發(fā)生失敗,Hystrix會根據(jù)不同的執(zhí)行方法作出不同的處理:
execute(): 拋出異常
queue(): 正常返回Future對象,但是調(diào)用get()來獲取結(jié)果時(shí)會拋出異常
observe(): 正常返回Observable對象,當(dāng)訂閱它的時(shí)候,將立即通過訂閱者的onError方法來通知中止請求
toObservable(): 正常返回Observable對象,當(dāng)訂閱它的時(shí)候,將通過調(diào)用訂閱者的onError方法來通知中止請求
第9步 返回成功的響應(yīng)
當(dāng)Hystrix命令執(zhí)行成功之后,它會將處理結(jié)果直接返回或是以O(shè)bservable的形式返回。具體的返回形式取決于不同的命令執(zhí)行方式。
toObservable(): 返回原始的Observable,必須通過訂閱它才會真正觸發(fā)命令的執(zhí)行流程
observe(): 在toObservable()產(chǎn)生原始Observable之后立即訂閱它,讓命令能夠馬上開始異步執(zhí)行,并返回一個(gè)Observable對象,當(dāng)調(diào)用它的subscribe時(shí),將重新產(chǎn)生結(jié)果和通知給訂閱者。
queue(): 將toObservable()產(chǎn)生的原始Observable通過toBlocking()方法轉(zhuǎn)換成BlockingObservable對象,并調(diào)用它的toFuture()方法返回異步的Future對象
execute(): 在queue()產(chǎn)生異步結(jié)果Future對象之后,通過調(diào)用get()方法阻塞并等待結(jié)果的返回。