Spring-Cloud 學習筆記-(5)熔斷器Hystrix
1、前言
-
上個章節我們做了什么?
上個章節我們使用了
Ribbon
實現了服務之間調用的負載均衡,具體可以分為三個步驟- 引ribbon依賴
- 在啟動類中的RestTemplate 加注解@LoadBalanced
- 把serviceId直接寫在RestTemplate 請求的url中調用
並且我們針對ribbon底層實現原理,走了一遍源碼。
-
這個章節我們會做什么?
熔斷器Hystrix
2、Hystrix介紹
2.1、簡介
Hystrix,英文翻譯是豪豬,是一種保護機制,Netflix公司的一款組件。
主頁:https://github.com/Netflix/Hystrix/
Hystix是Netflix開源的一個延遲和容錯庫,用於隔離訪問遠程服務、第三方庫,防止出現級聯失敗。
2.2、雪崩問題
上一章我們服務的調用方(order-service)調用了服務的提供方(user-serivce)查詢用戶的方法,我們可以稱order-service依賴於user-service,一旦我們user-service不可用,也會導致了order-service也不可用,類似這種級聯的失敗,我們可以稱作雪崩。
2.2.1、雪崩效應產生原因
- 服務的級聯失敗:就是剛剛說的A服務依賴B服務,B服務失敗了,倒是A服務也掛了,如果還有服務依賴A服務,這樣它也會掛了,就這樣一直延伸下去導致整個項目的不可用。
- 服務連接數被耗盡:失敗的服務占用了連接數,倒是正常的服務依舊訪問不了。
描述的詳細一點可以這么理解(圖片來自於:https://github.com/Netflix/Hystrix/wiki)
2.2.2、Hystrix如何解決雪崩問題
-
服務的熔斷和降級:
熔斷:當用戶的請求調用一個服務,這個服務掛了,阻塞了,我們設置一個超時時常,如果超過這個時間,我們會快速的返回一個失敗的友好提示給客戶端。
降級:以前訪問一個功能,我們可以提供所有的服務,但是現在我們有個地方有點問題,我們只能給你提供核心服務,不重要的暫時就訪問不了了。
-
線程的隔離:
比如我們Tomcat線程有500個,一個用戶的請求來了調用5個服務,我們分一個線程給他,讓這個線程去調用服務,調用成功返回結果,也就是說以前所有的服務都可以用這500個線程,這樣500個線程用完了,這個項目就掛了,現在的做法是什么呢,我們有針對性的給這些服務分配線程,比如一個服務分配100個線程,這樣就算有一個服務掛了, 就算服務I不可用,那只會阻塞這個100個線程,其余的400個線程還是正常,依舊可以調用其他正常的服務,我們把這種把不同的服務請求,用不同的線程池去隔離,就算你資源耗盡,僅僅會消耗當前線程池的連接數叫做線程的隔離。
官網對線程的隔離圖解
當服務繁忙時,如果服務出現異常,不是粗暴的直接報錯,而是返回一個友好的提示,雖然拒絕了用戶的訪問,但是會返回一個結果。
這就好比去買魚,平常超市買魚會額外贈送殺魚的服務。等到逢年過節,超時繁忙時,可能就不提供殺魚服務了,這就是服務的降級。
系統特別繁忙時,一些次要服務暫時中斷,優先保證主要服務的暢通,一切資源優先讓給主要服務來使用,在雙十一、618時,京東天貓都會采用這樣的策略。
3、服務的降級和線程隔離
3.1、代碼:
在服務的調用方(order-service)
3.1.1、引依賴:
<!-- hystrix -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
3.1.2、加注解
@SpringBootApplication
@EnableCircuitBreaker//開啟服務的熔斷
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class);
}
/**
* 把RestTemplate注入到Spring容器中
*/
@Bean
@LoadBalanced //讓RestTemplate內置一個負載均衡器
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
其實我們作為Eureka的服務端需要加注解@EnableEurekaServer
,同樣我們作為eureka的客戶端也需要加一個注解
@EnableDiscoveryClient
,只是我們Eureka比較智能,如果你有spring-cloud-starter-netflix-eureka-client
這個依賴,eureka就會默認你是一個eureka客戶端,所以@EnableDiscoveryClient
可以不用加。所以一個正常的springcloud微服務,基本上都會有三個注解,@SpringBootApplication
、@EnableDiscoveryClient
、@EnableCircuitBreaker
,所以springcloud很人性化的把這三個注解合成一個注解SpringCloudApplication
,所以大家嫌麻煩可以直接加一個SpringCloudApplication
注解就可以了。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication//啟動類
@EnableDiscoveryClient//eureka客戶端
@EnableCircuitBreaker//熔斷
public @interface SpringCloudApplication {
}
3.1.2、修改代碼:
上面說過,如果服務失敗,我們快速返回一個失敗信息,所以現在我們要做的是寫一個快速失敗的處理。
在OrderController中
//OrderController類
@RequestMapping("{user_id}")
//開啟服務的線程合理和降級處理,並指定失敗后調用的方法↓
@HystrixCommand(fallbackMethod = "findUserByIdFallbace")
public BaseData findUserById(@PathVariable("user_id")int id){
Order order = orderService.findById(id);
return new BaseData(order);
}
/** findUserById失敗后調用的方法
* 方法參數和返回值要和上面的完全一樣
*/
public BaseData findUserByIdFallbace(int id){
return new BaseData("服務器擁擠,請稍后再試!",null);
}
3.1.4、模擬服務調用異常
UserServiceImpl中
//UserServiceImpl類
/**
* 根據id查詢用戶基本信息
* @param id 用戶id
* @return 用戶對象
*/
@Override
public User findById(int id) {
//模擬服務器延遲
try {
//休眠兩秒
TimeUnit.SECONDS.sleep(2L);
} catch (InterruptedException e) {
e.printStackTrace();
}
User user = userMap.get(id);
user.setPort(port);
return user;
}
3.1.5、測試
啟動服務,訪問 http://localhost:8781/api/v1/order/2
3.2、升級版
剛剛我們是針對某一個類寫了一個降級方法,但是如果Controller中有很多方法我們就要寫很多的降級方法。所以我們可以針對一個類所有方法降級
3.1、代碼
3.1.1、注解修改
@RestController
@RequestMapping("api/v1/order")
@DefaultProperties(defaultFallback = "defaultFallback") //為整個類開啟服務的熔斷
public class OrderController {
//....
@RequestMapping("{user_id}")
//開啟服務的線程合理和降級處理
@HystrixCommand
public BaseData findUserById(@PathVariable("user_id")int id){
Order order = orderService.findById(id);
return new BaseData(order);
}
3.1.2、降級方法修改:
/**
* 方法參數為空
*/
public BaseData defaultFallback(){
return new BaseData("服務器擁擠,請稍后再試!",null);
}
3.1.3、測試
3.3、配置修改
3.3.1、單一方法Hystrix配置
打開控制台(F12),這里我們看的出來,雖然user-service我們設置了睡眠時間是2秒,但是每次一秒就返回結果了,說明Hytrix默認的超時時長是1秒,但是由於業務邏輯需要,比如發送郵件、銀行轉賬、等我們超時時長都可以稍微設置長一點,一些簡單查詢可以超時時長可以設置稍微短一點。
3.3.1.1、注解修改
//OrderController類
@RequestMapping("{user_id}")
/**
*commandProperties,中我們可以配置一些屬性,可以配置多個。
*但是這些配置的nama 和value 是什么呢...我們看源碼,這些配置都在HystrixCommandProperties類中
*/
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "",value = "")
})
public BaseData findUserById(@PathVariable("user_id")int id){
Order order = orderService.findById(id);
return new BaseData(order);
}
//HystrixCommandProperties類
//執行 超時時長 單位毫秒 我們ctrl+f 搜索一下default_executionTimeoutInMilliseconds,看看這個配置的key是什么...
private static final Integer default_executionTimeoutInMilliseconds = 1000;
//找到配置 key:execution.isolation.thread.timeoutInMilliseconds
this.executionTimeoutInMilliseconds = getProperty(propertyPrefix, key, "execution.isolation.thread.timeoutInMilliseconds", builder.getExecutionIsolationThreadTimeoutInMilliseconds(), default_executionTimeoutInMilliseconds);
所以我們要修改某一個方法熔斷配置,可以在@HystrixProperty中配置對於的name和value,比如我們配置超時時長為3秒,我們就可以
//OrderController類
@RequestMapping("{user_id}")
//開啟服務的線程合理和降級處理,並指定失敗后調用的方法
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
})
public BaseData findUserById(@PathVariable("user_id")int id){
Order order = orderService.findById(id);
return new BaseData(order);
}
3.3.1.2、測試
返回成功
3.3.2、全局配置
修改配置文件application.yml文件
#hystrix超時時長配置
hystrix:
command:
default:
execution.isolation.thread.timeoutInMilliseconds: 3000
我們刪除之前的方法上的超時配置,重啟一下order-service測試一下
依舊沒有問題。
4、服務的熔斷
4.1、熔斷的原理
-
一個請求過來,如果是關閉狀態,那請求繼續。
-
如果請求失敗超過一定的
閾值
(默認最近的20次請求有50%以上的請求失敗)則熔斷器打開。 -
后續請求發現熔斷器是開啟狀態,將直接返回錯誤信息,不會等待。
-
從熔斷器打開時候開始計時,熔斷器會經過一個
休眠時間窗
(默認5秒),超過5秒后熔斷器會進入半開狀態。 -
半開狀態的熔斷器會放一定量的請求通過進行嘗試,如果依舊超時,熔斷器繼續進入關閉狀態,然后在經歷休眠,如此反復,直到半開狀態的熔斷器放過去的請求成功了,熔斷器會繼續進入關閉狀態。
這些默認值也是在
//HystrixCommandProperties類 //打開熔斷器的最小請求次數 private static final Integer default_circuitBreakerRequestVolumeThreshold = 20; //休眠時間窗 private static final Integer default_circuitBreakerSleepWindowInMilliseconds = 5000; //設置打開熔斷並啟動回退邏輯的錯誤比率 private static final Integer default_circuitBreakerErrorThresholdPercentage = 50;
4.2、測試
修改代碼便於測試
- 把user-service中的睡眠時間刪除
- 修改OrderController代碼
//OrderController類
@RequestMapping("{user_id}")
//開啟服務的線程合理和降級處理
@HystrixCommand(
commandProperties = {
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"),
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "5000"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60")
}
)
public BaseData findUserById(@PathVariable("user_id")int id){
//手動控制請求成功失敗 技術
if(id%2==0){
throw new RuntimeException("");
}
Order order = orderService.findById(id);
return new BaseData(order);
}
-
測試
訪問:http://localhost:8781/api/v1/order/1 成功
訪問:http://localhost:8781/api/v1/order/2 失敗
如果我們一直訪問http://localhost:8781/api/v1/order/2 一直失敗
根據上面我們的配置在最近的10次請求中,如果失敗超過60%,這個時候熔斷器就會開啟,就算我們訪問http://localhost:8781/api/v1/order/1 成功的請求也會立即返回失敗信息,這個時候會經歷休眠時間窗5秒,超過5秒熔斷器進入半開狀態,我們訪問http://localhost:8781/api/v1/order/1,成功。