目錄
代碼地址:代碼地址-GitHub
一:Hystrix簡介
在微服務場景中,通常會有很多層的服務調用。如果一個底層服務出現問題,故障會被向上傳播給用戶。我們需要一種機制,當底層服務不可用時,可以阻斷故障的傳播。這就是斷路器的作用。他是系統服務穩定性的最後一重保障。
在springcloud中斷路器組件就是Hystrix。Hystrix也是Netflix套件的一部分。它是一個延遲和容錯庫,用於隔離訪問遠程服務,第三方庫,防止出現級聯失效。他的功能是,當對某個服務的調用在一定的時間內(默認10s,由metrics.rollingStats.timeInMilliseconds配置),有超過一定次數(默認20次,由circuitBreaker.requestVolumeThreshold參數配置)並且失敗率超過一定值(默認50%,由circuitBreaker.errorThresholdPercentage配置),該服務的斷路器會打開。返回一個由開發者設定的fallback。
fallback可以是另一個由Hystrix保護的服務調用,也可以是固定的值。fallback也可以設計成鏈式調用,先執行某些邏輯,再返回fallback。
二:問題引入
在微服務中我們把各個業務模塊都放在不同的服務器上,有時候用戶的一個請求要從不同的模塊(服務)收集數據完成一個響應,當其中一個模塊掛掉了,請求無法得到響應,或者返回一個錯誤。如果這種問題在這個服務容器上積累,可能就會導致其它正常的請求無法獲得連接導致整個系統掛掉。
比如我上篇介紹的Eureka中的消費者服務模塊,它調用服務生產者模塊,如果服務生產者模塊掛掉,那麼前面的調用鏈也會掛掉。
這就好比,一個汽車生產線,生產不同的汽車,需要使用不同的零件,如果某個零件因爲某種原因無法使用,那麼就會造成整臺車無法裝配,陷入等待零件的狀態。直到零件到位,才能繼續組裝。此時如果有很多個車型都需要這個零件,那麼整個工廠都會陷入等待狀態,導致所有的生產線癱瘓。一個零件的影響範圍不斷擴大。
爲了解決類似雪崩的問題,就要用到SpringCloud的Hystrix。它解決此類問題的方法有兩個:線程隔離,服務熔斷
三:線程隔離,服務降級原理
線程隔離:
比如上面的所示圖,假如服務模塊有很多 服務A,服務B,服務C等等,之前訪問方式是用tomcat中的線程池來管理線程,如果服務C掛了,無法響應,那麼會造成後續的調用的線程也掛掉,線程無法回收,線程池耗盡,整個系統掛掉。
在Hystrix中卻是爲每個服務模塊都分配一個線程池,比如服務A一個線程池管理5個線程,服務B一個線程池管理5個線程,服務C一個線程池管理5個線程,所以當上面所說的問題出現時,只會耗盡服務C的線程池,而不影響整個系統的線程池。
但是隻有線程隔離還是不行的,線程池滿了後面再有線程進來怎麼辦?那就需要用到服務降級了
服務降級:
當服務C線程池滿了以後,再請求進來的線程就會進入等待,等待了假如5秒(可設置)沒有反應就會立即返回一個狀態,告訴前面調用方服務正在忙。並不一定線程池滿了纔會有這樣的情況,請求響應超時也會這樣處理。這樣不會造成整個系統的阻塞。
服務降級就是優先保證核心服務,非核心服務不可用或弱可用。
觸發服務降級:線程池滿,請求超時,出現錯誤等。
四:使用Hystrix
我們接着Eureka博文(Eureka實踐一,Eureka實踐二)實踐中的代碼來繼續完善。
1:引入依賴。我們是要及時響應用戶的請求不要阻塞,因此需要在消費者服務模塊使用。但是我在直接引入依賴使用的時候啓動報錯,因爲我是在之前Eureka的基礎上使用,就報了jar包衝突,搞了半天才解決掉,這裏簡單介紹一下怎麼解決的。
idea右側點擊Maven Projects看到如下:
我們引入hystrix導致了很多jar衝突,可以按下圖看項目的依賴樹找到衝突的jar
有衝突的jar包在依賴樹中會顯示紅色,我們可以選中有衝突的jar包右鍵選中Exlude,就會在pom中自動添加<exclusions>標籤排除有衝突的jar包。
排除之後我們的依賴變成了如下內容:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<exclusions>
<exclusion>
<artifactId>slf4j-api</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
<exclusion>
<artifactId>jackson-annotations</artifactId>
<groupId>com.fasterxml.jackson.core</groupId>
</exclusion>
<exclusion>
<artifactId>jackson-databind</artifactId>
<groupId>com.fasterxml.jackson.core</groupId>
</exclusion>
<exclusion>
<artifactId>jackson-databind</artifactId>
<groupId>com.fasterxml.jackson.core</groupId>
</exclusion>
<exclusion>
<artifactId>guava</artifactId>
<groupId>com.google.guava</groupId>
</exclusion>
<exclusion>
<artifactId>jackson-module-afterburner</artifactId>
<groupId>com.fasterxml.jackson.module</groupId>
</exclusion>
<exclusion>
<artifactId>jackson-core</artifactId>
<groupId>com.fasterxml.jackson.core</groupId>
</exclusion>
</exclusions>
</dependency>
2:使用註解,我們先使用@EnableCircuitBreaker。此時算上我們之前Eureka中使用的註解,現在啓動類上已經有三個註解了。
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public class ConsumerApplication {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
public static void main (String [] arg){
SpringApplication.run(ConsumerApplication.class);
}
}
而且這三個註解是經常一起使用,所以SpringCloud爲了簡化開發,定義了一個新的註解:@SpringCloudApplication
點進去看源碼,可以看到它就是代替這三個註解而來的。所以我們也可以直接使用它。
package org.springframework.cloud.client;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public @interface SpringCloudApplication {
}
3:增加Hystrix處理邏輯。
爲了開啓服務降級,我們在需要Hystrix處理的方法上添加註解:
@HystrixCommand(fallbackMethod = "getAllProductFallBack")
其中fallbackMethod 屬性值是我們定義的一個方法名,這個方法的返回值用於當調用的服務出現問題時(請求超時,線程池滿了等)返回給用戶的提示信息,但是要注意這個自定義的方法有個特殊之處,就是要和我們@HystrixCommand註解加的方法的參數列表以及返回值
完全一樣,一般都會用String,這樣可以直接返回提示信息。
@GetMapping("/getAllProduct")
@HystrixCommand(fallbackMethod = "getAllProductFallBack")
public String getAllProduct(){
//使用Ribbon請求第一種方式: 我們把地址換成服務id即可
String uri="http://EUREKA-SERVICE.PRODUCER/getProducts";
String vos = restTemplate.getForObject(uri, String.class);
return vos;
}
//此方法的參數和返回值必須和對應處理方法一致,一般返回值都使用String,
// 這樣提示信息比較友好
public String getAllProductFallBack(){
return "系統繁忙,請稍後重試";
}
4:測試
我們在生產者服務模塊設置一個2秒休眠,模仿超時。
我們請求消費者模塊地址看到如下:
看到我們的Hystrix配置成功,而且1s就返回了,這是因爲默認的Hystrix默認1s就認爲服務不通便調用了我們的配置。
上述我們的降級服務是寫在方法上的,當方法多的時候是不實用的,我還可以配置在類上@DefaultProperties(defaultFallback = "默認方法名"),這樣只要controller類中某個方法加上@HystrixCommand都可以觸發服務降級,但是注意,這樣寫,那個默認的方法就需要是無參的方法.
重啓服務訪問也是可以的。
(1): 但是默認情況下1s就認爲服務需要降級顯然不符合實際,所以我們需要修改默認配置。我們可以對controller中不同的方法採取不同的時長,在我們的方法上配置如下內容:
@GetMapping("/getAllProduct")
//@HystrixCommand(fallbackMethod = "getAllProductFallBack")
@HystrixCommand(commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "3000")
})
public String getAllProduct(Long id){
//使用Ribbon請求第一種方式: 我們把地址換成服務id即可
String uri="http://EUREKA-SERVICE.PRODUCER/getProducts";
String vos = restTemplate.getForObject(uri, String.class);
return vos;
}
把超時時長配置成了3s,這樣我們在生產者服務中的休眠兩秒就不影響結果了,重啓後訪問如下:
看到兩秒之後就返回正常結果了,並沒有降級服務。
@HystrixProperty屬性可以配置多個,對應的屬性可以參考HystrixCommandProperties類。比如我們超時的配置:
使用的地方太長,不好截圖,服務代碼如下,
this.executionTimeoutInMilliseconds = getProperty(propertyPrefix, key, "execution.isolation.thread.timeoutInMilliseconds", builder.getExecutionIsolationThreadTimeoutInMilliseconds(), default_executionTimeoutInMilliseconds);
(2):我們還可以在配置文件中配置全局的超時時間這個每個方法的時間都是一致的。
首先方法上直接使用:
@GetMapping("/getAllProduct")
//@HystrixCommand(fallbackMethod = "getAllProductFallBack")
@HystrixCommand
public String getAllProduct(Long id){
//使用Ribbon請求第一種方式: 我們把地址換成服務id即可
String uri="http://EUREKA-SERVICE.PRODUCER/getProducts";
String vos = restTemplate.getForObject(uri, String.class);
return vos;
}
在配置application.yml中配置如下:
#Hystrix的超時時間
hystrix:
command:
default:
execution:
isolation :
thread :
timeoutInMilliseconds: 3000
也是可以成功的。
五:熔斷器的引入
上面的內容,只用到了服務降級,但是這樣的話會有一個問題:
如果調用的服務一直存在問題,我們設置的服務降級時間爲2s,那麼一次請求總是在2s之後纔會進行服務降級處理,在高併發的時候就會很佔用資源,因爲正常情況下明明就幾毫秒的響應時間卻要2s後返回結果,這個時候就需要熔斷處理了。
熔斷器有三個狀態:
相關英文描述---閾值:Threshold 熔斷器(斷路器):circuitBreaker
關閉:熔斷器關閉,所有請求能正常訪問。
打開:熔斷器打開,所有請求都會被降級。默認情況下最近20次請求中如果有50%(默認)都失敗了,就達到閾值,熔斷器就會 打開。
半開:從熔斷器打開開始默認持續5s(休眠時間窗),就會進入半開狀態。 會放一部分請求通過,來測試請求是否正常,如果失 敗,就會又回到打開狀態,再打開5s然後進入半開狀態,如果測試正常, 熔斷器就會關閉,請求恢復正常處理。如此往復。
六:測試熔斷器
1)爲了能看到效果,我們要控制一些請求成功和失敗的情況,我們在consumerController中添加如下邏輯來控制請求是否成功。
生產提供服務者中的sleep(2)可去掉。
//@HystrixCommand(fallbackMethod = "getAllProductFallBack")
/*@HystrixCommand(commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "3000")
})*/
@GetMapping("/getAllProduct/{id}")
@HystrixCommand
public String getAllProduct(@PathVariable int id){
if (id%2==0){
throw new RuntimeException();
}
//使用Ribbon請求第一種方式: 我們把地址換成服務id即可
String uri="http://EUREKA-SERVICE.PRODUCER/getProducts";
String vos = restTemplate.getForObject(uri, String.class);
return vos;
}
2)我們需要更改默認的閾值和時間窗口。我們還要使用@HystrixProperty中的屬性。
我還要去看HystrixCommandProperties類,根據英文的大致意思可以找到閾值,休眠時間窗口,以及失敗比。的屬性
我們根據屬性名找到使用他們的地方就找到對應的key值了。
失敗比默認是50%就會開啓熔斷,這個配置我們可以不用改,不影響我們觀察結果,我們配置閾值和休眠時間窗口。
@HystrixCommand(commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "3000"),
@HystrixProperty(name="circuitBreaker.requestVolumeThreshold",value = "10"),
@HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds",value = "10000")
})
@GetMapping("/getAllProduct/{id}")
//@HystrixCommand
public String getAllProduct(@PathVariable int id){
if (id%2==0){
throw new RuntimeException();
}
//使用Ribbon請求第一種方式: 我們把地址換成服務id即可
String uri="http://EUREKA-SERVICE.PRODUCER/getProducts";
String vos = restTemplate.getForObject(uri, String.class);
return vos;
}
然後重啓訪問,我把測試的結果描述一下:
2:訪問如下地址:http://localhost:8012/getAllProduct/1 結果正常返回,而且返回結果很快。先調用此地址無論多少次都是成功的(理論上)。
(2) 訪問http://localhost:8012/getAllProduct/2地址,是返回服務降級的。一直按F5(強制刷新請求後臺),一直返回這個錯。
刷新大概十次之後。馬上去訪問:http://localhost:8012/getAllProduct/1,看到返回了也是服務降級的錯,而且響應時間很快,連續刷新幾次還是這樣,大概過了10s,再刷新就恢復正常訪問了。
可以看出熔斷器已經起了作用。
對於生產中的配置,這三個數值(閾值,休眠時長,失敗百分比)一般用默認就行了。但是那個請求超時時長一般需要配置,默認1s太短了。
上面的請求過程可以用下圖描述: