目录
1 简介
背景,为什么要有熔断器:
- 在微服务中,一个请求可能需要多个微服务接口才能实现,会形成复杂的调用链路。
- 如果某服务出现异常,请求阻塞,用户得不到响应,容器中线程不会释放,于是越来越多用户请求堆积,越来越多线程阻塞。
- 单服务器支持线程和并发数有限,请求如果一直阻塞,会导致服务器资源耗尽,从而导致所有其他服务都不可用,从而形成雪崩效应。
- 而Hystrix解决雪崩问题的手段,主要是服务降级(兜底)、线程隔离,下面的内容只讲服务降级。
Hystrix 功能概述:
- 对通过第三方客户端库访问的依赖项(通常是通过网络)的延迟和故障进行保护和控制
- 在复杂的分布式系统中阻止雪崩效应
- 回退,尽可能优雅地降级
- 快速失败,快速恢复
2 熔断器原理
熔断器是一个大的概念,总的来说就是当负载超出一定限制后,熔断器会触发对系统的保护机制。
而服务降级是熔断器保护机制的具体表现形式。
- 问:服务A依赖于服务B,但是B无法正常提供服务了,怎么办?
- 答:在服务A上设置一个服务降级的方法,一旦服务B不能正常为服务A提供服务时,服务A也不会因为B的问题而受到牵连,于是服务A会采取保底的降级策略来继续提供服务。虽然此时的这个服务A不是用户真正想要的,但是总好过在显示界面上出现一堆异常提醒,或者牵连到后续的服务。
2.1 熔断器的三个状态
- 关闭状态,所有请求正常访问
- 打开状态,所有请求都会被降级。
Hystrix会对请求情况计数,当一定时间失败请求百分比达到阈值,则触发熔断,断路器完全关闭
默认失败比例的阈值是50%,请求次数最低不少于20次
- 半开状态
打开状态不是永久的,打开一会后会进入休眠时间(默认5秒)。休眠时间过后会进入半开状态。
半开状态:熔断器会判断下一次请求的返回状况,如果成功,熔断器切回关闭状态。如果失败,熔断器切回打开状态。
2.2 熔断的条件
首先,我认为“熔断”和“降级”是相似的概念,就类似于捕获并处理异常一样。
- 1 访问超时
- 2 服务不可用(死了)
- 3 服务抛出异常(虽然有异常但还活着)
- 此外:请求导致服务异常到达阈值后,会牵连到所有服务都会被降级
3 熔断案例
基于上面的4种情况,假设服务消费者A依赖于服务提供者B(甚至其他服务),现在B出错,看看访问服务A的时候有什么反应。
实现步骤
- 引入熔断的starter依赖坐标
- 开启熔断的注解@EnableCircuitBreaker
- 编写服务降级处理的方法
- 配置熔断的策略
- 模拟异常代码
- 测试熔断服务效果
<!--熔断Hystrix starter-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
//注解简化写法:微服务中,注解往往引入多个,简化注解可以使用组合注解。@SpringCloudApplication
// 等同于@SpringBootApplication+@EnableDiscoveryClient+@EnableCircuitBreaker
@SpringCloudApplication
public class ConsumerServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerServiceApplication.class, args);
}
/**
* 让消费者支持负载均衡:只需要在RestTemplate注入方法上加一个@LoadBalanced注解即可!
*/
@Bean //相当于 applicationContext.xml中的bean标签
@LoadBalanced //开启restTemplate对象支持负载均衡的能力
//一旦开启,请求时传统的url地址 http://127.0.0.1:9091/user/findById?id=1 就不能用了
// 而是用负载均衡地址: http://user-service/user/findById?id=1
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
/**
* 编写服务降级处理方法:使用@HystrixCommand定义fallback方法
*/
@RequestMapping("/hystrix_ribbon_consumer/{id}")
@HystrixCommand(fallbackMethod ="queryByIdFallback")
//参数fallbackMethod:设置该方法的降级方法,defaultFallback:设置全局服务降级方法
public User HystrixRibbonFindById(@PathVariable("id") Integer id){
//负载均衡访问服务,此时的访问地址中host和port部分变成了被访问服务的名称
//注意:restTemplate一旦使用了负载均衡,传统的url访问就不能用了!也就是说上面那个方法findById已经不适用了
//因为现在的做法是通过服务名字,再结合负载均衡器去选取适当的服务,
// 如果还是沿用url访问的话会将“host+port”当成服务名字去eureka中找对应的服务集群,这样当然是找不到的!
String url = "http://user-service/user/findById?id="+id;
User user = restTemplate.getForObject(url, User.class);
return user;
}
/**
* 服务降级方法:该方法的参数与返回值必须和被降级的方法保持一致!
*/
public User queryByIdFallback(Integer id){
User user = new User();
user.setUsername("网络拥挤,请稍后再试");
return user;
}
在A服务中配置熔断策略如下:
# 配置熔断策略:
# 触发熔断的三种情况:1、服务不可用 2、请求超时 3、抛异常
# 强制打开熔断器功能,默认false
hystrix.command.default.circuitBreaker.forceOpen: false
# 触发熔断错误比例阈值,默认值50%
hystrix.command.default.circuitBreaker.errorThresholdPercentage: 20
# 熔断后休眠时长,默认值5秒(单位是毫秒)
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds: 20000
# 熔断触发最小请求次数,默认值是20
hystrix.command.default.circuitBreaker.requestVolumeThreshold: 5
# 熔断超时设置,默认为1秒
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 2000
到目前为止,基础条件已经搭建好了,接下来测试2.2中所提到的4种异常情况:
- 1 访问超时
- 2 服务不可用(死了)
- 3 服务抛出异常(虽然有异常但还活着)
- 此外:请求导致服务异常到达阈值后,会牵连到所有服务都会被降级
(1)访问超时
在服务提供者的方法中设置3秒延迟,而在Hystrix的熔断配置中要求超过2秒就要熔断,发现确实对服务进行了降级处理(熔断)。
(2)服务不可用
由于现在还开着Ribbon负载均衡,服务提供者一共3个实例要是只停一个服务不容易导致服务不可用,干脆把三个服务提供者实例都停掉,结果发现确实对服务进行了降级处理。
(3)服务抛出异常
修改服务提供者的代码:如果请求参数为1则直接抛出异常,否则不抛出异常。这样的修改方式一方面测试了当服务提供者向消费者抛出异常时是否会进入熔断。而另一方面可用于测试当多次请求失败后出发熔断器开启是否会影响到其他服务的正常访问(请求参数不是1的情况)。
此时当访问参数为2时,仍然可以正常访问(),但参数为1时则触发降级。
多次访问请求参数为1的那个地址后,由于服务请求异常率到达阈值,则连请参数为2的那个服务(类比而已)也不能正常访问了,于是验证了第四种情况!
4 服务降级方法
注意:熔断服务降级方法必须保证与被降级方法相同的参数列表和返回值。
服务降级有两种编写方式:
- 方法上服务降级的fallback方法(3节中用的就是这个局部降级的方法)
使用HystrixCommon注解,定义
@HystrixCommand(fallbackMethod="queryByIdFallBack")用来声明一个降级逻辑的fallback兜底方法
- 类上默认服务降级的fallback方法(全局的降级方法)
把 @DefaultProperties(defaultFallback = "defaultFallBackMethod") 放到类上,而不是方法上
注意:如果用了全局的服务降级策略,@HystrixCommand后面就可以不加参数,但是该注解必须要加!