一、概述
1.1、分布式系统面临的问题
复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免的失败。
微服务之间调用关系错综复杂,一个请求,可能需要调用多个微服务接口才能实现,会形成非常复杂的调用链路:

1.2、服务雪崩
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的**“扇出”**。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。
对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。
如图,一次业务请求,需要调用A、P、H、I四个服务,这四个服务又可能调用其它服务。
如果此时,某个服务出现异常:

例如微服务I发生异常,请求阻塞,用户不会得到响应,则tomcat的这个线程不会释放,于是越来越多的用户请求到来,越来越多的线程会阻塞:

服务器支持的线程和并发数有限,请求一直阻塞,会导致服务器资源耗尽,从而导致所有其它服务都不可用,形成雪崩效应。
这就好比,一个汽车生产线,生产不同的汽车,需要使用不同的零件,如果某个零件因为种种原因无法使用,那么就会造成整台车无法装配,陷入等待零件的状态,直到零件到位,才能继续组装。 此时如果有很多个车型都需要这个零件,那么整个工厂都将陷入等待的状态,导致所有生产都陷入瘫痪。一个零件的波及范围不断扩大。
Hystix解决雪崩问题的手段有两个:
1.3、Hystrix是什么?
Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack)**,而不是长时间的等待或者抛出调用方无法处理的异常**,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
二、Hystrix重要概念
2.1、服务降级(fallback)
1、介绍
假设要调用的微服务/系统出现故障不可用了,此时不让客户端等待并立刻返回一个友好提示,fallback
2、会触发服务降级的情况
- 程序运行异常
- 超时
- 服务熔断触发服务降级
- 线程池/信号量打满也会触发服务降级。
2.2、服务熔断(break)
1、介绍
类比保险丝,当电器总功率达到一定限度后直接熔断,不让电器继续工作。即服务达到最大访问量后直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示,这样可以放置整个系统被拖垮。
故服务熔断会触发服务降级。
服务熔断就是保险丝:服务的降级->进而熔断->恢复调用链路
2、Hystrix的弹性容错
不同于电路熔断只能断不能自动重连,Hystrix可以实现弹性容错,当情况好转之后,可以自动重连。这就好比魔术师把鸽子变没了容易,但是真正考验技术的是如何把消失的鸽子再变回来。
通过断路的方式,可以将后续请求直接拒绝掉,一段时间之后允许部分请求通过,如果调用成功则回到电路闭合状态,否则继续断开。
3、Hystrix熔断状态机的三个状态
- Closed:关闭状态,所有请求都正常访问。
- Open:打开状态,所有请求都会被降级。Hystix会对请求情况计数,当一定时间内失败请求百分比达到阈值,则触发熔断,断路器会完全打开。默认失败比例的阈值是50%,请求次数最少不低于20次。
- Half Open:半开状态,open状态不是永久的,打开后会进入休眠时间(默认是5S)。随后断路器会自动进入半开状态。此时会释放部分请求通过,若这些请求都是健康的,则会完全关闭断路器,否则继续保持打开,再次进行休眠计时
2.3、服务限流(float limited)
秒杀高并发等操作中,严禁请求一窝蜂地过来拥挤,需要使请求排队,一秒钟N个,有序进行。
三、Hystrix案例
3.1、构建基础项目
新建cloud-provider-hystrix-payment8001微服务
1、POM
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
| <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>com.cloudstudy</groupId> <artifactId>common</artifactId> <version>1.0-SNAPSHOT</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.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>
|
2、YML
1 2 3 4 5 6 7 8 9 10 11
| 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/
|
3、主启动类
1 2 3 4 5 6 7
| @EnableEurekaClient @SpringBootApplication public class PaymentHystrixMain8001 { public static void main(String[] args) { SpringApplication.run(PaymentHystrixMain8001.class); } }
|
4、业务类
在service层中编写一个正常访问和一个超时的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Service public class PaymentServiceImpl {
public String paymentInfo_OK(Integer id) { return "线程池:" + Thread.currentThread().getName() + "paymentInfo_OK,id:" + id + "\t" + "😄"; }
public String paymentInfo_TimeOut(Integer id) { int timeOut = 3; try { TimeUnit.SECONDS.sleep(timeOut); } catch (InterruptedException interruptedException) { interruptedException.printStackTrace(); } return "线程池:" + Thread.currentThread().getName() + "paymentInfo_TimeOut,id:" + id + "\t" + "😢耗时(S):" + timeOut; } }
|
5、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
| @Slf4j @RestController public class PaymentController { @Autowired private PaymentServiceImpl 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; } }
|
6、测试
打开eureka7001、hystrix-payment8001,测试

调用成功的方法:http://localhost:8001/payment/hystrix/ok/31

调用超时方法:http://localhost:8001/payment/hystrix/timeout/31

3.2、高并发测试
以上面的项目为根基
1、使用JMeter进行压力测试

启动Jmeter后再次访问http://localhost:8001/payment/hystrix/ok/31接口,发现响应明显变慢,即http://localhost:8001/payment/hystrix/timeout/31接口被挤爆后,http://localhost:8001/payment/hystrix/ok/31接口也会被拖累。
2、使用消费者调用Payment8001
新建一个cloud-consumer-feign-hystrix-order80微服务
引入依赖
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
| <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-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>com.cloudstudy</groupId> <artifactId>common</artifactId> <version>1.0-SNAPSHOT</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.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>
|
编写yml文件
1 2 3 4 5 6 7 8 9 10 11 12 13
| server: port: 80
eureka: client: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://eureka7001.com:7001/eureka/
spring: application: name: cloud-provider-hystrix-order
|
主启动类
1 2 3 4 5 6 7 8 9
| @EnableFeignClients @SpringBootApplication @ComponentScan({"com.hzx.springcloud","com.hzx.common"}) public class OrderHystrixMain80 { public static void main(String[] args) { SpringApplication.run(OrderHystrixMain80.class,args); System.out.println("http://localhost/swagger-ui.html"); } }
|
业务类:service
1 2 3 4 5 6 7 8
| @FeignClient(value = "cloud-provider-hystrix-payment") 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
| @Slf4j @RestController public class OrderHystrixController {
@Autowired private PaymentHystrixService paymentHystrixService;
@GetMapping("/consumer/payment/hystrix/ok/{id}") public String paymentInfo_OK(@PathVariable("id") Integer id) { String result = paymentHystrixService.paymentInfo_OK(id); log.info("结果为:" + result); return result; } @GetMapping("/consumer/payment/hystrix/timeout/{id}") public String paymentInfo_TIMEOUT(@PathVariable("id") Integer id) { String result = paymentHystrixService.paymentInfo_TimeOut(id); log.info("结果为:" + result); return result; }
}
|
故障原因:
8001同一层次的其他接口服务被困死,因为tomcat线程里面的工作线程已经被挤占完毕
80此时调用8001,客户端访问响应缓慢,转圈圈
3.3、解决方案
1、问题1
超时导致服务器变慢(转圈)
超时不再等待
2、问题2
出错(服务器宕机或者程序运行错误)
出错要有兜底方案
3、解决
对方服务(8001)超时了,调用者(80)不能一直卡死等待,必须有服务降级
对方服务(8001)down机了,调用者(80)不能一直卡死等待,必须有服务降级
对方服务(8001)OK,调用者(80)自己出故障或有自我要求(自己的等待时间小于服务提供者),自己处理降级
3.4、服务降级(fallback)
1、对自身进行降级配置
先从8001自身找问题:需要设置自身调用超时时间的峰值,在峰值时间内可以正常运行,超过峰值后需要有兜底的方法处理,做降级处理。
使用步骤如下
- 在业务类中启用@HystrixCommand注解
- 使用@HystrixCommand注解的fallbackMethod属性指定兜底方法
- 使用@HystrixCommand注解的commandProperties属性指定自身调用超时时间的峰值。
1 2 3
| @HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",commandProperties = { @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000") //3秒钟以内就是正常的业务逻辑 })
|
- 若业务时间在超时时间峰值之内,那么走正常的业务逻辑,超过峰值后直接调用兜底方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Service public class PaymentServiceImpl {
public String paymentInfo_OK(Integer id) { return "线程池:" + Thread.currentThread().getName() + "paymentInfo_OK,id:" + id + "\t" + "😄"; }
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",commandProperties = { @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000") //3秒钟以内就是正常的业务逻辑 }) public String paymentInfo_TimeOut(Integer id) { int timeOut = 5; try { TimeUnit.SECONDS.sleep(timeOut); } catch (InterruptedException interruptedException) { interruptedException.printStackTrace(); } return "线程池:" + Thread.currentThread().getName() + "paymentInfo_TimeOut,id:" + id + "\t" + "😢耗时(S):" + timeOut; } public String paymentInfo_TimeOutHandler(Integer id) { return "线程池:" + Thread.currentThread().getName() + "paymentInfo_TimeOutHandler,id:" + id + "\t" + "😓"; } }
|
- 在主启动类上添加@EnableCircuitBreaker注解激活服务降级
- 测试,由于我们设置了峰值时间为3s,而我们在业务中休眠了5s,所以会触发服务降级

除超市外,系统出现异常时也会调用兜底方案
2、对消费端进行降级配置
对于需要远程调用payment8001的订单微服务80,它也可以更好地保护自己,对自己进行客户端降级保护。
hystrix的服务降级既可以做在消费(调用服务)端,也可以做在服务(提供服务)端,但服务降级一般做在消费端
1 2 3
| feign: hystrix: enabled: true
|
- 在80订单微服务的主启动类上添加@EnableHystrix注解激活Hystrix
- 修改业务类
1 2 3 4 5 6 7 8 9 10 11 12
| @GetMapping("/consumer/payment/hystrix/timeout/{id}") @HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",commandProperties = { @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1500") }) 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秒钟后再试或者自己运行出错请检查自己,(┬_┬)"; }
|
测试
- 修改paymentHystrix8001中的业务类,确保8001中的方法可以正常运行
1 2 3 4 5 6 7 8 9 10 11 12
| @HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",commandProperties = { @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "5000") //3秒钟以内就是正常的业务逻辑 }) public String paymentInfo_TimeOut(Integer id) { int timeOut = 3; try { TimeUnit.SECONDS.sleep(timeOut); } catch (InterruptedException interruptedException) { interruptedException.printStackTrace(); } return "线程池:" + Thread.currentThread().getName() + "paymentInfo_TimeOut,id:" + id + "\t" + "😢耗时(S):" + timeOut; }
|
- 启动微服务,此时由于80订单中微服务的超时时间峰值为1.5s,而8001休眠了3s,所以80会调用失败,此时会调用自身的兜底方法

如果80微服务端发生运行时异常,那么也会直接执行兜底方案。
3、以上配置存在的问题
- 每个业务方法对应一个兜底的方法,代码膨胀
- 业务逻辑和兜底方法混杂在一起,代码混乱。
- 全局兜底方法和自定义兜底方法分开
配置一个全局兜底方法,使用@DefaultProperties(defaultFallback = “全局兜底方法名”) 来指定全局兜底方法。
加入全局兜底方法后,代码结构可变为

1对N,除了个别重要核心业务有专属兜底方法,其余普通的方法都可以通过全局兜底方法统一兜底。
修改80订单微服务的Controller层
1 2 3 4 5 6
|
public String payment_Global_FallbackMethod() { return "Global异常处理信息,请稍后再试...😓"; }
|
1
| @DefaultProperties(defaultFallback = "payment_Global_FallbackMethod")
|
1 2 3 4 5 6
| @GetMapping("/consumer/payment/hystrix/timeout/{id}") @HystrixCommand public String paymentInfo_TIMEOUT(@PathVariable("id") Integer id) { String result = paymentHystrixService.paymentInfo_TimeOut(id); return result; }
|
4、为远程调用接口添加一个统一降级类
服务降级,客户端去调用服务端,碰上服务端宕机或关闭。
修改order80订单微服务,根据order80订单微服务已经有的PaymentHystrixService接口,重新新建一个类(PaymentFallbackService)实现该接口,统一为接口里面的方法进行异常处理。
编写一个类,实现PaymentHystrixService接口
1 2 3 4 5 6 7 8 9 10 11 12
| @Service public class PaymentFallbackService implements PaymentHystrixService { @Override public String paymentInfo_OK(Integer id) { return "----PaymentFallbackService fall back-payment_OK,😂"; }
@Override public String paymentInfo_TimeOut(Integer id) { return "----PaymentFallbackService fall back-paymentInfo_TimeOut,😂"; } }
|
此时没有配置任何兜底方法的paymentInfo_OK方法由PaymentFallbackService类中的实现方法兜底
测试


此时服务端provider已经down了,但是我们做了服务降级处理,让客户端在服务端不可用时也会获得提示信息而不会挂起耗死服务器
3.5、服务熔断
1、修改cloud-provider-hystrix-payment8001
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @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 不能负数,请稍候再试,(┬_┬)/~~ id: " +id; }
|
属性 | 含义 |
---|
@HystrixProperty(name = “circuitBreaker.enabled”,value = “true”) | 是否开启断路器 |
@HystrixProperty(name = “circuitBreaker.requestVolumeThreshold”,value = “10”) | 请求次数 |
@HystrixProperty(name = “circuitBreaker.sleepWindowInMilliseconds”,value = “10000”) | 时间窗口期/时间范围 |
@HystrixProperty(name = “circuitBreaker.errorThresholdPercentage”,value = “60”) | 失败率到达多少后跳闸 |
上面设置的意思时,如果10秒内10次请求中有60以上失败,就直接跳闸
1 2 3 4 5 6 7
| @GetMapping("/payment/circuit/{id}") public String paymentCircuitBreaker(@PathVariable("id") Integer id){ String result = paymentService.paymentCircuitBreaker(id); log.info("*******result:"+result); return result; }
|
2、测试符合条件的参数

3、演示服务熔断
- 演示服务熔断,在短时间内连续多次输入小于0的id号,然后再输入正确的id,查看结果

一段时间之后(默认是5秒),这个时候断路器是半开状态,会让其中一个请求进行转发。如果成功,断路器会关闭,若失败,继续开启。
4、结论
熔断类型
- 熔断打开:请求不再进行调用当前服务,内部设置时钟一般为MTTR(平均故障处理时间),当打开时长达到所设时钟则进入熔断状态
- 熔断关闭:熔断关闭不会对服务进行熔断
- 熔断半开:部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断
熔断器开启、半开、关闭的转化关系大致如下
- 当请求错误次数在一定时间内没达到峰值或者请求成功的情况下,熔断器关闭。
- 当请求错误次数在一定时间内达到峰值时,熔断器打开,此时进入服务熔断状态,通过服务降级对任何请求返回提示。
- 当熔断一定时间后,熔断器会进入半开状态,此时它会试着通过一些请求,如果请求通过,那么它会从半开状态转换为关闭状态,此时服务恢复,否则仍然进入打开状态,服务继续熔断。

5、断路器的三个重要参数
分别是快照时间窗、请求总数阈值和错误百分比阈值
- 快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒。
- 请求总数阀值:在快照时间窗内,必须满足请求总数阀值才有资格熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时或其他原因失败,断路器都不会打开。
- 错误百分比阀值:当请求总数在快照时间窗内超过了阀值,比如发生了30次调用,如果在这30次调用中,有15次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%阀值情况下,这时候就会将断路器打开。
6、Hystrix如何自动恢复?
对于这一问题,hystrix也为我们实现了自动恢复功能。
当断路器打开,对主逻辑进行熔断之后,hystrix会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑,当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器将继续闭合,主逻辑恢复,如果这次请求依然有问题,断路器继续进入打开状态,休眠时间窗重新计时。
3.6、服务限流
暂时留空
四、Hystrix工作流程
4.1、流程图

4.2、步骤说明
