服务降级——Hystrix 断路器 1、Hystrix 简述 1-1、分布式系统面临的问题 复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败
服务雪崩: 多个微服务之间调用的时候,假设微服务 A 调用微服务 B 和微服务 C,微服务 B 和微服务 C 又调用其它的微服务,这就是所谓的“扇出”,如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务 A 的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。
对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和,比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加、备份队列、线程和其他系统资源紧张,导致整个系统发生更多的级联故障,这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统,所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。
1-2、Hystrix 是什么 Hystrix 是一个用于处理分布式系统的延迟和容错的开源库 ,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix 能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
1-3、Hystrix 能干嘛
服务降级、服务熔断、接近实时的监控······
官网资料:https://github.com/Netflix/Hystrix/wiki/How-To-Use
Hystrix 官宣,停更进维:https://github.com/Netflix/Hystrix
被动修复 bugs、不再接受合并请求、不再发布新版本
2、Hystrix 重要概念 2-1、服务降级 服务器忙,请稍后再试,不让客户端等待并立刻返回一个友好提示,fallback
哪些情况会出发降级?
程序运行异常
超时
服务熔断触发服务降级
线程池/信号量打满也会导致服务降级
2-2、服务熔断 类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示
就是保险丝:服务的降级->进而熔断->恢复调用链路
2-3、服务限流 秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟 N 个,有序进行
3、hystrix 案例 3-1、构建模块 payment8001 新建 cloud-provider-hystrix-payment8001
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 <?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 > springCloud2023</artifactId > <groupId > com.jcvv.springcloud</groupId > <version > 1.0-SNAPSHOT</version > </parent > <modelVersion > 4.0.0</modelVersion > <artifactId > cloud-provider-hystrix-payment8001</artifactId > <properties > <maven.compiler.source > 8</maven.compiler.source > <maven.compiler.target > 8</maven.compiler.target > </properties > <dependencies > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-netflix-hystrix</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-netflix-eureka-client</artifactId > </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 > com.jcvv.springcloud</groupId > <artifactId > cloud-api-common</artifactId > <version > ${project.version}</version > </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 >
application.yml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 server: port: 8001 spring: application: name: cloud-provider-hystrix-payment eureka: client: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://eureka7001.com:7001/eureka
service 层 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 package com.jcvv.springcloud.service;import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;@Service public class PaymentService { public String paymentInfo_OK (Integer id) { return "线程池" + Thread.currentThread().getName() + " paymentInfo_OK,id: " + id + "\t" + "o(n_n)o哈哈~" ; } public String paymentInfo_TimeOut (Integer id) { int timeNumber = 3 ; try { TimeUnit.SECONDS.sleep(timeNumber); } catch (InterruptedException e) { e.printStackTrace(); } return "线程池" + Thread.currentThread().getName() + " paymentInfo_TimeOut,id: " + id + "\t" + "o(n_n)o哈哈~" + " 耗时(s):" + timeNumber + "秒钟" ; } }
controller 层 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 package com.jcvv.springcloud.controller;import com.jcvv.springcloud.service.PaymentService;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.RestController;import javax.annotation.Resource;@RestController @Slf4j public class PaymentController { @Resource private PaymentService paymentService; @Value("${server.port}") private String serverPort; @GetMapping("/payment/hystrix/ok/{id}") public String paymentInfo_OK (@PathVariable("id") Integer id) { String result = paymentService.paymentInfo_OK(id); log.info("******result:" + result); return result; } @GetMapping("/payment/hystrix/timeout/{id}") public String paymentInfo_TimeOut (@PathVariable("id") Integer id) { String result = paymentService.paymentInfo_TimeOut(id); log.info("******result:" + result); return result; } }
主启动类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.jcvv.springcloud;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;import org.springframework.cloud.netflix.eureka.EnableEurekaClient;@SpringBootApplication @EnableEurekaClient public class PaymentHystrixMain8001 { public static void main (String[] args) { SpringApplication.run(PaymentHystrixMain8001.class, args); } }
① 正常测试
② 高并发测试 使用 JMeter 进行压力测试,模拟多个请求
下载压缩包,解压,双击 /bin/ 下的 jmeter.bat 即可启动
开启 Jmeter,来 20000 个并发压死 8001,20000 个请求都去访问 paymentInfo_TimeOut 接口
ramp up 的值是启动全部线程所需的时间
200 个线程数,循环 100 次,1 秒钟启动完毕
添加 HTTP 请求
测试
从测试可以看出,当模拟的超长请求被高并发以后,访问普通的小请求速率也会被拉低。
两个都在自己转圈圈,为什么会被卡死?
tomcat 的默认的工作线程数被打满了,没有多余的线程来分解压力和处理 上面还是服务提供者 8001 自己测试,假如此时外部的消费者 80 也来访问,那消费者只能干等,最终导致消费端 80 不满意,服务端 8001 直接被拖死
看热闹不嫌弃事大,80 新建加入
3-2、构建 hystrix-order80 再压测 新建模块 cloud-consumer-feign-hystrix-order80
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 <?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 > springCloud2023</artifactId > <groupId > com.jcvv.springcloud</groupId > <version > 1.0-SNAPSHOT</version > </parent > <modelVersion > 4.0.0</modelVersion > <artifactId > cloud-consumer-feign-hystrix-order80</artifactId > <properties > <maven.compiler.source > 8</maven.compiler.source > <maven.compiler.target > 8</maven.compiler.target > </properties > <dependencies > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-netflix-hystrix</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-openfeign</artifactId > </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.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 > <dependency > <groupId > com.jcvv.springcloud</groupId > <artifactId > cloud-api-common</artifactId > <version > ${project.version}</version > </dependency > </dependencies > </project >
application.yml 1 2 3 4 5 6 7 8 server: port: 80 eureka: client: register-with-eureka: false service-url: defaultZone: http://eureka7001.com:7001/eureka/
主启动类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.jcvv.springcloud;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.openfeign.EnableFeignClients;@SpringBootApplication @EnableFeignClients public class OrderFeignMain80 { public static void main (String[] args) { SpringApplication.run(OrderFeignMain80.class, args); } }
service 层 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.jcvv.springcloud.service;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;@Component @FeignClient(value = "CLOUD-PAYMENT-SERVICE") public interface PaymentHystrixService { @GetMapping("/payment/hystrix/ok/{id}") public String paymentInfo_OK (@PathVariable("id") Integer id) ; @GetMapping("/payment/hystrix/timeout/{id}") public String paymentInfo_TimeOut (@PathVariable("id") Integer id) ; }
controller 层 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 package com.jcvv.springcloud.controller;import com.jcvv.springcloud.service.PaymentHystrixService;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;@RestController @Slf4j public class OrderHystirxController { @Resource private PaymentHystrixService paymentHystrixService; @GetMapping("/consumer/payment/hystrix/ok/{id}") public String paymentInfo_OK (@PathVariable("id") Integer id) { String result = paymentHystrixService.paymentInfo_OK(id); return result; } @GetMapping("/consumer/payment/hystrix/timeout/{id}") public String paymentInfo_TimeOut (@PathVariable("id") Integer id) { String result = paymentHystrixService.paymentInfo_TimeOut(id); return result; } }
① 正常测试
② 高并发测试 2W 个线程压 8001,消费端 80 微服务再去访问服务端 8001 地址
3-3、故障现象和导致原因 8001 同一层次的其它接口服务被困死,因为 tomcat 线程池里面的工作线程已经被挤占完毕 80 此时调用 8001,客户端访问响应缓慢,转圈圈 上述结论:正因为有上述故障或不佳表现,才有我们的降级/容错/限流等技术诞生
3-4、如何解决?解决的要求 超时导致服务器变慢(转圈):超时不再等待 出错(宕机或程序运行出错):出错要有兜底 解决:
对方服务(8001)超时了,调用者(80)不能一直卡死等待,必须有服务降级 对方服务(8001)宕机了,调用者(80)不能一直卡死等待,必须有服务降级 对方服务(8001)OK,调用者(80)自己出故障或有自我要求(自己的等待时间小于服务提供者)自己处理降级
4、服务降级 4-1、服务端降级 降级配置:在 @HystrixCommand
里面指定超时/出错的回调方法,作为兜底方法
首先 对 8001 的 service 进行配置(对容易超时的方法进行配置) :
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 package com.jcvv.springcloud.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;@Service public class PaymentService { public String paymentInfo_OK (Integer id) { return "线程池" + Thread.currentThread().getName() + " paymentInfo_OK,id: " + id + "\t" + "o(n_n)o哈哈~" ; } @HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler", //超时回调方法 commandProperties = { @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")//超时时间 }) public String paymentInfo_TimeOut (Integer id) { int timeNumber = 5 ; try { TimeUnit.SECONDS.sleep(timeNumber); } catch (InterruptedException e) { e.printStackTrace(); } return "线程池" + Thread.currentThread().getName() + " paymentInfo_TimeOut,id: " + id + "\t" + "o(n_n)o哈哈~" + " 耗时(s):" + timeNumber + "秒钟" ; } public String paymentInfo_TimeOutHandler (Integer id) { return "线程池" + Thread.currentThread().getName() + " paymentInfo_TimeOutHandler,id: " + id + "\t" + "/(ㄒoㄒ)/~~" ; } }
上图故意制造两个异常:
int s =10 /0; 计算异常 我们能接受 3 秒钟,它运行 5 秒钟,超时异常@HystrixCommand 报异常后如何处理?
一旦调用服务方法失败并抛出了错误信息后,会自动调用@HystrixCommand 标注好的 fallbackMethod 调用类中的指定方法
主启动类加上 @EnableCircuitBreaker 注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.jcvv.springcloud;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;import org.springframework.cloud.netflix.eureka.EnableEurekaClient;@SpringBootApplication @EnableEurekaClient @EnableCircuitBreaker public class PaymentHystrixMain8001 { public static void main (String[] args) { SpringApplication.run(PaymentHystrixMain8001.class, args); } }
测试
4-2、消费端降级 上面的案例是服务端降级,现在我们服务端处理 3s,然后返回。但是消费端等 1s 就等不住了,这时候就需要消费端也有降级方法。
80 的降级,原理是一样的,上面的 @HystrixCommand 降级可以放在服务端,也可以放在消费端。但一般放在消费端。
主启动类加上@EnableHystrix
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.jcvv.springcloud;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.netflix.hystrix.EnableHystrix;import org.springframework.cloud.openfeign.EnableFeignClients;@SpringBootApplication @EnableFeignClients @EnableHystrix public class OrderFeignMain80 { public static void main (String[] args) { SpringApplication.run(OrderFeignMain80.class, args); } }
application.yml
1 2 3 4 5 6 7 8 9 10 11 12 server: port: 80 eureka: client: register-with-eureka: false service-url: defaultZone: http://eureka7001.com:7001/eureka/ feign: hystrix: enabled: true
然后对 80 进行服务降级:很明显 service 层是接口,所以我们对消费者,在它的 controller 层进行降级。
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 package com.jcvv.springcloud.controller;import com.jcvv.springcloud.service.PaymentHystrixService;import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;@RestController @Slf4j public class OrderHystirxController { @Resource private PaymentHystrixService paymentHystrixService; @GetMapping("/consumer/payment/hystrix/ok/{id}") public String paymentInfo_OK (@PathVariable("id") Integer id) { String result = paymentHystrixService.paymentInfo_OK(id); return result; } @GetMapping("/consumer/payment/hystrix/timeout/{id}") @HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", commandProperties = { @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000")}) public String paymentInfo_TimeOut (@PathVariable("id") Integer id) { String result = paymentHystrixService.paymentInfo_TimeOut(id); return result; } public String paymentTimeOutFallbackMethod (@PathVariable("id") Integer id) { return "我是消费者80,对方支付系统繁忙请10秒钟后再试 或者 自己运行出错请检查自己,o(╥﹏╥)o" ; } }
然后出现了问题,消费端兜底失败 ⭐controller 中超时时间配置不生效原因: 关键在于 feign:hystrix:enabled: true 的作用,官网解释“Feign 将使用断路器包装所有方法”,也就是将@FeignClient 标记的那个 service 接口下所有的方法进行了 hystrix 包装(类似于在这些方法上加了一个@HystrixCommand),这些方法会应用一个默认的超时时间为 1s,所以你的 service 方法也有一个 1s 的超时时间,service1s 就会报异常,controller 立马进入备用方法,controller 上那个 3 秒那超时时间就没有效果了。
修改如下
feign: hystrix: enabled: true # 在 feign 中开启 hystrix client: config: default: connect-timeout: 5000 read-timeout: 5000
hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 5000
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 server: port: 80 eureka: client: register-with-eureka: false service-url: # 配置服务中心,openFeign去里面找服务 defaultZone: http://eureka7001.com:7001/eureka/ # 用于服务降级 在注解@FeignClient 中添加 fallback 属性值 feign: hystrix: enabled: true # 在feign中开启 hystrix client: config: default: connect-timeout: 5000 read-timeout: 5000 hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 5000
ps:配置文件这里的 timeoutInMilliseconds 并不是覆盖注解中的设置,而是两者取较低值, 同时也会算上 feign: ReadTimeout 的值,也就是三者取最低值。
@EnableHystrix 和@EnableCircuitBreaker 区别 在学习服务降级中,发现了@EnableHystrix 和@EnableCircuitBreaker 的功能类似,研究后特此记录一下。
查看@EnableHystrix 的源码可以发现,它继承了@EnableCircuitBreaker,并对它进行了在封装
1 2 3 4 5 6 7 @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @EnableCircuitBreaker public @interface EnableHystrix { }
这两个注解都是激活 hystrix 的功能,我们根据上面代码得出来结论,只需要在服务启动类加入@EnableHystrix 注解即可,无须增加@EnableCircuitBreaker 注解 ,本身@EnableHystrix 注解已经涵盖了 EnableCircuitBreaker 的功能。
4-3、目前问题 每个业务方法对应一个兜底的方法,代码膨胀 同样和自定义分开 我们定义一个全局的兜底方法,这样就不用每个方法都得写兜底方法了。
全局兜底 @DefaultProperties
加了@DefaultProperties 属性注解:
加了@HystrixCommand 属性注解,并且没有写具体回调方法的,就用统一全局的
加了@HystrixCommand 属性注解,写了具体回调方法的,就用自己的
OrderHystirxController 头注解添加
@DefaultProperties(defaultFallback = “payment_Global_FallbackMethod”)
OrderHystirxController 方法添加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 下面是全局fallback方法 public String payment_Global_FallbackMethod() { return "Global异常处理信息,请稍后再试,/(ㄒoㄒ)/~~"; } //哪些需要默认出错调用需要添加:@HystrixCommand @GetMapping("/consumer/payment/hystrix/ok/{id}") @HystrixCommand public String paymentInfo_OK(@PathVariable("id") Integer id) { int i = 1 / 0; String result = paymentHystrixService.paymentInfo_OK(id); return result; }
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 package com.jcvv.springcloud.controller;import com.jcvv.springcloud.service.PaymentHystrixService;import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;@RestController @Slf4j @DefaultProperties(defaultFallback = "payment_Global_FallbackMethod") public class OrderHystirxController { @Resource private PaymentHystrixService paymentHystrixService; @GetMapping("/consumer/payment/hystrix/ok/{id}") @HystrixCommand public String paymentInfo_OK (@PathVariable("id") Integer id) { int i = 1 / 0 ; String result = paymentHystrixService.paymentInfo_OK(id); return result; } @GetMapping("/consumer/payment/hystrix/timeout/{id}") @HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", commandProperties = { @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000")}) public String paymentInfo_TimeOut (@PathVariable("id") Integer id) { String result = paymentHystrixService.paymentInfo_TimeOut(id); return result; } public String paymentTimeOutFallbackMethod (@PathVariable("id") Integer id) { return "我是消费者80,对方支付系统繁忙请10秒钟后再试 或者 自己运行出错请检查自己,o(╥﹏╥)o" ; } public String payment_Global_FallbackMethod () { return "Global异常处理信息,请稍后再试,/(ㄒoㄒ)/~~" ; } }
业务逻辑混乱在一起的问题(解耦) 我们改在 service 层进行服务降级
根据 cloud-consumer-feign-hystrix-order80 已经有的 PaymentHystrixService 接口,重新新建一个类 PaymentFallbackService 实现该接口,统一为接口里面的方法进行异常处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.jcvv.springcloud.service;import org.springframework.stereotype.Component;@Component public class PaymentFallbackService implements PaymentHystrixService { @Override public String paymentInfo_OK (Integer id) { return "-----PaymentFallbackService fall back-paymentInfo_OK ,o(╥﹏╥)o" ; } @Override public String paymentInfo_TimeOut (Integer id) { return "-----PaymentFallbackService fall back-paymentInfo_TimeOut ,o(╥﹏╥)o" ; } }
将 controller 中添加的 @HystrixCommand 和 @DefaultProperties 两个注解去掉,在@FelignClient 添加回调类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.jcvv.springcloud.service;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;@Component @FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT", fallback = PaymentFallbackService.class) public interface PaymentHystrixService { @GetMapping("/payment/hystrix/ok/{id}") public String paymentInfo_OK (@PathVariable("id") Integer id) ; @GetMapping("/payment/hystrix/timeout/{id}") public String paymentInfo_TimeOut (@PathVariable("id") Integer id) ; }
测试
5、服务熔断 断路器:一句话就是家里的保险丝
作者文章:https://martinfowler.com/bliki/CircuitBreaker.html
5-1、熔断是什么 熔断机制是应对雪崩效应的一种微服务链路保护机制,当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息,当检测到该节点微服务调用响应正常后,恢复调用链路。
在 Spring Cloud 框架里,熔断机制通过 Hystrix 实现。Hystrix 会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是 5 秒内 20 次调用失败,就会启动熔断机制。
熔断机制的注解是@HystrixCommand
5-2、实操 修改 cloud-provider-hystrix-payment8001
PaymentService 在 PaymentService 里添加熔断方法
//=====服务熔断 @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 String paymentCircuitBreaker(@PathVariable(“id”) Integer id) { if(id < 0) { throw new RuntimeException(“** id 不能负数”); } String serialNumber = IdUtil.simpleUUID();
return Thread.currentThread().getName()+”\t”+”调用成功,流水号: “ + serialNumber; } public String paymentCircuitBreaker_fallback(@PathVariable(“id”) Integer id) { return “id 不能负数,请稍后再试,/(ㄒ o ㄒ)/~~ id: “ +id; }
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 package com.jcvv.springcloud.service;import cn.hutool.core.util.IdUtil;import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;import org.springframework.stereotype.Service;import org.springframework.web.bind.annotation.PathVariable;import java.util.concurrent.TimeUnit;@Service public class PaymentService { public String paymentInfo_OK (Integer id) { return "线程池" + Thread.currentThread().getName() + " paymentInfo_OK,id: " + id + "\t" + "o(n_n)o哈哈~" ; } @HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler", //超时回调方法 commandProperties = { @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000")//超时时间 }) public String paymentInfo_TimeOut (Integer id) { try { TimeUnit.MILLISECONDS.sleep(3000 ); } catch (InterruptedException e) { e.printStackTrace(); } return "线程池" + Thread.currentThread().getName() + " paymentInfo_TimeOut,id: " + id + "\t" + "o(n_n)o哈哈~" + " 耗时(s):" ; } public String paymentInfo_TimeOutHandler (Integer id) { return "线程池: " +Thread.currentThread().getName()+ " 8001系统繁忙或者运行报错,请稍后再试,id: " +id+"\t" +"o(╥﹏╥)o" ; } @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 String paymentCircuitBreaker (@PathVariable("id") Integer id) { if (id < 0 ) { throw new RuntimeException ("******id 不能负数" ); } String serialNumber = IdUtil.simpleUUID(); return Thread.currentThread().getName()+"\t" +"调用成功,流水号: " + serialNumber; } public String paymentCircuitBreaker_fallback (@PathVariable("id") Integer id) { return "id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: " +id; } }
controller 层
//====服务熔断 @GetMapping(“/payment/circuit/{id}”) public String paymentCircuitBreaker(@PathVariable(“id”) Integer id) { String result = paymentService.paymentCircuitBreaker(id); log.info(“****result: “ + result); return result; }
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 package com.jcvv.springcloud.controller;import com.jcvv.springcloud.service.PaymentService;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.RestController;import javax.annotation.Resource;@RestController @Slf4j public class PaymentController { @Resource private PaymentService paymentService; @Value("${server.port}") private String serverPort; @GetMapping("/payment/hystrix/ok/{id}") public String paymentInfo_OK (@PathVariable("id") Integer id) { String result = paymentService.paymentInfo_OK(id); log.info("******result:" + result); return result; } @GetMapping("/payment/hystrix/timeout/{id}") public String paymentInfo_TimeOut (@PathVariable("id") Integer id) { String result = paymentService.paymentInfo_TimeOut(id); log.info("******result:" + result); return result; } @GetMapping("/payment/circuit/{id}") public String paymentCircuitBreaker (@PathVariable("id") Integer id) { String result = paymentService.paymentCircuitBreaker(id); log.info("****result: " + result); return result; } }
测试 如何测试,输入一个地址 http://localhost:8001//payment/circuit/-1,带负数。疯狂请求 6 次以上,
然后快速把地址改为正数:http://localhost:8001//payment/circuit/1。此时,发现请求失败
说明熔断器开启
实验效果为,多次出错调用 fallback 后,调用正常的也出错调用 fallback。过了一会又自己恢复了。
5-3、熔断类型
5-4、断路器在什么情况下开始起作用 涉及到断路器的三个重要参数快照时间窗、请求总数阀值、错误百分比阀值
快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的 10 秒。
请求总数阀值:在快照时间窗内,必须满足请求总数阀值才有资格熔断。默认为 20,意味着在 10 秒内,如果该 hystrix 命令的调用次数不足 20 次,即使所有的请求都超时或具他原因失败,断路器都不会打开。
错误百分比阀值:当请求总数在快照时间窗内超过了阀值,假设发生了 30 次调用,在这 30 次调用中,有 15 次发生了超时异常,也就是超过 50%的错误百分比,在默认设定 50%阀值情况,这时候就会将断路器打开。
5-5、断路器开启或者关闭的条件
当满足一定的阀值的时候(默认 10 秒内超过 20 个请求次数)
当失败率达到一定的时候(默认 10 秒内超过 50%的请求失败)
到达以上阀值,断路器将会开启
当开启的时候,所有请求都不会进行转发
一段时间之后(默认是 5 秒),这个时候断路器是半开状态,会让其中一个请求进行转发;如果成功,断路器会关闭;若失败,继续开启。重复 4 和 5
5-6、断路器打开之后 再有请求调用的时候,将不会调用主逻辑,而是直接调用降级 fallback,通过断路器,实现了自动地发现错误并将降级逻辑切换为主逻辑,减少响应延迟的效果
5-7、原来的主逻辑如何恢复 对于这一问题,hystrix 也为我们实现了自动恢复功能。
当断路器打开,对主逻辑进行熔断之后,hystrix 会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑,当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器将继续闭合,主逻辑恢复;如果这次请求依然有问题,断路器继续进入打开状态,休眠时间窗重新计时。
5-8、@HystrixCommand 所有配置 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 @HystrixCommand(fallbackMethod = "str_fallbackMethod", groupKey = "strGroupCommand", commandKey = "strCommand", threadPoolKey = "strThreadPool", commandProperties = { // 设置隔离策略,THREAD 表示线程池 SEMAPHORE:信号池隔离 @HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"), // 当隔离策略选择信号池隔离的时候,用来设置信号池的大小(最大并发数) @HystrixProperty(name = "execution.isolation.semaphore.maxConcurrentRequests", value = "10"), // 配置命令执行的超时时间 @HystrixProperty(name = "execution.isolation.thread.timeoutinMilliseconds", value = "10"), // 是否启用超时时间 @HystrixProperty(name = "execution.timeout.enabled", value = "true"), // 执行超时的时候是否中断 @HystrixProperty(name = "execution.isolation.thread.interruptOnTimeout", value = "true"), // 执行被取消的时候是否中断 @HystrixProperty(name = "execution.isolation.thread.interruptOnCancel", value = "true"), // 允许回调方法执行的最大并发数 @HystrixProperty(name = "fallback.isolation.semaphore.maxConcurrentRequests", value = "10"), // 服务降级是否启用,是否执行回调函数 @HystrixProperty(name = "fallback.enabled", value = "true"), // 是否启用断路器 @HystrixProperty(name = "circuitBreaker.enabled", value = "true"), // 该属性用来设置在滚动时间窗中,断路器熔断的最小请求数。例如,默认该值为 20 的时候, // 如果滚动时间窗(默认10秒)内仅收到了19个请求, 即使这19个请求都失败了,断路器也不会打开。 @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"), // 该属性用来设置在滚动时间窗中,表示在滚动时间窗中,在请求数量超过 // circuitBreaker.requestVolumeThreshold 的情况下,如果错误请求数的百分比超过50, // 就把断路器设置为 "打开" 状态,否则就设置为 "关闭" 状态。 @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"), // 该属性用来设置当断路器打开之后的休眠时间窗。 休眠时间窗结束之后, // 会将断路器置为 "半开" 状态,尝试熔断的请求命令,如果依然失败就将断路器继续设置为 "打开" 状态, // 如果成功就设置为 "关闭" 状态。 @HystrixProperty(name = "circuitBreaker.sleepWindowinMilliseconds", value = "5000"), // 断路器强制打开 @HystrixProperty(name = "circuitBreaker.forceOpen", value = "false"), // 断路器强制关闭 @HystrixProperty(name = "circuitBreaker.forceClosed", value = "false"), // 滚动时间窗设置,该时间用于断路器判断健康度时需要收集信息的持续时间 @HystrixProperty(name = "metrics.rollingStats.timeinMilliseconds", value = "10000"), // 该属性用来设置滚动时间窗统计指标信息时划分"桶"的数量,断路器在收集指标信息的时候会根据 // 设置的时间窗长度拆分成多个 "桶" 来累计各度量值,每个"桶"记录了一段时间内的采集指标。 // 比如 10 秒内拆分成 10 个"桶"收集这样,所以 timeinMilliseconds 必须能被 numBuckets 整除。否则会抛异常 @HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "10"), // 该属性用来设置对命令执行的延迟是否使用百分位数来跟踪和计算。如果设置为 false, 那么所有的概要统计都将返回 -1。 @HystrixProperty(name = "metrics.rollingPercentile.enabled", value = "false"), // 该属性用来设置百分位统计的滚动窗口的持续时间,单位为毫秒。 @HystrixProperty(name = "metrics.rollingPercentile.timeInMilliseconds", value = "60000"), // 该属性用来设置百分位统计滚动窗口中使用 “ 桶 ”的数量。 @HystrixProperty(name = "metrics.rollingPercentile.numBuckets", value = "60000"), // 该属性用来设置在执行过程中每个 “桶” 中保留的最大执行次数。如果在滚动时间窗内发生超过该设定值的执行次数, // 就从最初的位置开始重写。例如,将该值设置为100, 滚动窗口为10秒,若在10秒内一个 “桶 ”中发生了500次执行, // 那么该 “桶” 中只保留 最后的100次执行的统计。另外,增加该值的大小将会增加内存量的消耗,并增加排序百分位数所需的计算时间。 @HystrixProperty(name = "metrics.rollingPercentile.bucketSize", value = "100"), // 该属性用来设置采集影响断路器状态的健康快照(请求的成功、 错误百分比)的间隔等待时间。 @HystrixProperty(name = "metrics.healthSnapshot.intervalinMilliseconds", value = "500"), // 是否开启请求缓存 @HystrixProperty(name = "requestCache.enabled", value = "true"), // HystrixCommand的执行和事件是否打印日志到 HystrixRequestLog 中 @HystrixProperty(name = "requestLog.enabled", value = "true"), }, threadPoolProperties = { // 该参数用来设置执行命令线程池的核心线程数,该值也就是命令执行的最大并发量 @HystrixProperty(name = "coreSize", value = "10"), // 该参数用来设置线程池的最大队列大小。当设置为 -1 时,线程池将使用 SynchronousQueue 实现的队列, // 否则将使用 LinkedBlockingQueue 实现的队列。 @HystrixProperty(name = "maxQueueSize", value = "-1"), // 该参数用来为队列设置拒绝阈值。 通过该参数, 即使队列没有达到最大值也能拒绝请求。 // 该参数主要是对 LinkedBlockingQueue 队列的补充,因为 LinkedBlockingQueue // 队列不能动态修改它的对象大小,而通过该属性就可以调整拒绝请求的队列大小了。 @HystrixProperty(name = "queueSizeRejectionThreshold", value = "5"), } )
6、服务限流 后面高级篇讲解 alibaba 的 Sentinel 说明
7、服务监控 hystrixDashboard 7-1、概述 除了隔离依赖服务的调用以外,Hystrix 还提供了准实时的调用监控(Hystrix Dashboard)
Hystrix 会持续地记录所有通过 Hystrix 发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等。Netflix 通过 hystrix-metrics-event-stream 项目实现了对以上指标的监控。Spring Cloud 也提供了 Hystrix Dashboard 的整合,对监控内容转化成可视化界面。
7-2、仪表盘 9001 新建 cloud-consumer-hystrix-dashboard9001
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 <?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 > springCloud2023</artifactId > <groupId > com.jcvv.springcloud</groupId > <version > 1.0-SNAPSHOT</version > </parent > <modelVersion > 4.0.0</modelVersion > <artifactId > cloud-consumer-hystrix-dashboard9001</artifactId > <properties > <maven.compiler.source > 8</maven.compiler.source > <maven.compiler.target > 8</maven.compiler.target > </properties > <dependencies > <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.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 >
application.yml
主启动类 加上这个注解:@EnableHystrixDashboard
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.jcvv.springcloud;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); } }
测试
7-3、监控实战 所有 Provider 微服务提供类(8001/8002/8003)都需要监控依赖配置
1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-actuator</artifactId > </dependency >
此外还要有主启动类上加 @EnableCircuitBreaker
,用于对豪猪熔断机制的支持
8001 主启动类
此配置是为了服务监控而配置,与服务容错无关
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.jcvv.springcloud;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;import org.springframework.cloud.netflix.eureka.EnableEurekaClient;@SpringBootApplication @EnableEurekaClient @EnableCircuitBreaker public class PaymentHystrixMain8001 { public static void main (String[] args) { SpringApplication.run(PaymentHystrixMain8001.class, args); } }
启动 1 个 eureka 或者 3 个 eureka 集群均可
9001 监控 8001,填写监控地址:http://localhost:8001/hystrix.stream
注意:SpringCloud 版本过高会导致一直 loading,需要降版本。
错误
处理方式 在 8001 主启动类加上一下方法
/** > 此配置是为了服务监控而配置,与服务容错本身无关,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; }
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 package com.jcvv.springcloud;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.context.annotation.Bean;@SpringBootApplication @EnableEurekaClient @EnableCircuitBreaker public class PaymentHystrixMain8001 { public static void main (String[] args) { SpringApplication.run(PaymentHystrixMain8001.class, args); } @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; } }
再测试
打开新网页,输入 http://localhost:8001//payment/circuit/1,再切回 hystrix dashboard,测试成功
相关链接:Gateway网关