一、Hystrix
1.1 Hystrix 介绍
Hystrix 同样是 Netflix 公司出品的一款组件。其目的是为了让我们的应用不被个别服务的错误影响而导致整个系统的级联异常。
在分布式环境中,许多服务依赖项中的一些服务会失败。Hystrix 是一个库,通过添加延迟容忍和容错逻辑,帮助我们控制这些分布式服务之间的交互。Hystrix 通过隔离服务之间的
访问点、停止级联失败和提供回退等来实现这一点,所有这些都可以提高系统的整体弹性。
1.2 级联异常——雪崩
复杂分布式体系结构中的应用程序有许多依赖项,每个依赖项在某些时候都不可避免地会失败。如果主机应用程序没有与这些外部故障隔离,那么它有可能被他们拖垮。
例如:一个请求需要依赖 A,H,I,P 四个服务,当一切都正常的时候如下图所示:
但是,如果在这个过程中,某一个服务出现了延时或者异常情况,那么它久会影响到整
个用户的请求。如下图所示:
如果是在高流量的情况下,一个服务的延迟可能导致整个服务器资源在数秒之内饱和。也就意味着当有一个服务异常的时候,在几秒之内让资源处于饱和状态而无法为用户访问提供服务。如下图所示:
那么 Hystrix 是如何来防止这种雪崩现象的呢?
1.3 线程隔离、服务降级
当使用 Hystrix 来包装每个依赖服务的时候,那么每个依赖服务将会相互隔离。当如果有某个服务发生异常或者延时的时候,它会被限制在某一个范围之内。如下图所示:
Hystrix 为每一个依赖服务的调用分配一个小的线程池;如果线程池已满,那么调用将立即被拒绝,而不会采用排队处理,这样可以加快失败的判定时间。
当某个服务被阻塞(某个服务的线程池已满),那么不可能让这个服务一直被阻塞下去,那么这个时候会采用服务降级,或者熔断来处理。
服务降级:优先保证核心服务,而非核心服务不可用或者弱可用。
1.4 服务降级程序案例
1.4.1 服务降级案例一:降级指定方法
1、在 consumer 中添加依 Hystix 依赖
<!-- hystrix 熔断依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
2、启动服务降级/熔断
在启动类上加上注解:@EnableCircuitBreaker //启用服务熔断(一般使用这个)
3、改造 consumer 程序,并声明一个失败时的回滚处理方法(降级逻辑)。
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private RestTemplate template;
@GetMapping(value="/detail/{id}",produces= MediaType.APPLICATION_JSON_VALUE)
@HystrixCommand(fallbackMethod = "selectUserFallBack")
public User customer(@PathVariable("id")String id){
//使用服务名称调用
String serviceurl = "http://user-service/user/"+id;
return template.getForObject(serviceurl, User.class);
}
/**
* 降级方法,做用:当原方法出现问题时,用此方法代替
* @param id 参数必须和customer参数一直
* @return 与原方法的返回值一直
*/
public User selectUserFallBack(String id){
User user = new User();
user.setUsername("用户信息查询出现异常");
return user;
}
}
@HystrixCommand(fallbackMethod=“selectUserFallBack”):声明一个失败回滚处理函数 selectUserFallBack,当 customer执行超时(默认是 1000毫秒),就会执行 fallback 函数,返回错误提示。
注意:降级逻辑方法的返回值和参数列表要与正常逻辑方法的返回值和参数一致。
4、可以关闭数据库服务
5、测试,分别启动 eureka,service 以及 consumer。然后访问 consumer/user/detail/{id}。
1.4.2 服务降级案例二:默认降级逻辑
在上面的案例中,指定了一个方法的降级逻辑。但是如果在一个类中存在很多的服务,而这些服务的降级逻辑大多都是一样的。那么就可以使用默认降级逻辑来进行统一的处理。
如下程序所示:
1、定义统一返回值类型
不可能所有的服务返回值都是一样的,所以就需要对返回值类
型进行一定的处理。
一般的处理办法是创建一个类,这个类中包含了需要返回的对象,以及各种状态。
public class Msg {
private int flag;
private String message;
private Object data;
public Msg(int flag, String message) {
this.flag = flag;
this.message = message;
}
public Msg(int flag, Object data) {
this.flag = flag;
this.data = data;
}
public Msg() {
}
public int getFlag() {
return flag;
}
public void setFlag(int flag) {
this.flag = flag;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
2、修改返回值使用
/**
* 添加默认降级方法
*/
@RestController
@RequestMapping("/usermsg")
@DefaultProperties(defaultFallback = "defaultFallback")
public class UserMsgController {
@Autowired
private RestTemplate template;
/**
* 使用统一得返回值类型。
* @param id
* @return
*/
@GetMapping(value="/detail/{id}",produces= MediaType.APPLICATION_JSON_VALUE)
@HystrixCommand(commandProperties={
@HystrixProperty(name="circuitBreaker.errorThresholdPercentage",value="60") //60%
})//设置超时时长
public Msg customer(@PathVariable("id")String id){
//使用服务名称调用
String serviceurl = "http://user-service/user/"+id;
User user = template.getForObject(serviceurl, User.class);
Msg msg = new Msg();
msg.setFlag(1);
msg.setMessage("查询成功");
msg.setData(user);
return msg;
}
/**
* 默认降级方法
* 要求 1、不能有参数
* 2、需要和正常逻辑方法一致
* @return
*/
public Msg defaultFallback(){
Msg msg = new Msg();
msg.setFlag(-1);
msg.setMessage("查询失败");
return msg;
}
}
测试结果:
1、正常情况返回值:
2、异常情况返回值:
1.4.3 设置服务超时时长
在 Hystrix 中,服务的超时时长默认是 1000ms。在实际应用中,可能存在某些耗时服务需要执行比较长的时间。那么默认的 1000ms 就可能在正常情况下也不能完成服务的运行。那么就需要这个超时时长可设置。
设置 Hystrix 的超时时长一般有两种方式:
1、在代码中设置超时时长
如下代码所示:
@GetMapping(value="/detail/{id}",produces= MediaType.APPLICATION_JSON_VALUE)
@HystrixCommand(commandProperties={
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseco
nds",value="3000")
})//设置超时时长
public Msg customer(@PathVariable("id")String id){
Integer i = Integer.parseInt(id);
//当 id 为偶数时,绝对失败
if(i % 2 == 0){
throw new RuntimeException("");
}
//使用服务名称调用
String serviceurl = "http://user-service/user/"+id;
User user = template.getForObject(serviceurl, User.class);
Msg msg = new Msg();
msg.setFlag(1);
msg.setMessage("查询成功");
msg.setData(user);
return msg;
}
2、在配置文件中设置超时时长
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 6000 # 设置 hystrix 的超时时间为 6000ms
上面在配置文件中配置的超时时长是对整个项目起作用。而在方法中设置超时时长,则是只对某一个服务起作用。
也可以配置指定服务的超时时长:如下配置所示:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 6000 # 设置 hystrix 的超时时间为 6000ms
userservice: # 设置 userservice 服务的超时时长
execution:
isolation:
thread:
timeoutInMilliseconds: 3000
1.4.4 重试和降级
上面的程序执行中,虽然执行了服务的降级,但是并没有执行服务的重试因为 Ribbon超时时间设置的是 1000ms。与 Hystix 的超时默认时间是一样的(都是 1000ms),重试机制没有被触发,而是先触发了熔断。所以,在项目配置中,Ribbon 的超时时长一定要设置为小于 Hystix 的超时时长。
可以通过hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds来设置Hystix 的超时时间。如下所示:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 6000 # 设置 hystrix 的超时时间为 6000ms
1.5 服务熔断
1.5.1 服务熔断介绍
在上面的示例中,对超时服务进行了降级。但是这个超时时长会比较长,例如上面设置的 6000ms,3000ms。但是对于一个正常的业务逻辑,一般只需要几十毫秒就能完成。超时3000ms 都能处理上百次请求了。那么如果某个服务发生了异常,那么就需要对每个请求都要等待。就会是整个程序的并发处理能力极具下降。这种情况如何处理?就需要使用到Hystrix 的熔断服务,直接将此服务断开以保证其他服务的高可用(断开后还要连回来)。
熔断(circuit breaker),例如保险丝,电路中正确安置保险丝,保险丝就会在电流异常升高到一定的高度和热度的时候,自身熔断切断电流,保护了电路安全运行。熔断就跟保险丝一样,当一个服务请求并发特别大,服务器已经招架不住了,调用错误率飙升,当错误率达到一定阈值后,就将这个服务熔断了。熔断之后,后续的请求就不会再请求服务器了,以减缓服务器的压力。而在程序中,熔断之后肯定不能一直都不调用服务了,所以还需要有个
重启的机制。
服务熔断的三种状态:
- 关闭(closed):正常情况下 hystrix 为关闭状态,所有服务都可正常访问
- 半打开(half open):当服务接口对应的熔断器状态为 open 的时候,所有服务调用方调用该服务方法时候都是执行本地降级方法,那么什么时候才会恢复到远程调用呢?Hystrix 提供了一种测试策略,即设置了一个时间窗口(休眠时间窗,默认是 5 秒),从熔断器状态变为 open 状态开始的一个时间窗口内,调用该服务接口时候都委托服务降级方法进行执行。如果时间超过了时间窗口,则把熔断状态从 open->half-open,这时候服务调用方调用服务接口时候,就可以发起远程调用而不再使用本地降级接口,如果发起远程调用还是失败,则重新设置熔断器状态为 open 状态,重新记录时间窗口开始时间。
- 打开(open):当访问同一个接口超过设定阈值并且错误次数超过设置错误阈值时,就会打开熔断机制,状态从 closed->open。(默认失败比例阈值为:50%;请求次数最少不少于 20 次。——最近 20 次请求中,有 50%的请求都超时)
1.5.2 服务熔断测试
代码如下所示:
/**
* 添熔断机制测试
*/
@RestController
@RequestMapping("/usermsg")
@DefaultProperties(defaultFallback = "defaultFallback")
public class UserMsgController {
@Autowired
private RestTemplate template;
/**
* 使用统一得返回值类型。
* @param id
* @return
*/
@GetMapping(value="/detail/{id}",produces= MediaType.APPLICATION_JSON_VALUE)
@HystrixCommand(commandProperties={
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="2000"),
@HystrixProperty(name="circuitBreaker.requestVolumeThreshold",value="10"), //10 次
@HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds",value= "10000"),//休眠 10 秒
@HystrixProperty(name="circuitBreaker.errorThresholdPercentage",value="60") //60%
})//设置超时时长
public Msg customer(@PathVariable("id")String id){
Integer i = Integer.parseInt(id);
//当 id 为偶数时,绝对失败
if(i % 2 == 0){
throw new RuntimeException("");
}
//使用服务名称调用
String serviceurl = "http://user-service/user/"+id;
User user = template.getForObject(serviceurl, User.class);
Msg msg = new Msg();
msg.setFlag(1);
msg.setMessage("查询成功");
msg.setData(user);
return msg;
}
/**
* 默认降级方法
* 要求 1、不能有参数
* 2、需要和正常逻辑方法一致
* @return
*/
public Msg defaultFallback(){
Msg msg = new Msg();
msg.setFlag(-1);
msg.setMessage("查询失败");
return msg;
}
}
3、启动服务并测试
1)正常请求,id 为奇数时。
2)请求偶数,异常请求。连续异常请求 6-10 次。
3)然后再请求奇数(正常请求)
服务被熔断,正常请求也会失败
4)等待 10 秒后,再访问奇数
服务又恢复正常
5)说明
第一:在正常项目中,一般可以使用默认的时间窗等参数,不用调整(当然,根据实际项目的情况,也可能需要调整这些参数以优化项目性能)
第二:在上面的程序中,当连续访问 id 偶数达到(10 次的 60%,6 次)。则达到熔断的阈值。然后熔断器状态从 close->opern。此时,服务被熔断,及时再访问 id 奇数,同样会执行服务降级。
第三:当休眠时间窗时间(10 秒)到达后(oper->half open),再次请求奇数 id,则访问成功(half open->open)。