Hystrix
Table of Contents generated with DocToc (opens new window)
# Hystrix
# 概述
# 分布式系统面临的问题
复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。
# 服务雪崩
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的**“扇出”**。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。
对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列、线程和其他系统资源紧张,进而导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
# 是什么
Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
"断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控〔类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
# 能干嘛
服务降级
服务熔断
接近实时的监控
# 官方资料
wiki:
https://github.com/Netflix/Hystrix/wiki/How-To-Use
Hystrix已停止更新,进入仅维护状态
# Hystrix重要概念
# 服务降级(fallback)
- 服务器忙,请稍后再试,不让客户端等待并立刻返回一个友好提示,fallback
# 哪些情况会触发降级
- 程序运行异常
- 超时
- 服务熔断触发服务降级
- 线程池/信号量打满
# 服务熔断(break)
类比保险丝,在达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示。
服务降级->进而熔断->一段时间再恢复正常调用
机制概述
熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。 当检测到该节点微服务调用响应正常后,恢复调用链路。
在Spring Cloud框架里,熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的状况, 当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解是@Hys
# 服务限流(flowlimit)
限制访问量(秒杀等高并发操作)
# Hystrix降级案例
新建cloud-provider-hystrix-payment8001工程
# payment
# pom
新增hystrix的依赖即可
这里由于cloud2020已经没有hystrix的版本了,所以需要自己加一个引入
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.2.10.RELEASE</version>
</dependency>
2
3
4
5
# yaml
server:
port: 8001
# 服务名称 (必写)
spring:
application:
name: cloud-payment-hystrix-service
eureka:
client:
# false表示不想注册中心注册自己
register-with-eureka: true
# false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要检查服务
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# service
package com.zdk.springcloud.service;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* @Description
* @Author zdk
* @Date 2022/10/28 18:03
*/
@Service
public class HystrixPaymentService {
public String paymentInfoOk(Integer id){
return "线程池:"+Thread.currentThread().getName()+" paymentInfoOk,id: "+id+"\t"+"欧克欧克";
}
public String paymentInfoTimeout(Integer id){
int time = 3;
try {
TimeUnit.SECONDS.sleep(time);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "线程池:"+Thread.currentThread().getName()+" paymentInfoTimeout,id: "+id+"\t"+"欧克欧克 耗时:"+time+"秒";
}
}
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
# controller
package com.zdk.springcloud.controller;
import com.zdk.springcloud.service.HystrixPaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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;
/**
* @Description
* @Author zdk
* @Date 2022/10/28 18:07
*/
@Slf4j
@RestController
public class HystrixPaymentController {
@Autowired
private HystrixPaymentService hystrixPaymentService;
@Value("${server.port}")
private String serverPort;
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfoOk(@PathVariable Integer id){
String result = hystrixPaymentService.paymentInfoOk(id);
log.info("*****result:"+result);
return result;
}
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfoTimeout(@PathVariable Integer id){
String result = hystrixPaymentService.paymentInfoTimeout(id);
log.info("*****result:"+result);
return result;
}
}
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
# 测试
启动eureka7001,再启动此8001payment,访问测试
目前访问正常
# 压测
使用jemter 2万qps请求timeout,此时再去请求ok接口,会发现ok接口响应速度明显变慢了
# order
# 新建feign-hystrixOrder80
压测是一样的结果,会出现卡顿
# 解决
有三种情况需要进行服务降级处理:
- 对方服务(8001)超时了,调用者(80)不能一直卡死等待,
调用者
必须有服务降级 - 对方服务(8001)宕机了,调用者(80)不能一直等待,
调用者
必须有服务降级 - 对方服务(8001)OK,调用者(80)自己出故障或有自我要求(自己的等待时间小于服务提供者),
调用者
必须自己处理服务降级
# Hystrix降级配置
# 提供侧
首先在主启动类上增加
@EnableCircuitBreaker
注解编写方法
package com.zdk.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; /** * @Description * @Author zdk * @Date 2022/10/28 18:03 */ @Service public class HystrixPaymentService { public String paymentInfoOk(Integer id){ return "线程池:"+Thread.currentThread().getName()+" paymentInfoOk,id: "+id+"\t"+"欧克欧克"; } @HystrixCommand(fallbackMethod = "paymentInfoTimeoutHandler", commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")}) public String paymentInfoTimeout(Integer id){ int a = 10/0; int time = 5; try { TimeUnit.SECONDS.sleep(time); } catch (InterruptedException e) { throw new RuntimeException(e); } return "线程池:"+Thread.currentThread().getName()+" paymentInfoTimeout,id: "+id+"\t"+"欧克欧克 耗时:"+time+"秒"; } public String paymentInfoTimeoutHandler(Integer id){ return "线程池:"+Thread.currentThread().getName()+"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重要的是
@HystrixCommand
注解,此注解声明的方法即为被Hystrix接管的方法,fallbackMethod
参数用来指定调用因异常或超时失败后,兜底处理的降级的方法的名称,是全限定名(同一类中可以省略)
它的commandProperties属性,是
@HystrixProperty
注解的数组形式,name指定要配置的属性名称,value配置值,比如execution.isolation.thread.timeoutInMilliseconds
就是指定服务调用的最大超时时间,value单位为毫秒,超过这个时间,Hystrix会调用fallbackMethod
指定的方法作为返回测试
# 消费侧(一般都在此侧做降级配置)
消费侧使用feign进行服务调用
主启动类增加
@EnableHystrix
注解,开启Hystrix//...... @EnableCircuitBreaker public @interface EnableHystrix { //可以看到这个注解实际就是EnableCircuitBreaker注解,只是特定用于Hystrix }
1
2
3
4
5为了方便,就在消费侧controller中编写测试
@GetMapping(value = "/consumer/payment/hystrix/timeout1/{id}") @HystrixCommand(fallbackMethod = "timeoutHandler", commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1500")}) public String timeout1(@PathVariable("id") Integer id) { return paymentFeignService.paymentInfoTimeout(id); } public String timeoutHandler(@PathVariable("id") Integer id){ return "消费者80发现被调用方系统繁忙~~~~"; }
1
2
3
4
5
6
7
8
9结果:
在提供侧也配置了超时的时候,如果消费侧所需的时间要更短(1500<2000),那么会在消费侧直接降级,不会调用到提供侧的降级方法,相反,如果时间符合消费侧要求,就走提供侧降级
@HystrixCommand(fallbackMethod = "paymentInfoTimeoutHandler", commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "2000")}) public String paymentInfoTimeout(Integer id){ int a = 10/0; int time = 5; try { TimeUnit.SECONDS.sleep(time); } catch (InterruptedException e) { throw new RuntimeException(e); } return "线程池:"+Thread.currentThread().getName()+" paymentInfoTimeout,id: "+id+"\t"+"欧克欧克 耗时:"+time+"秒"; } public String paymentInfoTimeoutHandler(Integer id){ return "线程池:"+Thread.currentThread().getName()+"id:"+id+" 超时或运行报错->服务降级咯~~~~~"; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15同时,如果消费侧先出现异常,那么也是在消费侧直接降级,不进入提供侧降级
# 全局服务降级DefaultProperties
因为如果每个业务方法都对应一个fallback方法的话,会导致代码大量重复、膨胀
我们使用@DefaultProperties(defaultFallback="")
来配置全局的服务降级方法
在消费侧的controller上增加
@DefaultProperties(defaultFallback = "paymentGlobalFallbackMethod")
编写paymentGlobalFallbackMethod方法
//下面是全局fallback方法 public String paymentGlobalFallbackMethod(){ return "Global异常处理信息,请稍后再试~~~~~"; }
1
2
3
4去掉原来指明的fallback方法注解,增加
@HystrixCommand
注解,表示不指定fallback,用全局的@GetMapping(value = "/consumer/payment/hystrix/timeout1/{id}") // @HystrixCommand(fallbackMethod = "timeoutHandler", commandProperties = // {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1500")}) @HystrixCommand public String timeout1(@PathVariable("id") Integer id) { //模拟报错 int a = 10/0; return paymentFeignService.paymentInfoTimeout(id); }
1
2
3
4
5
6
7
8
9
测试会发现,此时就走了全局的方法
# FeignFallback配置
上面的全局配置,是在controller层接口上的配置,导致代码耦合严重,我们可以在Feign的Service里面,对所有的外部服务的接口,进行统一的降级配置,这样就不需要在controller层写了
新建一个类,让其实现feign的service接口
@Component public class PaymentFallbackService implements PaymentFeignHystrixService{ @Override public String paymentInfoOk(Integer id) { return "调用失败-----PaymentFallbackService fallback-paymentInfoOk"; } @Override public String paymentInfoTimeout(Integer id) { return "调用失败-----PaymentFallbackService fallback-paymentInfoTimeout"; } }
1
2
3
4
5
6
7
8
9
10
11
12开启yaml配置
feign: circuitbreaker: enabled: true
1
2
3将提供者服务关闭,进行测试,发现走到我们实现的新方法
# Hystrix熔断案例
# 修改payment8001的service
增加下面的方法
/**
* =========服务熔断
*/
@HystrixCommand(fallbackMethod = "paymentCircuitBreakerFallback",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"),//失败率达到多少后跳闸
//这里意思是:在10秒内,如果10次请求中有6次请求都失败了,就会进行断路
})
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 paymentCircuitBreakerFallback(@PathVariable("id") Integer id){
return "******id 不能是负数,请稍后再试o(╥﹏╥)o~ id:"+id;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 修改controller
@GetMapping("/payment/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id){
String result = hystrixPaymentService.paymentCircuitBreaker(id);
log.info("*****result {}",result);
return result;
}
2
3
4
5
6
# 测试
经测试会发现,多次传入负数导致方法抛出异常后,再传入正数也同样会走到fallback,即实现了熔断,再多次调用正数一段时间后,调用恢复
调整窗口期时间参数发现,触发熔断以后,确实要等待接近这个窗口期时间以后,再遇到正确的请求,才会关闭熔断恢复正常
# 总结
# 熔断类型
- 熔断打开:请求不再进行调用当前服务,内部设置时钟一般为MTTR(平均故障处理时间),当打开时长达到所设时钟则进入半熔断状态
- 熔断关闭:熔断关闭不会对服务进行熔断
- 熔断半开:部分请求根据规则调用当前服务,如果请求成功且符合规则,则任务当前服务恢复正常,关闭熔断
# 断路器开启或关闭的条件
- 当满足一定的阈值的时候(默认10秒内超过20个请求次数)
- 当失败率达到一定的时候(默认10秒内超过50%的请求失败)
- 同时达到以上阈值,断路器将会开启
- 当断路器开启的时候,所有请求都不会进行转发
- 一段时间之后(默认是5秒),这个时候断路器会成为半开状态,会让其中一个请求进行转发,如果请求成功,断路器会关闭,如果失败,继续开启,重复4、5步骤
# 断路器打开之后
再有请求调用的时候,将不会调用主逻辑,而是直接调用降级fallback,通过断路器,实现了自动地发现错误并将降级逻辑切换为主逻辑,减少响应延迟的效果。
原来的主逻辑要如何恢复呢?
对于这一问题,hystrix也为我们实现了自动恢复功能。 当断路器打开,对主逻辑进行熔断之后,hystrix会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑,当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器将闭合,主逻辑恢复;如果这次请求依然有问题,断路器继续进入打开状态,休眠时间窗重新计时。
# Hystrix工作流程
# 服务监控 Hystrix Dashboard
除了隔离依赖服务的调用以外,Hystrix还提供了准实时的调用监控(Hystrix Dashboard),Hystri会持续地记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等。Netflix通过 hystrix-metrics-event-stream项目实现了对以上指标的监控。Spring Cloud也提供了Hystrix Dashboard的整合,对监控内容转化成可视化界面。
新建cloud-consumer-hystrix-dashboard9001模块
# 依赖
<dependencies>
<!-- 通用的依赖-->
<dependency>
<groupId>com.zdk</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>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
<version>2.2.10.RELEASE</version>
</dependency>
</dependencies>
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
# yaml
server:
port: 9001
hystrix:
dashboard:
# 这里要配置允许监控的host列表 要把地址加上才能监控
# 其实配置为 * 就可以了
# proxy-stream-allow-list: localhost
proxy-stream-allow-list: *
2
3
4
5
6
7
8
9
# 启动类
package com.zdk.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
/**
* @Description
* @Author zdk
* @Date 2022/11/9 17:19
*/
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardMain9001 {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardMain9001.class, args);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 所有服务提供者依赖
所有微服务提供者(8001/8002/8003)都要依赖监控配置即都要有actuator的依赖
# 启动
启动9001服务,访问http://localhost:9001/hystrix,出现以下界面代表成功
# 还需添加的配置
除yaml中的host配置外,还需在被监控服务中添加一个bean才行,且
@EnableCircuitBreaker
注解要打开
package com.zdk.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.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
/**
* @Description
* @Author zdk
* @Date 2022/10/28 18:02
*/
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
@EnableCircuitBreaker
public class HystrixPaymentMain8001 {
public static void main(String[] args) {
SpringApplication.run(HystrixPaymentMain8001.class, args);
}
/**
* 此配置是为了服务监控,与服务容错本身无关 是springcloud升级后的坑
* ServletRegistrationBean因为SpringBoot的默认路径不是"/hystrix.stream",
* 只要在自己的项目里配置好下面的servlet就行了
* @return
*/
@Bean
public ServletRegistrationBean<HystrixMetricsStreamServlet> getServlet(){
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean<HystrixMetricsStreamServlet> registrationBean = new ServletRegistrationBean<>(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
}
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
# 测试
输入监控地址:http://localhost:8001/hystrix.stream
刚进入是loading状态,然后访问多次8001的服务以后就会出现访问的结果了
且注意,监控只会监控
@HystrixCommand
的方法