Hystrix(豪猪)熔断器官网:https://github.com/Netflix/hystrix
一、解决了什么?
Hystrix帮我们解决雪崩问题
雪崩问题
微服务中,服务间调用关系错综复杂,一个请求,可能要调用多个微服务接口才能实现,会形成非常复杂的调用链路:
只有这些服务都成功,这个请求才会成功。
如果此时,某个服务出现异常:
例如微服务i发生异常,请求阻塞,用户不会得到响应,则tomcat这个线程不会释放,这个请求占用了tomcat的一个连接,但又不返回,因为tomcat的连接是有限的(线程连接池里有固定有限数量的连接),
如果其他用户也发起这个请求,那么会有越来越多的连接被占用,这些线程都会阻塞,tocmat可用连接数会越来越少,最后将可用连接占用完全,最后再来请求即使不是访问此服务(i)的,也无法访问了(没有连接,线程用完了)资源耗尽了,服务就崩溃了,形成雪崩效应。
解决方式: Hystrix:
- 线程隔离,服务降级(线程隔离内部帮我们实现,我们只需要配置服务的降级)
- 服务熔断
原理
1、线程隔离,服务降级
线程隔离是将服务器中的全部连接池,按各个微服务的权重分配给各个微服务一定量的县城里连接数。每个服务都自己独立的线程池。
比如,tomcat的总连接数是500,给服务a分配10个连接,给服务b分配5个连接,给服务c分配8个连接。
这样的好处是,当服务b发生阻塞时,只会阻塞这5个线程,其余线程并不会全部拿来请求服务b,请求到达其他服务时也有线程可用。
把不同的服务请求用不同的线程池隔离
但是,仅仅线程隔离是不够的,要将这5个线程永远阻塞吗?
不会,还要有服务降级,就是说当这个服务的线程池被占满时,当再有请求此服务时,会立即告诉你此服务不可用,返回一个错误信息。这样来,即便线程池满了也不会让你无限等待。
而且这5个线程也不会一直占着,会释放的。
Hystrix为每个依赖服务调用分配一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队,加速失败的判定时间。
用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,或者请求超时,则会进行降级处理,什么时服务降级?
服务降级:优先保证核心服务,而非核心服务不可用(暂时不可用,返回错误信息)或弱可用
用户的请求故障时,不会被阻塞,更不会无休止啊的等待或者看到系统崩溃,至少可以看到一个执行结果(例如返回友好提示信息)
服务降级虽然会导致请求失败,但是不会导致阻塞,而且最多会影响这个依赖服务对应的线程池中的资源,对其他服务没有相应。
触发Hystrix服务降级的情况:
- 线程池已满
- 请求超时
2、服务熔断
二、该怎么做?
默认情况的请求超时时长为1s
当超过1s服务未相应时,就认为服务有问题,返回错误信息。
为了模拟服务不可用,我们在 user-service 的 UserController/queryById 方法里添加
try {//模拟服务阻塞
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
用来模拟服务阻塞,看hystrix是否会对服务进行降级控制
二.1、线程隔离,服务降级
1、引入依赖
在服务提供方,引入Hystrix依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
2、在启动类上添加启用熔断器注解
@EnableCircuitBreaker
由于添加注解后,启动类上的注解为:
//启用熔断器
@EnableCircuitBreaker
//启用eureka注册处中心
@EnableDiscoveryClient
@SpringBootApplication
上面三个注解可用以下注解替换:
@SpringCloudApplication
3、编写降级逻辑,做失败处理
第一种,针对方法
当目标服务的调用出现故障,我们希望快速失败,给用户一个友好提示,因此需要编写好失败时的降级处理逻辑。在方法上添加@HystrixCommand注解
@GetMapping("/{id}")
@HystrixCommand(fallbackMethod = "queryByIdFallback")//返回值,参数必须一样
public String queryById(@PathVariable String id){
String url = "http://user-service/user/"+id;
String user = restTemplate.getForObject(url,String.class);
//getForObject本来就是返回的json格式字符串,不过看你写了User.class,他又给你转为对象了
return user;
}
public String queryByIdFallback(String id){
return "服务器正忙,请稍后重试...";
}
降级逻辑方法必须跟正常逻辑方法保证:参数一致,返回值一致。
第二种,针对类
直接在类上添加注解@DefaultProperties(defaultFallback = “defaultFallback”),然后在方法上开启熔断@HystrixCommand
@Slf4j
@RestController
@RequestMapping("consumer")
@DefaultProperties(defaultFallback = "defaultFallback")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/{id}")
@HystrixCommand//只是启用服务降级,一旦触发,就找默认Fallback
public String queryById(@PathVariable String id){
String url = "http://user-service/user/"+id;
String user = restTemplate.getForObject(url,String.class);
//getForObject本来就是返回的json格式字符串,不过看你写了User.class,他又给你转为对象了
return user;
}
public String defaultFallback(){
log.error("查询用户信息失败");
return "服务器正忙,请稍后重试...";
}
}
测试:
http://localhost:8081/user/111
http://localhost:8080/consumer/222
访问consumer(设置了服务降级),直接出错误页面,
并且访问时间是1s多
因为超过1s未相应的服务,超过了超时时长,
Hystrix会认为该服务不可用,直接执行错误函数
自定义超时时长
1、针对某个方法
在方法上加的注解里,添加配置
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")//配置超时时长 3s
})
2、针对全局
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 3000
user-service: #这里也可以针对某个服务
execution:
isolation:
thread:
timeoutInMilliseconds: 2000
配置的
hystrix:
command:
default:
的值
值是一个键值对形式:
execution:
isolation:
thread:
timeoutInMilliseconds: 3000
二.2、服务熔断 熔断器
熔断器相当于,电路的熔断器,就是当电路中负载过高,则断开,保护电路。
Hystrix的熔断器也是类似这个功能
熔断器三个状态
关闭,
打开,
半开。
- 关闭状态:
正常情况下,熔断器处于关闭状态,她会查看最近的20次请求里,如果有50%的请求都出现了超时,那么会认为此服务有问题或不可用(占用太多时间),就会将断路器打开,当用户再次访问请求时,就会立即返回错误信息,不会再去请求服务。
- 打开状态:
默认熔断器打开状态持续时间为5s,称为休眠时间窗,5s过后熔断器进入半开状态(这5s内当有请求访问此服务时,立即返回错误信息)
- 半开状态
5s(休眠时间窗)过后会进入半开状态,此时会放一定个数的请求通过,请求访问服务,测试是否正常,如果请求访问服务依然失败,则熔断器会重新进入打开状态,再次开始休眠时间窗;如果请求访问服务正常,则熔断器进入关闭状态,请求正常访问服务。
三个状态之间的转换:
测试
1、我们将上面程序中,user-service里的睡眠2s去掉,将配置的自定义熔断时长也去掉,剩下干净的程序(defaultFallback类的默认错误处理不要去掉)。
下面来通过模拟服务可用和不可用来看熔断器的三个状态
1、配置
- 熔断统计次数
- 休眠时间窗
- 错误百分比,
@Slf4j
@RestController
@RequestMapping("consumer")
@DefaultProperties(defaultFallback = "defaultFallback")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/{id}")
//熔断统计次数 默认20
//休眠时间窗 休眠10s(10000ms)才会放行一定量的请求 默认5s
//错误百分比,高于60%才会触发开启熔断 默认50%
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"),
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "10")
})
public String queryById(@PathVariable String id){
if(id.equals("111")){//手动控制服务。如果访问111 则让这个请求失败(触发熔断)。请求其他,让这个请求成功
throw new RuntimeException("RuntimeException111");
}
String url = "http://user-service/user/"+id;
String user = restTemplate.getForObject(url,String.class);
//getForObject本来就是返回的json格式字符串,不过看你写了User.class,他又给你转为对象了
return user;
}
public String defaultFallback(){
log.error("查询用户信息失败");
return "服务器正忙,请稍后重试...";
}
}
启动服务,访问
然后我们快速访问好几次111,这时就会触发熔断器开启(最近的10此请求里,有60%的请求都超时,那么会认为服务有问题,熔断器开启),
然后再访问222,发现访问222快速失败。因为此时熔断器已经开启。
等待10s(休眠时间窗)后,再次访问222,服务正常,则熔断器关闭