簡介
Hystrix設計目標
- 對來自依賴的延遲和故障進行防護和控制
- 阻止故障的連鎖反應
- 快速失敗并迅速恢復
- 回退并優雅降級
- 提供近實時的監控與告警
Hystrix遵循的設計原則
- 防止任何單獨的依賴耗盡資源(線程)
- 過載立即切斷并快速失敗,防止排隊
- 盡可能提供回退以保護用戶免受故障
- 使用隔離技術(例如隔板,泳道和斷路器模式)來限制任何一個依賴的影響
- 通過近實時的指標,監控和告警,確保故障被及時發現
- 通過動態修改配置屬性,確保故障及時恢復
- 防止整個依賴客戶端執行失敗,而不僅僅是網絡通信
Hystrix如何實現這些設計目標
- 使用命令模式將所有對外部服務(或依賴關系)的調用包裝在HystrixCommand或HystrixObservableCommand對象中,并將該對象放在單獨的線程中執行。
- 每個依賴都維護著一個線程池(或信號量),線程池被耗盡則拒絕請求(而不是讓請求排隊)。
- 記錄請求成功,失敗,超時和線程拒絕。
- 服務錯誤百分比超過了閾值,熔斷器開關自動打開,一段時間內停止對該服務的所有請求。
- 請求失敗,被拒絕,超時或熔斷時執行降級邏輯。
- 近實時地監控指標和配置的修改。
Hystrix能做什么
- 通過hystrix可以解決雪崩效應問題,它提供了資源隔離、降級機制、融斷、緩存等功能。
- 資源隔離:包括線程池隔離和信號量隔離,限制調用分布式服務的資源使用,某一個調用的服務出現問題不會影響其他服務調用。
- 降級機制:超時降級、資源不足時(線程或信號量)降級,降級后可以配合降級接口返回托底數據。
- 融斷:當失敗率達到閥值自動觸發降級(如因網絡故障/超時造成的失敗率高),熔斷器觸發的快速失敗會進行快速恢復。
- 緩存:返回結果緩存,后續請求可以直接走緩存。
- 請求合并:可以實現將一段時間內的請求(一般是對同一個接口的請求)合并,然后只對服務提供者發送一次請求。
隔離模式
Hystrix提供了兩種隔離模式:線程池隔離模式、信號量隔離模式。
線程池隔離模式:使用一個線程池來存儲當前請求,線程池對請求作處理,設置任務返回處理超時時間,堆積的請求先入線程池隊列。這種方式要為每個依賴服務申請線程池,有一定的資源消耗,好處是可以應對突發流量(流量洪峰來臨時,處理不完可將數據存儲到線程池隊里慢慢處理)
信號量隔離模式:使用一個原子計數器(或信號量)記錄當前有多少個線程在運行,請求來先判斷計數器的數值,若超過設置的最大線程個數則丟棄該類型的新請求,若不超過則執行計數操作請求來計數器+1,請求返回計數器-1。這種方式是嚴格的控制線程且立即返回模式,無法應對突發流量(流量洪峰來臨時,處理的線程超過數量,其他的請求會直接返回,不繼續去請求依賴的服務)
降級
服務降級的目的保證上游服務的穩定性,當整體資源快不夠了,將某些服務先關掉,待渡過難關,再開啟回來。
快速模式:如果調用服務失敗了,那么立即失敗并返回。
故障轉移:如果調用服務失敗了,那么調用備用服務,因為備用服務也可能失敗,所以也可能有再下一級的備用服務,如此形成一個級聯。例如:如果服務提供者不響應,則從緩存中取默認數據。
主次模式:舉個例子,開發中需要上線一個新功能,但為了防止新功能上線失敗可以回退到老的代碼,我們會做一個開關比做一個配置開關,可以動態切換到老代碼功能。那么Hystrix它是使用通過一個配置來在兩個command中進行切換。
熔斷器原理
請求合并器
微服務架構中通常需要依賴多個遠程的微服務,而遠程調用中最常見的問題就是通信消耗與連接數占用。在高并發的情況之下,因通信次數的增加,總的通信時間消耗將會變得越來越長。同時,因為依賴服務的線程池資源有限,將出現排隊等待與響應延遲的清況。
為了優化這兩個問題,Hystrix 提供了HystrixCollapser來實現請求的合并,以減少通信消耗和線程數的占用。
HystrixCollapser實現了在 HystrixCommand之前放置一個合并處理器,將處于一個很短的時間窗(默認10毫秒)內對同一依賴服務的多個請求進行整合,并以批量方式發起請求的功能(前提是服務提供方提供相應的批量接口)。HystrixCollapser的封裝多個請求合并發送的具體細節,開發者只需關注將業務上將單次請求合并成多次請求即可。
需要注意請求合并的額外開銷:用于請求合并的延遲時間窗會使得依賴服務的請求延遲增高。比如,某個請求不通過請求合并器訪問的平均耗時為5ms,請求合并的延遲時間窗為lOms (默認值), 那么當該請求設置了請求合并器之后,最壞情況下(在延遲時間 窗結束時才發起請求)該請求需要15ms才能完成。
合并請求存在額外開銷,所以需要根據依賴服務調用的實際情況決定是否使用此功能,主要考慮下面兩個方面:
請求命令本身的延遲:
對于單次請求而言,如果[單次請求平均時間/時間窗口]越小,對于單次請求的性能形象越小。如果依賴服務的請求命令本身是一個高延遲的命令,那么可以使用請求合并器,因為延遲時間窗的時間消耗顯得微不足道了。
并發量:
時間窗口內并發量越大,合并求情的性能提升越明顯。如果一個時間窗內只有少數幾個請求,那么就不適合使用請求合并器。相反,如果一個時間窗內具有很高的并發量,那么使用請求合并器可以有效減少網絡連接數量并極大提升系統吞吐量,此時延遲時間窗所增加的消耗就可以忽略不計了。
Hystrix工作流程
簡單例子
在文章中做些簡單修改:/2020/12/cloud_springcloud_eureka/
注冊中心:
服務提供方,分別啟動2個服務實例,
1 2 3
| java -jar demo.jar --server.port=9001
java -jar demo.jar --server.port=9002
|
服務消費方:
未加入斷路器之前,關閉其中一個服務,如9001的實例,發送GET請求到: http://127.0.0.1:9101/ribbon-consumer ,可以獲得以下信息:
1 2 3
| ... I/O error on GET request for "http://localhost:8888/ribbon-consumer/default": Connection refused: connect; ...
|
那么我們現在加入hystrix,在 demo-consumer工程中,pom.xml,加入內容:
1 2 3 4
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency>
|
pom.xml所有內容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <!-- springboot版本與springcloud須匹配 --> <version>2.1.4.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>demo-consumer</artifactId> <version>0.0.1-SNAPSHOT</version> <name>demo-consumer</name> <description>Demo project for Spring Boot</description>
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>1.8</java.version> <!-- springboot版本與springcloud須匹配 --> <spring.boot.version>2.1.4.RELEASE</spring.boot.version> <spring.cloud.version>Greenwich.SR1</spring.cloud.version> <spring.maven.version>2.4.1</spring.maven.version> </properties>
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency>
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
</dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring.cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring.maven.version}</version> </plugin> </plugins> </build>
</project>
|
創建一個HelloService.java,內容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| package com.example.democonsumer.service;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
@Service public class HelloService { @Autowired private RestTemplate restTemplate;
@HystrixCommand(fallbackMethod = "helloFallback") public String helloService() { String memberUrl = "http://HELLO-SERVICE/hello"; String result = restTemplate.getForObject(memberUrl, String.class); System.out.println("訪問結果" + result); return result; }
public String helloFallback() { String str = "error:helloFallback"; System.out.println(str); return str; } }
|
ConsumerController.java,修改內容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| package com.example.democonsumer.controller;
import com.example.democonsumer.service.HelloService; 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.slf4j.Logger; import org.slf4j.LoggerFactory;
@RestController public class ConsumerController {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired private HelloService helloService;
@RequestMapping(value = "/ribbon-consumer", method = RequestMethod.GET) public String helloConsumer() {
return helloService.helloService(); }
}
|
DemoConsumerApplication.java加入@EnableCircuitBreaker,內容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| package com.example.democonsumer;
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 DemoConsumerApplication {
@Bean // 讓RestTemplate在請求時擁有客戶端負載均衡的能力 @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); }
public static void main(String[] args) { SpringApplication.run(DemoConsumerApplication.class, args); } }
|
加入斷路器后,關閉其中一個服務,如9001的實例,發送GET請求到: http://127.0.0.1:9101/ribbon-consumer ,輸出信息: