Spring Cloud 之 Netflix Hystrix 服務容錯
本文較大篇幅引用https://www.mrhelloworld.com/hystrix-circuit-breaker/,版權歸該文章作者所有
hystrix是什麼?
Hystrix是一個用於處理分散式系統的延遲和容錯的開源庫,在分散式系統裡,許多依賴不可避免的會呼叫失敗,比 如超時、異常等,
Hystrix能夠保證在一個依賴出問題的情況下,不會導致整體服務失敗,避免級聯故障,以提高分 布式系統的彈性。
“斷路器”本身是一種開關裝置,當某個服務單元發生故障之後,通過斷路器的故障監控(類似熔斷保險絲),向呼叫方返回一個符合預期的、可處理的備選響應(FallBack),而不是長時間的等待或者丟擲呼叫方無法處理的異常,
這樣就保證了服務呼叫方的執行緒不會被長時間、不必要地佔用,從而避免了故障在分散式系統中的蔓延,乃至雪崩。
大型專案中會出現的一些問題
典型的一個案例就是服務血崩效應 我們來看一張圖:
上圖是一條微服務呼叫鏈, 正常的情況我們就不必在討論了, 我們來說一下非正常情況, 假設現在 微服務H 響應 時間過長,或者微服務H直接down機瞭如圖:
來看下上圖, 我們聯想一下上圖, 如果發生這種情況, 也就是說所有發給微服務D的請求 都會被卡在微服務H 那, 就會導致執行緒一直累計在這裡, 那麼其他的微服務(比如A,B,C...) 就沒有可用執行緒了, 導致整個伺服器 崩潰,這就是服務血崩。
針對上面的問題,我們來看看有哪些解決方案 :
服務限流
超時監控
服務熔斷
服務降級
環境準備
hystrix-demo
聚合工程。SpringBoot 2.2.4.RELEASE
、Spring Cloud Hoxton.SR1
。
eureka-server
:註冊中心eureka(埠8761)eureka-server02
:註冊中心eureka(埠8762,兩個註冊中心相互註冊,搭建過程省略)product-service
:商品服務,提供了/product/{id}
/product/list
介面,/product/listByIds
介面order-service-rest
:訂單服務,基於Ribbon
通過RestTemplate
呼叫商品服務order-server-feign
:訂單服務,基於Feign
通過宣告式服務呼叫商品服務
商品服務 product-service
1.建立專案
2.新增依賴
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>product-service</artifactId> <version>1.0-SNAPSHOT</version> <!-- 繼承父依賴 --> <parent> <groupId>com.example</groupId> <artifactId>hystrix-demo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <!-- 專案依賴 --> <dependencies> <!-- netflix eureka client 依賴 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!-- spring boot web 依賴 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- lombok 依賴 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> <!-- spring boot test 依賴 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> </project>
3.配置檔案
server: port: 7070 # 埠 spring: application: name: product-service # 應用名稱 # 配置 Eureka Server 註冊中心 eureka: instance: prefer-ip-address: true # 是否使用 ip 地址註冊 instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port client: service-url: # 設定服務註冊中心地址 defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
4.實體類
package com.example.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; @Data @NoArgsConstructor @AllArgsConstructor public class Product implements Serializable { private Integer id; private String productName; private Integer productNum; private Double productPrice; }
5.編寫服務
package com.example.service; import com.example.pojo.Product; import java.util.List; /** * 商品服務 */ public interface ProductService { /** * 查詢商品列表 * * @return */ List<Product> selectProductList(); /** * 根據多個主鍵查詢商品 * * @param ids * @return */ List<Product> selectProductListByIds(List<Integer> ids); /** * 根據主鍵查詢商品 * * @param id * @return */ Product selectProductById(Integer id); }
實現類
package com.example.service.impl; import com.example.pojo.Product; import com.example.service.ProductService; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * 商品服務 */ @Service public class ProductServiceImpl implements ProductService { /** * 查詢商品列表 * * @return */ @Override public List<Product> selectProductList() { return Arrays.asList( new Product(1, "華為手機", 1, 5800D), new Product(2, "聯想筆記本", 1, 6888D), new Product(3, "小米平板", 5, 2020D) ); } /** * 根據多個主鍵查詢商品 * * @param ids * @return */ @Override public List<Product> selectProductListByIds(List<Integer> ids) { List<Product> products = new ArrayList<>(); ids.forEach(id -> products.add(new Product(id, "電視機" + id, 1, 5800D))); return products; } /** * 根據主鍵查詢商品 * * @param id * @return */ @Override public Product selectProductById(Integer id) { return new Product(id, "冰箱", 1, 2666D); } }
6.控制層
package com.example.controller; import com.example.pojo.Product; import com.example.service.ProductService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/product") public class ProductController { @Autowired private ProductService productService; /** * 查詢商品列表 * * @return */ @GetMapping("/list") public List<Product> selectProductList() { return productService.selectProductList(); } /** * 根據多個主鍵查詢商品 * * @param ids * @return */ @GetMapping("/listByIds") public List<Product> selectProductListByIds(@RequestParam("id") List<Integer> ids) { return productService.selectProductListByIds(ids); } /** * 根據主鍵查詢商品 * * @param id * @return */ @GetMapping("/{id}") public Product selectProductById(@PathVariable("id") Integer id) { return productService.selectProductById(id); } }
7.啟動類
package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication // 開啟 EurekaClient 註解,目前版本如果配置了 Eureka 註冊中心,預設會開啟該註解 //@EnableEurekaClient public class ProductServiceApplication { public static void main(String[] args) { SpringApplication.run(ProductServiceApplication.class, args); } }
訂單服務 order-service-rest
建立專案
新增依賴
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>order-service-rest</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 繼承父依賴 -->
<parent>
<groupId>com.example</groupId>
<artifactId>hystrix-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<!-- 專案依賴 -->
<dependencies>
<!-- netflix eureka client 依賴 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- spring boot web 依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- lombok 依賴 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- spring boot test 依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>
配置檔案
server: port: 8080 # 埠 spring: application: name: order-service-rest # 應用名稱 # 配置 Eureka Server 註冊中心 eureka: instance: prefer-ip-address: true # 是否使用 ip 地址註冊 instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port client: service-url: # 設定服務註冊中心地址 defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
實體類
package com.example.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; @Data @NoArgsConstructor @AllArgsConstructor public class Product implements Serializable { private Integer id; private String productName; private Integer productNum; private Double productPrice; }
order
package com.example.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.util.List; @Data @NoArgsConstructor @AllArgsConstructor public class Order implements Serializable { private Integer id; private String orderNo; private String orderAddress; private Double totalPrice; private List<Product> productList; }
消費服務
package com.example.service; import com.example.pojo.Product; import java.util.List; /** * 商品管理 */ public interface ProductService { /** * 查詢商品列表 * * @return */ List<Product> selectProductList(); /** * 根據多個主鍵查詢商品 * * @param ids * @return */ List<Product> selectProductListByIds(List<Integer> ids); /** * 根據主鍵查詢商品 * * @param id * @return */ Product selectProductById(Integer id); }
我們先使用 Ribbon 並通過 RestTemplate 來實現遠端服務的呼叫product服務。先講解 RestTemplate 方式的服務容錯處理。
ProductServiceImpl.java
package com.example.service.impl; import com.example.pojo.Product; import com.example.service.ProductService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpMethod; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.util.List; /** * 商品管理 */ @Service public class ProductServiceImpl implements ProductService { @Autowired private RestTemplate restTemplate; /** * 查詢商品列表 * * @return */ @Override public List<Product> selectProductList() { // ResponseEntity: 封裝了返回資料 return restTemplate.exchange( "http://product-service/product/list", HttpMethod.GET, null, new ParameterizedTypeReference<List<Product>>() { }).getBody(); } /** * 根據多個主鍵查詢商品 * * @param ids * @return */ @Override public List<Product> selectProductListByIds(List<Integer> ids) { StringBuffer sb = new StringBuffer(); ids.forEach(id -> sb.append("id=" + id + "&")); return restTemplate.getForObject("http://product-service/product/listByIds?" + sb.toString(), List.class); } /** * 根據主鍵查詢商品 * * @param id * @return */ @Override public Product selectProductById(Integer id) { return restTemplate.getForObject("http://product-service/product/" + id, Product.class); } }
OrderService.java
package com.example.service; import com.example.pojo.Order; public interface OrderService { /** * 根據主鍵查詢訂單 * * @param id * @return */ Order selectOrderById(Integer id); /** * 根據主鍵查詢訂單 * * @param id * @return */ Order queryOrderById(Integer id); /** * 根據主鍵查詢訂單 * * @param id * @return */ Order searchOrderById(Integer id); }
OrderServiceImpl.java
package com.example.service.impl; import com.example.pojo.Order; import com.example.service.OrderService; import com.example.service.ProductService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Arrays; @Service public class OrderServiceImpl implements OrderService { @Autowired private ProductService productService; /** * 根據主鍵查詢訂單 * * @param id * @return */ @Override public Order selectOrderById(Integer id) { return new Order(id, "order-001", "中國", 22788D, productService.selectProductList()); } /** * 根據主鍵查詢訂單 * * @param id * @return */ @Override public Order queryOrderById(Integer id) { return new Order(id, "order-002", "中國", 11600D, productService.selectProductListByIds(Arrays.asList(1, 2))); } /** * 根據主鍵查詢訂單 * * @param id * @return */ @Override public Order searchOrderById(Integer id) { return new Order(id, "order-003", "中國", 2666D, Arrays.asList(productService.selectProductById(5))); } }
控制層
package com.example.controller; import com.example.pojo.Order; import com.example.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/order") public class OrderController { @Autowired private OrderService orderService; /** * 根據主鍵查詢訂單-呼叫商品服務 /product/list * * @param id * @return */ @GetMapping("/{id}/product/list") public Order selectOrderById(@PathVariable("id") Integer id) { return orderService.selectOrderById(id); } /** * 根據主鍵查詢訂單-呼叫商品服務 /product/listByIds * * @param id * @return */ @GetMapping("/{id}/product/listByIds") public Order queryOrderById(@PathVariable("id") Integer id) { return orderService.queryOrderById(id); } /** * 根據主鍵查詢訂單-呼叫商品服務 /product/{id} * * @param id * @return */ @GetMapping("/{id}/product") public Order searchOrderById(@PathVariable("id") Integer id) { return orderService.searchOrderById(id); } }
啟動類
package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @SpringBootApplication public class OrderServiceRestApplication { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(OrderServiceRestApplication.class, args); } }
服務降級
我們先來解釋一下降級,降級是當我們的某個微服務響應時間過長,或者不可用了,講白了也就是那個微服務調用不了了,我們不能把錯誤資訊返回出來,或者讓他一直卡在那裡,所以要在準備一個對應的策略(一個方法)當發生 這種問題的時候我們直接呼叫這個方法來快速返回這個請求,不讓他一直卡在那 。
講了這麼多,我們來看看具體怎麼操作: 我們剛剛說了某個微服務調用不了了要做降級,也就是說,要在呼叫方做降級(不然那個微服務都down掉了再做 降級也沒什麼意義了) 比如說我們 order呼叫product那麼就在order做降級
新增依賴
服務消費者 pom.xml 新增 hystrix 依賴。
<!-- spring-cloud netflix hystrix 依賴 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
業務層
服務消費者業務層程式碼新增服務降級規則。
然後在我們的需要做服務降級方法上面加入註解@HystrixCommand(fallbackMethod就是我們剛剛說的方法的名字)
import com.example.pojo.Product; import com.example.service.ProductService; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; /** * 商品管理 */ @Service public class ProductServiceImpl implements ProductService { @Autowired private RestTemplate restTemplate; /** * 根據主鍵查詢商品 * * @param id * @return */ // 宣告需要服務容錯的方法 // 服務降級 @HystrixCommand(fallbackMethod = "selectProductByIdFallback") @Override public Product selectProductById(Integer id) { return restTemplate.getForObject("http://product-service/product/" + id, Product.class); } // 託底資料 private Product selectProductByIdFallback(Integer id) { return new Product(id, "託底資料", 1, 2666D); } }
啟動類
服務消費者啟動類開啟熔斷器註解。
package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; // 開啟熔斷器註解 2 選 1,@EnableHystrix 封裝了 @EnableCircuitBreaker // @EnableHystrix @EnableCircuitBreaker @SpringBootApplication public class OrderServiceRestApplication { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(OrderServiceRestApplication.class, args); } }
@EnableHystrix 或者@EnableCircuitBreaker(他們之間是一個繼承關係,2個註解所描述的內容是 完全一樣的)
測試
訪問:http://localhost:9090/order/3/product 結果如下:
關閉服務提供者,再次訪問:http://localhost:9090/order/3/product 結果如下:
通過結果可以看到,服務降級已經啟用。當 Provider 不可用時返回託底資料,直到服務可用快速恢復。
我們在服務提供者的該方法上新增2秒延時,
@GetMapping("/{id}") public Product selectProductById(@PathVariable("id") Integer id) throws InterruptedException { Thread.sleep(2000); return productService.selectProductById(id); }
再重啟服務提供者product-service測試一次
可能有些同學有疑問, 我這裡什麼都沒幹, 就讓他休眠了一下 , 怎麼就知道我這裡超時了呢? 因為hystrix他有預設的超時監聽,當你這個請求預設超過了1秒鐘就會超時
當然,這個可以配置的,至於怎麼配 置,待會兒我會把一些配置統一列出來
講了這麼多, 這個降級到底有什麼用呢?
第一, 他可以監聽你的請求有沒有超時,
第二,報錯了他這裡直接截斷了沒有讓請求一直卡在這裡
其實降級還有一個好處, 就是當你的系統馬上迎來大量的併發(雙十一秒殺這種 或者促銷活動) 這時候如果發現系 統馬上承載不了這麼大的併發時, 可以考慮先關閉一些不重要的微服務(在降級方法裡面返回一個比較友好的信 息),
把資源讓給主微服務,總結一下就是 整體資源快不夠了,忍痛將某些服務先關掉,待渡過難關,再開啟回來。
服務熔斷
服務熔斷一般是指軟體系統中,由於某些原因使得服務出現了過載現象,為防止造成整個系統故障,從而採用的一種保護措施,所以很多地方把熔斷亦稱為過載保護。
其實熔斷,就好像我們生活中的跳閘一樣, 比如說你的電路出故障了,為了防止出現 大型事故 這裡直接切斷了你的電源以免意外繼續發生, 把這個概念放在我們程式上也是如此, 當一個微服務呼叫多 次出現問題時(預設是10秒內20次當然 這個也能配置),hystrix就會採取熔斷機制,不再繼續呼叫你的方法(會 在預設5秒鐘內和電器短路一樣,5秒鐘後會試探性的先關閉熔斷機制,但是如果這時候再失敗一次{之前是20次} 那麼又會重新進行熔斷) 而是直接呼叫降級方法,這樣就一定程度上避免了服務雪崩的問題
業務層
服務消費者業務層程式碼新增服務熔斷規則。
package com.example.service.impl; import com.example.pojo.Product; import com.example.service.ProductService; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty; import com.netflix.hystrix.contrib.javanica.conf.HystrixPropertiesManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; /** * 商品管理 */ @Service public class ProductServiceImpl implements ProductService { @Autowired private RestTemplate restTemplate; /** * 根據主鍵查詢商品 * * @param id * @return */ // 宣告需要服務容錯的方法 // 服務熔斷 @HystrixCommand(commandProperties = { // 當請求符合熔斷條件觸發 fallbackMethod 預設 20 個 @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD, value = "10"), // 請求錯誤率大於 50% 就啟動熔斷器,然後 for 迴圈發起重試請求,當請求符合熔斷條件觸發 fallbackMethod @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE, value = "50"), // 熔斷多少秒後去重試請求,預設 5s @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS, value = "5000"), }, fallbackMethod = "selectProductByIdFallback") @Override public Product selectProductById(Integer id) { System.out.println("-----selectProductById-----" + LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME)); // 模擬查詢主鍵為 1 的商品資訊會導致異常 if (1 == id) throw new RuntimeException("查詢主鍵為 1 的商品資訊導致異常"); return restTemplate.getForObject("http://product-service/product/" + id, Product.class); } // 託底資料 private Product selectProductByIdFallback(Integer id) { return new Product(id, "託底資料", 1, 2666D); } }
啟動類
服務消費者啟動類開啟熔斷器註解。
這個東西光筆記不太好測試,只能你們自己去測試了
Feign整合hystrix:
環境準備
我們在父工程下再建立一個 Consumer 專案這次是基於 Feign 實現宣告式服務呼叫。
新增依賴
服務提供者新增openfeign
依賴,openfeign 預設集成了 hystrix 依賴。
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>order-service-feign</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 繼承父依賴 -->
<parent>
<groupId>com.example</groupId>
<artifactId>hystrix-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<!-- 專案依賴 -->
<dependencies>
<!-- netflix eureka client 依賴 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- spring cloud openfeign 依賴 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- spring boot web 依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- lombok 依賴 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- spring boot test 依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>
配置檔案
服務提供者需要開啟 Feign 對於 Hystrix 的支援。
server: port: 9091 # 埠 spring: application: name: order-service-feign # 應用名稱 # 配置 Eureka Server 註冊中心 eureka: instance: prefer-ip-address: true # 是否使用 ip 地址註冊 instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port client: service-url: # 設定服務註冊中心地址 defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/ # Feign 開啟 Hystrix 支援 feign: hystrix: enabled: true
實體類
package com.example.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; @Data @NoArgsConstructor @AllArgsConstructor public class Product implements Serializable { private Integer id; private String productName; private Integer productNum; private Double productPrice; }
Order.java
package com.example.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.util.List; @Data @NoArgsConstructor @AllArgsConstructor public class Order implements Serializable { private Integer id; private String orderNo; private String orderAddress; private Double totalPrice; private List<Product> productList; }
消費服務
package com.example.service; import com.example.fallback.ProductServiceFallback; import com.example.pojo.Product; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; import java.util.List; // 宣告需要呼叫的服務和服務熔斷處理類 @FeignClient(value = "product-service", fallback = ProductServiceFallback.class) public interface ProductService { /** * 查詢商品列表 * * @return */ @GetMapping("/product/list") List<Product> selectProductList(); /** * 根據多個主鍵查詢商品 * * @param ids * @return */ @GetMapping("/product/listByIds") List<Product> selectProductListByIds(@RequestParam("id") List<Integer> ids); /** * 根據主鍵查詢商品 * * @param id * @return */ @GetMapping("/product/{id}") Product selectProductById(@PathVariable("id") Integer id); }
OrderService.java
package com.example.service; import com.example.pojo.Order; public interface OrderService { /** * 根據主鍵查詢訂單 * * @param id * @return */ Order selectOrderById(Integer id); /** * 根據主鍵查詢訂單 * * @param id * @return */ Order queryOrderById(Integer id); /** * 根據主鍵查詢訂單 * * @param id * @return */ Order searchOrderById(Integer id); }
OrderServiceImpl.java
package com.example.service.impl; import com.example.pojo.Order; import com.example.service.OrderService; import com.example.service.ProductService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Arrays; @Service public class OrderServiceImpl implements OrderService { @Autowired private ProductService productService; /** * 根據主鍵查詢訂單 * * @param id * @return */ @Override public Order selectOrderById(Integer id) { return new Order(id, "order-001", "中國", 22788D, productService.selectProductList()); } /** * 根據主鍵查詢訂單 * * @param id * @return */ @Override public Order queryOrderById(Integer id) { return new Order(id, "order-002", "中國", 11600D, productService.selectProductListByIds(Arrays.asList(1, 2))); } /** * 根據主鍵查詢訂單 * * @param id * @return */ @Override public Order searchOrderById(Integer id) { return new Order(id, "order-003", "中國", 2666D, Arrays.asList(productService.selectProductById(5))); } }
熔斷降級
ProductServiceFallback.java
package com.example.fallback; import com.example.pojo.Product; import com.example.service.ProductService; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * 服務熔斷降級處理 */ @Component public class ProductServiceFallback implements ProductService { // 查詢商品列表介面的託底資料 @Override public List<Product> selectProductList() { return Arrays.asList( new Product(1, "託底資料-華為手機", 1, 5800D), new Product(2, "託底資料-聯想筆記本", 1, 6888D), new Product(3, "託底資料-小米平板", 5, 2020D) ); } // 根據多個主鍵查詢商品介面的託底資料 @Override public List<Product> selectProductListByIds(List<Integer> ids) { List<Product> products = new ArrayList<>(); ids.forEach(id -> products.add(new Product(id, "託底資料-電視機" + id, 1, 5800D))); return products; } // 根據主鍵查詢商品介面的託底資料 @Override public Product selectProductById(Integer id) { return new Product(id, "託底資料", 1, 2666D); } }
控制層
package com.example.controller; import com.example.pojo.Order; import com.example.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/order") public class OrderController { @Autowired private OrderService orderService; /** * 根據主鍵查詢訂單-呼叫商品服務 /product/list * * @param id * @return */ @GetMapping("/{id}/product/list") public Order selectOrderById(@PathVariable("id") Integer id) { return orderService.selectOrderById(id); } /** * 根據主鍵查詢訂單-呼叫商品服務 /product/listByIds * * @param id * @return */ @GetMapping("/{id}/product/listByIds") public Order queryOrderById(@PathVariable("id") Integer id) { return orderService.queryOrderById(id); } /** * 根據主鍵查詢訂單-呼叫商品服務 /product/{id} * * @param id * @return */ @GetMapping("/{id}/product") public Order searchOrderById(@PathVariable("id") Integer id) { return orderService.searchOrderById(id); } }
啟動類
服務消費者啟動類開啟@EnableFeignClients
註解。
package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication // 開啟 FeignClients 註解 @EnableFeignClients public class OrderServiceFeignApplication { public static void main(String[] args) { SpringApplication.run(OrderServiceFeignApplication.class, args); } }
測試
http://localhost:9091/order/5/product
正常顯示頁面
新增2秒睡眠時間,再次請求
我們已經可以通過 Feign 實現服務降級處理,
但是服務不可用時如果我們想要捕獲異常資訊該如何實現?接下來一起學習一下
捕獲服務異常
消費服務
通過fallbackFactory
屬性宣告服務熔斷降級處理類。
package com.example.service; import com.example.fallback.ProductServiceFallbackFactory; import com.example.pojo.Product; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; import java.util.List; // 宣告需要呼叫的服務和服務熔斷處理類 @FeignClient(value = "product-service", fallbackFactory = ProductServiceFallbackFactory.class) public interface ProductService { /** * 查詢商品列表 * * @return */ @GetMapping("/product/list") List<Product> selectProductList(); /** * 根據多個主鍵查詢商品 * * @param ids * @return */ @GetMapping("/product/listByIds") List<Product> selectProductListByIds(@RequestParam("id") List<Integer> ids); /** * 根據主鍵查詢商品 * * @param id * @return */ @GetMapping("/product/{id}") Product selectProductById(@PathVariable("id") Integer id); }
熔斷降級
實現FallbackFactory
介面。
package com.example.fallback; import com.example.pojo.Product; import com.example.service.ProductService; import feign.hystrix.FallbackFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * 服務熔斷降級處理可以捕獲異常 */ @Component public class ProductServiceFallbackFactory implements FallbackFactory<ProductService> { // 獲取日誌,在需要捕獲異常的方法中進行處理 Logger logger = LoggerFactory.getLogger(ProductServiceFallbackFactory.class); @Override public ProductService create(Throwable throwable) { return new ProductService() { // 查詢商品列表介面的託底資料 @Override public List<Product> selectProductList() { logger.error("product-service 服務的 selectProductList 方法出現異常,異常資訊如下:" + throwable); return Arrays.asList( new Product(1, "託底資料-華為手機", 1, 5800D), new Product(2, "託底資料-聯想筆記本", 1, 6888D), new Product(3, "託底資料-小米平板", 5, 2020D) ); } // 根據多個主鍵查詢商品介面的託底資料 @Override public List<Product> selectProductListByIds(List<Integer> ids) { logger.error("product-service 服務的 selectProductListByIds 方法出現異常,異常資訊如下:" + throwable); List<Product> products = new ArrayList<>(); ids.forEach(id -> products.add(new Product(id, "託底資料-電視機" + id, 1, 5800D))); return products; } // 根據主鍵查詢商品介面的託底資料 @Override public Product selectProductById(Integer id) { logger.error("product-service 服務的 selectProductById 方法出現異常,異常資訊如下:" + throwable); return new Product(id, "託底資料", 1, 2666D); } }; } }
測試
訪問:http://localhost:9091/order/1/product/list 結果如下:
控制檯列印結果:
hystrix相關配置:
hystrix.command.default和hystrix.threadpool.default中的default為預設CommandKey
Command Properties
Execution相關的屬性的配置:
hystrix.command.default.execution.isolation.strategy 隔離策略,預設是Thread, 可選Thread|Semaphore
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds 命令執行超時時間,預設1000ms
hystrix.command.default.execution.timeout.enabled 執行是否啟用超時,預設啟用true
hystrix.command.default.execution.isolation.thread.interruptOnTimeout 發生超時是是否中斷,預設true
hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests 最大併發請求數,預設10,該引數當使用ExecutionIsolationStrategy.SEMAPHORE策略時才有效。如果達到最大併發請求數,請求會被拒絕。理論上選擇semaphore size的原則和選擇thread size一致,但選用semaphore時每次執行的單元要比較小且執行速度快(ms級別),否則的話應該用thread。
semaphore應該佔整個容器(tomcat)的執行緒池的一小部分。
Fallback相關的屬性
這些引數可以應用於Hystrix的THREAD和SEMAPHORE策略
hystrix.command.default.fallback.isolation.semaphore.maxConcurrentRequests 如果併發數達到該設定值,請求會被拒絕和丟擲異常並且fallback不會被呼叫。預設10
hystrix.command.default.fallback.enabled 當執行失敗或者請求被拒絕,是否會嘗試呼叫hystrixCommand.getFallback() 。預設true
Circuit Breaker相關的屬性
hystrix.command.default.circuitBreaker.enabled 用來跟蹤circuit的健康性,如果未達標則讓request短路。預設true
hystrix.command.default.circuitBreaker.requestVolumeThreshold 一個rolling window內最小的請求數。如果設為20,那麼當一個rolling window的時間內(比如說1個rolling window是10秒)收到19個請求,即使19個請求都失敗,也不會觸發circuit break。預設20
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds 觸發短路的時間值,當該值設為5000時,則當觸發circuit break後的5000毫秒內都會拒絕request,也就是5000毫秒後才會關閉circuit。預設5000
hystrix.command.default.circuitBreaker.errorThresholdPercentage錯誤比率閥值,如果錯誤率>=該值,circuit會被開啟,並短路所有請求觸發fallback。預設50
hystrix.command.default.circuitBreaker.forceOpen 強制開啟熔斷器,如果開啟這個開關,那麼拒絕所有request,預設false
hystrix.command.default.circuitBreaker.forceClosed 強制關閉熔斷器 如果這個開關開啟,circuit將一直關閉且忽略circuitBreaker.errorThresholdPercentage
Metrics相關引數
hystrix.command.default.metrics.rollingStats.timeInMilliseconds 設定統計的時間視窗值的,毫秒值,circuit break 的開啟會根據1個rolling window的統計來計算。若rolling window被設為10000毫秒,則rolling window會被分成n個buckets,每個bucket包含success,failure,timeout,rejection的次數的統計資訊。預設10000
hystrix.command.default.metrics.rollingStats.numBuckets 設定一個rolling window被劃分的數量,若numBuckets=10,rolling window=10000,那麼一個bucket的時間即1秒。必須符合rolling window % numberBuckets == 0。預設10
hystrix.command.default.metrics.rollingPercentile.enabled 執行時是否enable指標的計算和跟蹤,預設true
hystrix.command.default.metrics.rollingPercentile.timeInMilliseconds 設定rolling percentile window的時間,預設60000
hystrix.command.default.metrics.rollingPercentile.numBuckets 設定rolling percentile window的numberBuckets。邏輯同上。預設6
hystrix.command.default.metrics.rollingPercentile.bucketSize 如果bucket size=100,window=10s,若這10s裡有500次執行,只有最後100次執行會被統計到bucket裡去。增加該值會增加記憶體開銷以及排序的開銷。預設100
hystrix.command.default.metrics.healthSnapshot.intervalInMilliseconds 記錄health 快照(用來統計成功和錯誤綠)的間隔,預設500ms
Request Context 相關引數
hystrix.command.default.requestCache.enabled 預設true,需要過載getCacheKey(),返回null時不快取
hystrix.command.default.requestLog.enabled 記錄日誌到HystrixRequestLog,預設true
Collapser Properties 相關引數
hystrix.collapser.default.maxRequestsInBatch 單次批處理的最大請求數,達到該數量觸發批處理,預設Integer.MAX_VALUE
hystrix.collapser.default.timerDelayInMilliseconds 觸發批處理的延遲,也可以為建立批處理的時間+該值,預設10
hystrix.collapser.default.requestCache.enabled 是否對HystrixCollapser.execute() and HystrixCollapser.queue()的cache,預設true
ThreadPool 相關引數
執行緒數預設值10適用於大部分情況(有時可以設定得更小),如果需要設定得更大,那有個基本得公式可以follow:
requests per second at peak when healthy × 99th percentile latency in seconds + some breathing room
每秒最大支撐的請求數 (99%平均響應時間 + 快取值)
比如:每秒能處理1000個請求,99%的請求響應時間是60ms,那麼公式是:
(0.060+0.012)
基本得原則時保持執行緒池儘可能小,他主要是為了釋放壓力,防止資源被阻塞。
當一切都是正常的時候,執行緒池一般僅會有1到2個執行緒啟用來提供服務
hystrix.threadpool.default.coreSize 併發執行的最大執行緒數,預設10
hystrix.threadpool.default.maxQueueSize BlockingQueue的最大佇列數,當設為-1,會使用SynchronousQueue,值為正時使用LinkedBlcokingQueue。該設定只會在初始化時有效,之後不能修改threadpool的queue size,除非reinitialising thread executor。預設-1。
hystrix.threadpool.default.queueSizeRejectionThreshold 即使maxQueueSize沒有達到,達到queueSizeRejectionThreshold該值後,請求也會被拒絕。因為maxQueueSize不能被動態修改,這個引數將允許我們動態設定該值。if maxQueueSize == -1,該欄位將不起作用
hystrix.threadpool.default.keepAliveTimeMinutes 如果corePoolSize和maxPoolSize設成一樣(預設實現)該設定無效。如果通過plugin(https://github.com/Netflix/Hystrix/wiki/Plugins)使用自定義實現,該設定才有用,預設1.
hystrix.threadpool.default.metrics.rollingStats.timeInMilliseconds 執行緒池統計指標的時間,預設10000
hystrix.threadpool.default.metrics.rollingStats.numBuckets 將rolling window劃分為n個buckets,預設10
至此 Hystrix 服務容錯知識點就講解結束了。