服務雪崩:多個微服務之間調用的時候,假設A調用B、C,B、C服務又調用其他服務,這就是所謂的扇出。如果扇出的鏈路上某個微服務調用的時間過長或者不可用,對微服務A的調用就會占用越來越多的資源,從而引起系統崩潰,這就是所謂的"雪崩效應"。
對於高流量的應用來說,單一的后端依賴可能會導致所有服務器上的所有資源在幾秒內飽和。比失敗更糟糕的是,這些應用程序還可能導致服務之間的延遲增加,備份隊列,線程和其他資源緊張,導致整個系統發生其他的級聯故障。這些都表示需要對故障和延遲進行隔離和關聯,以便單個依賴關系的失敗,不能取消整個應用程序或系統。
1.Hystrix簡介
1.Hystrix是什么
在布式系統面臨的一個重要問題:應用程序可能有數十個依賴,每個依賴關系不可避免的會出現失敗,比如超時、異常等。Hystrix是一個用於分布式系統的延遲和容錯的開源庫,能夠在一個依賴出問題的情況下,不會導致整體服務失敗,避免級聯故障,以提高分布式系統的彈性。
"斷路器"本身是一種開關裝置,當某個服務單元發生故障之后,通過斷路器的故障監控(類似熔斷保險絲),向調用方返回一個預期的、可處理的備選響應(FallBack),而不是長時間的等待或者拋出調用方無法處理的異常,這就保證了服務調用方的線程不會被長時間、不必要的占用,從而避免了故障在分布式系統的蔓延乃至雪崩。
git地址:https://github.com/Netflix/Hystrix
2.Hystrix的作用以及重要概念
可以進行服務降級、服務熔斷、接近實時的監控。不過官網上Hystrix已經停止更新。與之對應的還有resilience4j、sentinel。
服務降級(fallback):就是某個服務出現故障了,不讓調用方一直等待,返回一個友好的提示(fallback)。下面情況會發出降級:程序運行異常、超時、服務熔斷觸發服務降級、線程池\信號量打滿也會導致服務降級。
服務熔斷(break):類比保險絲達到最大服務訪問后,直接拒絕訪問,拉閘限電,然后調用服務降級的方法並返回友好提示。通常過程:服務降級-》熔斷-》恢復調用鏈路。
服務限流(flowlimit): 限制接口的訪問次數,嚴禁接口無休止的調用,比如某個接口1秒鍾只能調用200次。自己實現的話可以在Controller層用AOP實現。
2.使用
1. 創建payment支付服務
1.新建項目
2.修改pom
<?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"> <parent> <artifactId>cloud</artifactId> <groupId>cn.qz.cloud</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-provider-hystrix-payment8081</artifactId> <dependencies> <!--hystrix--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <!--eureka-client--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!--引入自己抽取的工具包--> <dependency> <groupId>cn.qz.cloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> <!--web--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>
3.修改yml
server:
port: 8081
spring:
application:
name: cloud-provider-hystrix-payment
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
#defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
defaultZone: http://localhost:7001/eureka
4.啟動類
package cn.qz.cloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; /** * @Author: qlq * @Description * @Date: 22:08 2020/10/17 */ @SpringBootApplication @EnableEurekaClient public class PaymentHystrixMain8081 { public static void main(String[] args) { SpringApplication.run(PaymentHystrixMain8081.class, args); } }
5.業務類:
Service(這里直接用class,不用接口)
package cn.qz.cloud.service; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; /** * @Author: qlq * @Description * @Date: 22:15 2020/10/17 */ @Service public class PaymentService { /** * 正常 * * @param id * @return */ public String success(Integer id) { return "success,線程池: " + Thread.currentThread().getName() + " success,id: " + id; } public String timeout(Integer id) { try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } return "timeout,線程池: " + Thread.currentThread().getName() + " success,id: " + id; } }
Controller
package cn.qz.cloud.controller; import cn.qz.cloud.service.PaymentService; import cn.qz.cloud.utils.JSONResultUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; 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; import javax.annotation.Resource; /** * @Author: qlq * @Description * @Date: 22:22 2020/10/17 */ @RestController @Slf4j @RequestMapping("/hystrix/payment") public class PaymentController { @Resource private PaymentService paymentService; @Value("${server.port}") private String serverPort; @GetMapping("/success/{id}") public JSONResultUtil<String> success(@PathVariable("id") Integer id) { String result = paymentService.success(id); log.info("*****result: " + result); return JSONResultUtil.successWithData(result); } @GetMapping("/timeout/{id}") public JSONResultUtil timeout(@PathVariable("id") Integer id) { String result = paymentService.timeout(id); log.info("*****result: " + result); return JSONResultUtil.successWithData(result); } }
6.測試正常
2.創建訂單服務
1.創建模塊
2.修改pom
<?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"> <parent> <artifactId>cloud</artifactId> <groupId>cn.qz.cloud</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-consumer-feign-hystrix-order80</artifactId> <dependencies> <!--hystrix--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <!--openfeign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--eureka-client--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!--引入自己抽取的工具包--> <dependency> <groupId>cn.qz.cloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>
3.修改yml文件
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://localhost:7001/eureka/
feign:
hystrix:
enabled: true
4.啟動類:
package cn.qz.cloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.hystrix.EnableHystrix; import org.springframework.cloud.openfeign.EnableFeignClients; /** * @Author: qlq * @Description * @Date: 14:25 2020/10/18 */ @SpringBootApplication @EnableFeignClients @EnableHystrix public class OrderHystrixMain80 { public static void main(String[] args) { SpringApplication.run(OrderHystrixMain80.class, args); } }
5.業務類
(1)Service
package cn.qz.cloud.service; import cn.qz.cloud.utils.JSONResultUtil; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; /** * @Author: qlq * @Description * @Date: 14:31 2020/10/18 */ @Component @FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT") public interface PaymentHystrixService { @GetMapping("/hystrix/payment/success/{id}") public JSONResultUtil<String> success(@PathVariable("id") Integer id); @GetMapping("/hystrix/payment/timeout/{id}") public JSONResultUtil<String> timeout(@PathVariable("id") Integer id); }
(2)controller
package cn.qz.cloud.controller; import cn.qz.cloud.service.PaymentHystrixService; import cn.qz.cloud.utils.JSONResultUtil; import lombok.extern.slf4j.Slf4j; 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; /** * @Author: qlq * @Description * @Date: 14:49 2020/10/18 */ @RestController @Slf4j @RequestMapping("/consumer/hystrix/payment") public class OrderHystirxController { @Autowired private PaymentHystrixService paymentHystrixService; @GetMapping("/success/{id}") public JSONResultUtil<String> success(@PathVariable("id") Integer id) { return paymentHystrixService.success(id); } @GetMapping("/timeout/{id}") public JSONResultUtil timeout(@PathVariable("id") Integer id) { return paymentHystrixService.timeout(id); } }
6.測試:
3.壓力測試
(1)用jmeter測試2W個並發訪問8081服務的timeout方法。
1)新建線程組:
2)新建HttpRequest請求
3)新建listener->view results Tree
4)執行jmeter測試。相當於2W個請求同時去請求timeout接口,8081的tomcat會分配線程組處理這2W個請求。
可以從8081服務查看日志發現也是用線程池處理timeout請求。
5)訪問正常的success接口報錯。因為沒有可分配的線程來處理success請求。
4.解決上面的超時和報錯(服務降級)
服務降級可以在服務消費者端進行,也可以在服務提供者進行,一般是在消費者端進行。
主要從下面三個維度處理:
(1)服務提供者8081超時,調用者80不能一直卡死等待,需要有降級
(2)服務提供者8081down機了,調用者80需要有降級
(3)服務提供者8081服務OK,調用者80自己出故障或者有自我要求(自己的等待時間小於服務的處理時間),需要降級。
1.服務提供者8081進行服務降級,超時或者異常之后走自己指定的fallback方法
(1)主啟動類增加注解:
@EnableCircuitBreaker
(2)Service聲明HystrixCommand進行降級:
package cn.qz.cloud.service; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; /** * @Author: qlq * @Description * @Date: 22:15 2020/10/17 */ @Service public class PaymentService { /** * 正常 * * @param id * @return */ public String success(Integer id) { return "success,線程池: " + Thread.currentThread().getName() + " success,id: " + id; } @HystrixCommand(fallbackMethod = "timeOutHandler", commandProperties = { @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000") }) public String timeout(Integer id) { try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } return "timeout,線程池: " + Thread.currentThread().getName() + " success,id: " + id; } public String timeOutHandler(Integer id) { return "線程池: " + Thread.currentThread().getName() + " 8081系統繁忙或者運行報錯,請稍后再試,id: " + id; } }
上面表示超過3s后走 timeOutHandler 降級方法。程序中休眠5s模擬請求耗時五秒。
(3)測試如下:
$ curl -X GET http://localhost:8081/hystrix/payment/timeout/1 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 135 0 135 0 0 44 0 --:--:-- 0:00:03 --:--:-- 44{"success":true,"code":"200","msg":"","data":"線程池: HystrixTimer-4 8081系統繁忙或者運行報錯,請稍后再試,id: 1"}
可以看到是Hystrix相關的線程池在處理請求。
(4)修改timeout方法,模擬程序報錯:
public String timeout(Integer id) { int i = 10 / 0; // try { // TimeUnit.SECONDS.sleep(5); // } catch (InterruptedException e) { // e.printStackTrace(); // } return "timeout,線程池: " + Thread.currentThread().getName() + " success,id: " + id; }
發現程序也是走的 timeOutHandler 方法,可以滿足實際中的需求。
(5)還原方法,認為5秒鍾是正常請求,線程休眠3s模擬實際處理耗時3s
@HystrixCommand(fallbackMethod = "timeOutHandler", commandProperties = { @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000") }) public String timeout(Integer id) { try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } return "timeout,線程池: " + Thread.currentThread().getName() + " success,id: " + id; }
2.服務消費者端80進行服務降級
(1)修改OrderHystirxController
package cn.qz.cloud.controller; import cn.qz.cloud.service.PaymentHystrixService; import cn.qz.cloud.utils.JSONResultUtil; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty; import lombok.extern.slf4j.Slf4j; 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; /** * @Author: qlq * @Description * @Date: 14:49 2020/10/18 */ @RestController @Slf4j @RequestMapping("/consumer/hystrix/payment") public class OrderHystirxController { @Autowired private PaymentHystrixService paymentHystrixService; @GetMapping("/success/{id}") public JSONResultUtil<String> success(@PathVariable("id") Integer id) { return paymentHystrixService.success(id); } @GetMapping("/timeout/{id}") @HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", commandProperties = { @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500") }) public JSONResultUtil timeout(@PathVariable("id") Integer id) { return paymentHystrixService.timeout(id); } public JSONResultUtil paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) { return JSONResultUtil.successWithData("消費者80,paymentTimeOutFallbackMethod, 線程池: " + Thread.currentThread().getName() + " 8081系統繁忙或者運行報錯,請稍后再試,id: " + id); } }
(2)測試:(調用服務提供者的timeout接口走的是paymentTimeOutFallbackMethod方法)
$ curl http://localhost/consumer/hystrix/payment/timeout/1 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 199 0 199 0 0 187 0 --:--:-- 0:00:01 --:--:-- 193{"success":true,"code":"200","msg":"","data":"消費者80,paymentTimeOutFallbackMethod, 線程池: hystrix-OrderHystirxController-2 8081系統繁忙或者運行報錯,請稍后再試,id: 1"}
3.上面降級存在的問題:
(1)每個方法配置一個fallback降級方法,代碼膨脹
(2)降級方法和業務邏輯混在一起,代碼混亂
解決辦法:
(1)defaultFallback解決上面問題一,實現默認降級處理
controller方法增加全局默認的降級處理,如下:
package cn.qz.cloud.controller; import cn.qz.cloud.service.PaymentHystrixService; import cn.qz.cloud.utils.JSONResultUtil; import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import lombok.extern.slf4j.Slf4j; 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; /** * @Author: qlq * @Description * @Date: 14:49 2020/10/18 */ @RestController @Slf4j @RequestMapping("/consumer/hystrix/payment") @DefaultProperties(defaultFallback = "paymentGlobalFallbackMethod") public class OrderHystirxController { @Autowired private PaymentHystrixService paymentHystrixService; @GetMapping("/success/{id}") public JSONResultUtil<String> success(@PathVariable("id") Integer id) { return paymentHystrixService.success(id); } @GetMapping("/timeout/{id}") // @HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", commandProperties = { // @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500") // }) // 采用默認的全局fallback @HystrixCommand public JSONResultUtil timeout(@PathVariable("id") Integer id) { int i = 1 / 0; return paymentHystrixService.timeout(id); } public JSONResultUtil paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) { return JSONResultUtil.successWithData("消費者80,paymentTimeOutFallbackMethod, 線程池: " + Thread.currentThread().getName() + " 8081系統繁忙或者運行報錯,請稍后再試,id: " + id); } // 下面是全局fallback方法 public JSONResultUtil paymentGlobalFallbackMethod() { return JSONResultUtil.successWithData("消費者80全局服務降級,paymentGlobalFallbackMethod, 線程池: " + Thread.currentThread().getName()); } }
定義了defaultFallback的只需要在方法聲明HystrixCommand注解出錯或調用的服務超時即可調用paymentGlobalFallbackMethod全局降級方法。也可以對方法單獨設置,單獨設置的會優先取方法上設置的降級方法。如果沒有聲明 HystrixCommand注解,不會進行服務的降級,報錯和超時都會直接走error。
測試如下:
$ curl http://localhost/consumer/hystrix/payment/timeout/1 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 155 0 155 0 0 3297 0 --:--:-- --:--:-- --:--:-- 4843{"success":true,"code":"200","msg":"","data":"消費者80全局服務降級,paymentGlobalFallbackMethod, 線程池: hystrix-OrderHystirxController-2"}
(2)通配服務降級FeignFallback:解決上面問題2,相當於每個方法fallback和業務分離
修改PaymentHystrixService增加 fallback屬性
package cn.qz.cloud.service; import cn.qz.cloud.utils.JSONResultUtil; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; /** * @Author: qlq * @Description * @Date: 14:31 2020/10/18 */ @Component @FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT", fallback = PaymentFallbackService.class) public interface PaymentHystrixService { @GetMapping("/hystrix/payment/success/{id}") public JSONResultUtil<String> success(@PathVariable("id") Integer id); @GetMapping("/hystrix/payment/timeout/{id}") public JSONResultUtil<String> timeout(@PathVariable("id") Integer id); }
增加PaymentFallbackService類:相當於處理上面接口中對應方法的fallback
package cn.qz.cloud.service; import cn.qz.cloud.utils.JSONResultUtil; import org.springframework.stereotype.Component; /** * @Author: qlq * @Description * @Date: 20:57 2020/10/18 */ @Component public class PaymentFallbackService implements PaymentHystrixService { @Override public JSONResultUtil<String> success(Integer id) { return JSONResultUtil.successWithData("PaymentFallbackService fallback,success 方法, threadName: " + Thread.currentThread().getName() + "\tid: " + id); } @Override public JSONResultUtil<String> timeout(Integer id) { return JSONResultUtil.successWithData("PaymentFallbackService fallback,timeout 方法, threadName: " + Thread.currentThread().getName() + "\tid: " + id); } }
測試:
$ curl http://localhost/consumer/hystrix/payment/timeout/1 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 129 0 129 0 0 49 0 --:--:-- 0:00:02 --:--:-- 50{"success":true,"code":"200","msg":"","data":"PaymentFallbackService fallback,timeout 方法, threadName: HystrixTimer-1\tid: 1"}
5.服務熔斷
熔斷機制是應對雪崩效應的一種微服務鏈路保護機制,當扇出鏈路的某個服務出錯不可用或響應時間太長時,會進行服務的降級,進而熔斷該節點微服務的調用,快速返回錯誤的響應信息。當檢測到該節點微服務調用響應正常后,恢復調用鏈路。
在SpringCloud框架里,熔斷機制通過Hystrix實現。Hystrix會監控微服務調用的狀況,當失敗的調用達到一定的閾值,缺省是5s內20次調用失敗,就會啟動熔斷機制。熔斷機制的注解是@HystrixCommand。
參考:https://martinfowler.com/bliki/CircuitBreaker.html
斷路器的3種狀態:
關閉 - 當一切正常時,斷路器保持閉合狀態,所有調用都能訪問到服務。當故障數超過預定閾值時,斷路器跳閘,並進入打開狀態。
打開 - 斷路器在不執行該服務的情況下為調用返回錯誤。
半開 - 超時后,斷路器切換到半開狀態,以測試問題是否仍然存在。如果在這種半開狀態下單個調用失敗,則斷路器再次打開。如果成功,則斷路器重置回正常關閉狀態。補充一下鏈路回復的過程:斷路器開啟一段時間之后(默認5s),這個時候斷路器是半開狀態,會讓其中一個請求進行處理,如果成功則關閉斷路器,若失敗,繼續開啟斷路器。
支付服務設置服務熔斷:
(1)修改PaymentService增加熔斷設置
package cn.qz.cloud.service; import cn.hutool.core.util.IdUtil; import cn.qz.cloud.utils.JSONResultUtil; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; /** * @Author: qlq * @Description * @Date: 22:15 2020/10/17 */ @Service public class PaymentService { /** * 正常 * * @param id * @return */ public String success(Integer id) { return "success,線程池: " + Thread.currentThread().getName() + " success,id: " + id; } @HystrixCommand(fallbackMethod = "timeOutHandler", commandProperties = { @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000") }) public String timeout(Integer id) { try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } return "timeout,線程池: " + Thread.currentThread().getName() + " success,id: " + id; } public String timeOutHandler(Integer id) { return "線程池: " + Thread.currentThread().getName() + " 8081系統繁忙或者運行報錯,請稍后再試,id: " + id; } //=====服務熔斷 @HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback", commandProperties = { @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),// 是否開啟斷路器 @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),// 請求次數 @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), // 時間窗口期 @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"),// 失敗率達到多少后跳閘 }) public JSONResultUtil<String> circuit(Integer id) { if (id < 0) { throw new RuntimeException("******id 不能負數"); } String serialNumber = IdUtil.simpleUUID(); return JSONResultUtil.successWithData(Thread.currentThread().getName() + " 調用成功,id: " + id + " ,流水號: " + serialNumber); } public JSONResultUtil<String> paymentCircuitBreaker_fallback(Integer id) { return JSONResultUtil.successWithData("paymentCircuitBreaker_fallback 降級處理, " + Thread.currentThread().getName() + " 調用失敗, id 不能負數, id: " + id); } }
需要注意方法上面三個重要的參數。關於更詳細的配置,可以參考類:HystrixCommandProperties
快照時間窗:斷路器確定是否打開統計一些請求和錯誤數據,而統計的時間范圍就是快照時間窗,默認為最近的10秒。
請求次數:在快照時間窗內必須達到請求次數才有資格熔斷,默認為20.如果達不到總次數,即使全部失敗也不會開啟斷路器。
錯誤百分比閾值:默認是50,就是一半失敗的情況下會開啟斷路。
(2)controller增加方法:
package cn.qz.cloud.controller; import cn.qz.cloud.service.PaymentService; import cn.qz.cloud.utils.JSONResultUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; 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; import javax.annotation.Resource; /** * @Author: qlq * @Description * @Date: 22:22 2020/10/17 */ @RestController @Slf4j @RequestMapping("/hystrix/payment") public class PaymentController { @Resource private PaymentService paymentService; @Value("${server.port}") private String serverPort; @GetMapping("/success/{id}") public JSONResultUtil<String> success(@PathVariable("id") Integer id) { String result = paymentService.success(id); log.info("*****result: " + result); return JSONResultUtil.successWithData(result); } @GetMapping("/timeout/{id}") public JSONResultUtil timeout(@PathVariable("id") Integer id) { String result = paymentService.timeout(id); log.info("*****result: " + result); return JSONResultUtil.successWithData(result); } //====服務熔斷 @GetMapping("/circuit/{id}") public JSONResultUtil<String> circuit(@PathVariable("id") Integer id) { JSONResultUtil<String> result = paymentService.circuit(id); log.info("****result: " + result); return result; } }
(3)測試:
-先測試成功:
liqiang@root MINGW64 ~/Desktop $ curl http://localhost:8081/hystrix/payment/circuit/5 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 139 0 139 0 0 4483 0 --:--:-- --:--:-- --:--:-- 8687{"success":true,"code":"200","msg":"","data":"hystrix-PaymentService-10 調用成功,id: 5 ,流水號: 2f5966b1f6cd469ab8b05ef83067c156"} liqiang@root MINGW64 ~/Desktop $ curl http://localhost:8081/hystrix/payment/circuit/-5 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 158 0 158 0 0 3361 0 --:--:-- --:--:-- --:--:-- 9875{"success":true,"code":"200","msg":"","data":"paymentCircuitBreaker_fallback 降級處理, hystrix-PaymentService-10 調用失敗, id 不能負數, id: -5"} liqiang@root MINGW64 ~/Desktop $ curl http://localhost:8081/hystrix/payment/circuit/5 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 139 0 139 0 0 4483 0 --:--:-- --:--:-- --:--:-- 8687{"success":true,"code":"200","msg":"","data":"hystrix-PaymentService-10 調用成功,id: 5 ,流水號: a528efccc26444cdac3219328616b491"}
-多調用幾次http://localhost:8081/hystrix/payment/circuit/-5連接,使得10秒鍾達到10次請求,並且每次都是失敗。斷路器會自動開啟,過段時間又會關閉斷路器,如下:
可以看到即使調用成功的ID,也是走的降級的方法。待自動關閉斷路器之后又自動進行鏈路恢復。
liqiang@root MINGW64 ~/Desktop $ curl http://localhost:8081/hystrix/payment/circuit/5 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 152 0 152 0 0 4903 0 --:--:-- --:--:-- --:--:-- 10133{"success":true,"code":"200","msg":"","data":"paymentCircuitBreaker_fallback 降級處理, http-nio-8081-exec-3 調用失敗, id 不能負數, id: 5"} liqiang@root MINGW64 ~/Desktop $ curl http://localhost:8081/hystrix/payment/circuit/5 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 152 0 152 0 0 4903 0 --:--:-- --:--:-- --:--:-- 148k{"success":true,"code":"200","msg":"","data":"paymentCircuitBreaker_fallback 降級處理, http-nio-8081-exec-4 調用失敗, id 不能負數, id: 5"} liqiang@root MINGW64 ~/Desktop $ curl http://localhost:8081/hystrix/payment/circuit/5 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 139 0 139 0 0 2957 0 --:--:-- --:--:-- --:--:-- 8687{"success":true,"code":"200","msg":"","data":"hystrix-PaymentService-10 調用成功,id: 5 ,流水號: c6438748e5a44ca9a619161c84a8296b"}
6.服務限流
暫時不做研究。之后研究alibaba的sentinel。
7.hystrix工作流程
注意分為如下步驟:
1.Construct a HystrixCommand or HystrixObservableCommand Object
2.Execute the Command
3.Is the Response Cached?
4.Is the Circuit Open?
5.Is the Thread Pool/Queue/Semaphore Full?
6.HystrixObservableCommand.construct() or HystrixCommand.run()
7.Calculate Circuit Health
8.Get the Fallback
9.Return the Successful Response
補充:Hystrix的資源隔離策略
1.為什么需要資源隔離:
例如,我們容器(Tomcat)配置的線程個數為1000,服務A-服務B,其中服務A的並發量非常的大,需要500個線程來執行,此時,服務A又掛了,那么這500個線程很可能就夯死了,那么剩下的服務,總共可用的線程為500個,隨着並發量的增大,剩余服務掛掉的風險就會越來越大,最后導致整個系統的所有服務都不可用,直到系統宕機。這就是服務的雪崩效應。
Hystrix就是用來做資源隔離的,比如說,當客戶端向服務端發送請求時,給服務A分配了10個線程,只要超過了這個並發量就走降級服務,就算服務A掛了,最多也就導致服務A不可用,容器的10個線程不可用了,但是不會影響系統中的其他服務。
2.Hystrix的資源隔離策略有兩種,分別為:線程池和信號量。
(1)線程池隔離模式:使用一個線程池來存儲當前的請求,線程池對請求作處理,設置任務返回處理超時時間,堆積的請求堆積入線程池隊列。這種方式需要為每個依賴的服務申請線程池,有一定的資源消耗,好處是可以應對突發流量(流量洪峰來臨時,處理不完可將數據存儲到線程池隊里慢慢處理)。
(2)信號量隔離模式:使用一個原子計數器(或信號量)來記錄當前有多少個線程在運行,請求來先判斷計數器的數值,若超過設置的最大線程個數則丟棄改類型的新請求,若不超過則執行計數操作請求來計數器+1,請求返回計數器-1。這種方式是嚴格的控制線程且立即返回模式,無法應對突發流量(流量洪峰來臨時,處理的線程超過數量,其他的請求會直接返回,不繼續去請求依賴的服務)
3.可以切換hystrix的資源隔離方式,默認是線程池模式。可以對某個方法單獨切換,也可以切換全局的,切換全局的如下:
hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 1000 strategy: SEMAPHORE # 信號量隔離 # strategy: THREAD # 線程池 semaphore: maxConcurrentRequests: 100 # 最大信號量上限
(1) 默認是線程池模式,HystrixCommand 降級以及熔斷方法完全采用hystrix的線程池
(2) 設置信號量模式: 會使用tomcat的線程池,可以通過信號量的多少控制並發量。
參考:https://github.com/Netflix/Hystrix/wiki/How-it-Works
3.Hystrixdashboard實現服務監控
Hystrix提供了對於微服務調用狀態的監控信息,但是需要結合spring-boot-actuator模塊一起使用。Hystrix Dashboard是Hystrix的一個組件,Hystrix Dashboard提供一個斷路器的監控面板,可以使我們更好的監控服務和集群的狀態。
1.新建監控微服務
1.新建模塊cloud-consumer-hystrix-dashboard9001
2.完善pom
<?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"> <parent> <artifactId>cloud</artifactId> <groupId>cn.qz.cloud</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-consumer-hystrix-dashboard9001</artifactId> <dependencies> <!--引入自己抽取的工具包--> <dependency> <groupId>cn.qz.cloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId> </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-hystrix</artifactId> </dependency> <!--eureka client--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!--熱部署--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>
3.修改yml
server:
port: 9001
4.啟動類:
package cn.qz.cloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard; @SpringBootApplication // 開啟儀表盤監控注解 @EnableHystrixDashboard public class HystrixDashboardMain9001 { public static void main(String[] args) { SpringApplication.run(HystrixDashboardMain9001.class, args); } }
2.修改原來的cloud-provider-hystrix-payment8081服務:
注意pom需要加上:
<!--web--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
修改主啟動類:(必須增加下面的getServlet配置,否則報錯連接不到)
package cn.qz.cloud; import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.cloud.netflix.hystrix.EnableHystrix; import org.springframework.context.annotation.Bean; /** * @Author: qlq * @Description * @Date: 22:08 2020/10/17 */ @SpringBootApplication @EnableEurekaClient @EnableCircuitBreaker @EnableHystrix public class PaymentHystrixMain8081 { public static void main(String[] args) { SpringApplication.run(PaymentHystrixMain8081.class, args); } /** * 此配置是為了服務監控而配置,與服務容錯本身無關,springcloud升級后的坑 * ServletRegistrationBean因為SpringBoot的默認路徑不是 “/hystrix.stream" * 只要在自己的項目里配置上下的servlet就可以了 */ @Bean public ServletRegistrationBean getServlet() { HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet(); ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet); registrationBean.setLoadOnStartup(1); registrationBean.addUrlMappings("/hystrix.stream"); registrationBean.setName("HystrixMetricsStreamServlet"); return registrationBean; } }
修改后啟動服務
3.啟動hystrixdashboard服務
(1)訪問首頁如下:
(2)多次訪問http://localhost:8081/hystrix/payment/circuit/-1,使其斷路器打開
(3)輸入以下地址查看:http://localhost:8081/hystrix.stream
(4)進入monitor
(5)上面測試的效果不是很明顯,可以用jmeter批量進行測試
補充:可以設置hystrix默認執行時長,超時進行降級處理,這里需要注意下Ribbon鏈接時長和等待請求處理時長的影響
hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 1000
這些配置的屬性都可以從類HystrixPropertiesManager、 HystrixCommandProperties 中查看。
補充:如果通過feign調用服務沒有進行服務的降級。比如A服務調B服務,B服務拋出除0異常,A服務報錯如下:
會報feign相關的錯誤:
2020-11-26 14:54:17.603 ERROR 27404 --- [o-auto-1-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is com.netflix.hystrix.exception.HystrixRuntimeException: PaymentHystrixService#error() failed and no fallback available.] with root cause feign.FeignException$InternalServerError: status 500 reading PaymentHystrixService#error() at feign.FeignException.serverErrorStatus(FeignException.java:195) ~[feign-core-10.4.0.jar:na] at feign.FeignException.errorStatus(FeignException.java:144) ~[feign-core-10.4.0.jar:na] at feign.FeignException.errorStatus(FeignException.java:133) ~[feign-core-10.4.0.jar:na] at feign.codec.ErrorDecoder$Default.decode(ErrorDecoder.java:92) ~[feign-core-10.4.0.jar:na] at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:151) ~[feign-core-10.4.0.jar:na] at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:80) ~[feign-core-10.4.0.jar:na] at feign.hystrix.HystrixInvocationHandler$1.run(HystrixInvocationHandler.java:109) ~[feign-hystrix-10.4.0.jar:na] at com.netflix.hystrix.HystrixCommand$2.call(HystrixCommand.java:302) ~[hystrix-core-1.5.18.jar:1.5.18] at com.netflix.hystrix.HystrixCommand$2.call(HystrixCommand.java:298) ~[hystrix-core-1.5.18.jar:1.5.18] at rx.internal.operators.OnSubscribeDefer.call(OnSubscribeDefer.java:46) ~[rxjava-1.3.8.jar:1.3.8] at rx.internal.operators.OnSubscribeDefer.call(OnSubscribeDefer.java:35) ~[rxjava-1.3.8.jar:1.3.8] at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:48) ~[rxjava-1.3.8.jar:1.3.8] at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30) ~[rxjava-1.3.8.jar:1.3.8] at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:48) ~[rxjava-1.3.8.jar:1.3.8] at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30) ~[rxjava-1.3.8.jar:1.3.8] at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:48) ~[rxjava-1.3.8.jar:1.3.8] at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30) ~[rxjava-1.3.8.jar:1.3.8] at rx.Observable.unsafeSubscribe(Observable.java:10327) ~[rxjava-1.3.8.jar:1.3.8] at rx.internal.operators.OnSubscribeDefer.call(OnSubscribeDefer.java:51) ~[rxjava-1.3.8.jar:1.3.8] at rx.internal.operators.OnSubscribeDefer.call(OnSubscribeDefer.java:35) ~[rxjava-1.3.8.jar:1.3.8] at rx.Observable.unsafeSubscribe(Observable.java:10327) ~[rxjava-1.3.8.jar:1.3.8] at rx.internal.operators.OnSubscribeDoOnEach.call(OnSubscribeDoOnEach.java:41) ~[rxjava-1.3.8.jar:1.3.8] at rx.internal.operators.OnSubscribeDoOnEach.call(OnSubscribeDoOnEach.java:30) ~[rxjava-1.3.8.jar:1.3.8] at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:48) ~[rxjava-1.3.8.jar:1.3.8] at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30) ~[rxjava-1.3.8.jar:1.3.8] at rx.Observable.unsafeSubscribe(Observable.java:10327) ~[rxjava-1.3.8.jar:1.3.8] at rx.internal.operators.OperatorSubscribeOn$SubscribeOnSubscriber.call(OperatorSubscribeOn.java:100) ~[rxjava-1.3.8.jar:1.3.8] at com.netflix.hystrix.strategy.concurrency.HystrixContexSchedulerAction$1.call(HystrixContexSchedulerAction.java:56) ~[hystrix-core-1.5.18.jar:1.5.18] at com.netflix.hystrix.strategy.concurrency.HystrixContexSchedulerAction$1.call(HystrixContexSchedulerAction.java:47) ~[hystrix-core-1.5.18.jar:1.5.18] at com.netflix.hystrix.strategy.concurrency.HystrixContexSchedulerAction.call(HystrixContexSchedulerAction.java:69) ~[hystrix-core-1.5.18.jar:1.5.18] at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55) ~[rxjava-1.3.8.jar:1.3.8] at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) ~[na:1.8.0_171] at java.util.concurrent.FutureTask.run(FutureTask.java:266) ~[na:1.8.0_171] at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) ~[na:1.8.0_171] at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) ~[na:1.8.0_171] at java.lang.Thread.run(Thread.java:748) [na:1.8.0_171]
補充:另外每個應用可以通過全局異常攔截器進行規避一些錯誤,攔截到錯誤之后將錯誤信息返回給上個服務。