目录
代码地址:代码地址-GitHub
一:Hystrix简介
在微服务场景中,通常会有很多层的服务调用。如果一个底层服务出现问题,故障会被向上传播给用户。我们需要一种机制,当底层服务不可用时,可以阻断故障的传播。这就是断路器的作用。他是系统服务稳定性的最后一重保障。
在springcloud中断路器组件就是Hystrix。Hystrix也是Netflix套件的一部分。它是一个延迟和容错库,用于隔离访问远程服务,第三方库,防止出现级联失效。他的功能是,当对某个服务的调用在一定的时间内(默认10s,由metrics.rollingStats.timeInMilliseconds配置),有超过一定次数(默认20次,由circuitBreaker.requestVolumeThreshold参数配置)并且失败率超过一定值(默认50%,由circuitBreaker.errorThresholdPercentage配置),该服务的断路器会打开。返回一个由开发者设定的fallback。
fallback可以是另一个由Hystrix保护的服务调用,也可以是固定的值。fallback也可以设计成链式调用,先执行某些逻辑,再返回fallback。
二:问题引入
在微服务中我们把各个业务模块都放在不同的服务器上,有时候用户的一个请求要从不同的模块(服务)收集数据完成一个响应,当其中一个模块挂掉了,请求无法得到响应,或者返回一个错误。如果这种问题在这个服务容器上积累,可能就会导致其它正常的请求无法获得连接导致整个系统挂掉。
比如我上篇介绍的Eureka中的消费者服务模块,它调用服务生产者模块,如果服务生产者模块挂掉,那么前面的调用链也会挂掉。
这就好比,一个汽车生产线,生产不同的汽车,需要使用不同的零件,如果某个零件因为某种原因无法使用,那么就会造成整台车无法装配,陷入等待零件的状态。直到零件到位,才能继续组装。此时如果有很多个车型都需要这个零件,那么整个工厂都会陷入等待状态,导致所有的生产线瘫痪。一个零件的影响范围不断扩大。
为了解决类似雪崩的问题,就要用到SpringCloud的Hystrix。它解决此类问题的方法有两个:线程隔离,服务熔断
三:线程隔离,服务降级原理
线程隔离:
比如上面的所示图,假如服务模块有很多 服务A,服务B,服务C等等,之前访问方式是用tomcat中的线程池来管理线程,如果服务C挂了,无法响应,那么会造成后续的调用的线程也挂掉,线程无法回收,线程池耗尽,整个系统挂掉。
在Hystrix中却是为每个服务模块都分配一个线程池,比如服务A一个线程池管理5个线程,服务B一个线程池管理5个线程,服务C一个线程池管理5个线程,所以当上面所说的问题出现时,只会耗尽服务C的线程池,而不影响整个系统的线程池。
但是只有线程隔离还是不行的,线程池满了后面再有线程进来怎么办?那就需要用到服务降级了
服务降级:
当服务C线程池满了以后,再请求进来的线程就会进入等待,等待了假如5秒(可设置)没有反应就会立即返回一个状态,告诉前面调用方服务正在忙。并不一定线程池满了才会有这样的情况,请求响应超时也会这样处理。这样不会造成整个系统的阻塞。
服务降级就是优先保证核心服务,非核心服务不可用或弱可用。
触发服务降级:线程池满,请求超时,出现错误等。
四:使用Hystrix
我们接着Eureka博文(Eureka实践一,Eureka实践二)实践中的代码来继续完善。
1:引入依赖。我们是要及时响应用户的请求不要阻塞,因此需要在消费者服务模块使用。但是我在直接引入依赖使用的时候启动报错,因为我是在之前Eureka的基础上使用,就报了jar包冲突,搞了半天才解决掉,这里简单介绍一下怎么解决的。
idea右侧点击Maven Projects看到如下:
我们引入hystrix导致了很多jar冲突,可以按下图看项目的依赖树找到冲突的jar
有冲突的jar包在依赖树中会显示红色,我们可以选中有冲突的jar包右键选中Exlude,就会在pom中自动添加<exclusions>标签排除有冲突的jar包。
排除之后我们的依赖变成了如下内容:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<exclusions>
<exclusion>
<artifactId>slf4j-api</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
<exclusion>
<artifactId>jackson-annotations</artifactId>
<groupId>com.fasterxml.jackson.core</groupId>
</exclusion>
<exclusion>
<artifactId>jackson-databind</artifactId>
<groupId>com.fasterxml.jackson.core</groupId>
</exclusion>
<exclusion>
<artifactId>jackson-databind</artifactId>
<groupId>com.fasterxml.jackson.core</groupId>
</exclusion>
<exclusion>
<artifactId>guava</artifactId>
<groupId>com.google.guava</groupId>
</exclusion>
<exclusion>
<artifactId>jackson-module-afterburner</artifactId>
<groupId>com.fasterxml.jackson.module</groupId>
</exclusion>
<exclusion>
<artifactId>jackson-core</artifactId>
<groupId>com.fasterxml.jackson.core</groupId>
</exclusion>
</exclusions>
</dependency>
2:使用注解,我们先使用@EnableCircuitBreaker。此时算上我们之前Eureka中使用的注解,现在启动类上已经有三个注解了。
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public class ConsumerApplication {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
public static void main (String [] arg){
SpringApplication.run(ConsumerApplication.class);
}
}
而且这三个注解是经常一起使用,所以SpringCloud为了简化开发,定义了一个新的注解:@SpringCloudApplication
点进去看源码,可以看到它就是代替这三个注解而来的。所以我们也可以直接使用它。
package org.springframework.cloud.client;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public @interface SpringCloudApplication {
}
3:增加Hystrix处理逻辑。
为了开启服务降级,我们在需要Hystrix处理的方法上添加注解:
@HystrixCommand(fallbackMethod = "getAllProductFallBack")
其中fallbackMethod 属性值是我们定义的一个方法名,这个方法的返回值用于当调用的服务出现问题时(请求超时,线程池满了等)返回给用户的提示信息,但是要注意这个自定义的方法有个特殊之处,就是要和我们@HystrixCommand注解加的方法的参数列表以及返回值
完全一样,一般都会用String,这样可以直接返回提示信息。
@GetMapping("/getAllProduct")
@HystrixCommand(fallbackMethod = "getAllProductFallBack")
public String getAllProduct(){
//使用Ribbon请求第一种方式: 我们把地址换成服务id即可
String uri="http://EUREKA-SERVICE.PRODUCER/getProducts";
String vos = restTemplate.getForObject(uri, String.class);
return vos;
}
//此方法的参数和返回值必须和对应处理方法一致,一般返回值都使用String,
// 这样提示信息比较友好
public String getAllProductFallBack(){
return "系统繁忙,请稍后重试";
}
4:测试
我们在生产者服务模块设置一个2秒休眠,模仿超时。
我们请求消费者模块地址看到如下:
看到我们的Hystrix配置成功,而且1s就返回了,这是因为默认的Hystrix默认1s就认为服务不通便调用了我们的配置。
上述我们的降级服务是写在方法上的,当方法多的时候是不实用的,我还可以配置在类上@DefaultProperties(defaultFallback = "默认方法名"),这样只要controller类中某个方法加上@HystrixCommand都可以触发服务降级,但是注意,这样写,那个默认的方法就需要是无参的方法.
重启服务访问也是可以的。
(1): 但是默认情况下1s就认为服务需要降级显然不符合实际,所以我们需要修改默认配置。我们可以对controller中不同的方法采取不同的时长,在我们的方法上配置如下内容:
@GetMapping("/getAllProduct")
//@HystrixCommand(fallbackMethod = "getAllProductFallBack")
@HystrixCommand(commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "3000")
})
public String getAllProduct(Long id){
//使用Ribbon请求第一种方式: 我们把地址换成服务id即可
String uri="http://EUREKA-SERVICE.PRODUCER/getProducts";
String vos = restTemplate.getForObject(uri, String.class);
return vos;
}
把超时时长配置成了3s,这样我们在生产者服务中的休眠两秒就不影响结果了,重启后访问如下:
看到两秒之后就返回正常结果了,并没有降级服务。
@HystrixProperty属性可以配置多个,对应的属性可以参考HystrixCommandProperties类。比如我们超时的配置:
使用的地方太长,不好截图,服务代码如下,
this.executionTimeoutInMilliseconds = getProperty(propertyPrefix, key, "execution.isolation.thread.timeoutInMilliseconds", builder.getExecutionIsolationThreadTimeoutInMilliseconds(), default_executionTimeoutInMilliseconds);
(2):我们还可以在配置文件中配置全局的超时时间这个每个方法的时间都是一致的。
首先方法上直接使用:
@GetMapping("/getAllProduct")
//@HystrixCommand(fallbackMethod = "getAllProductFallBack")
@HystrixCommand
public String getAllProduct(Long id){
//使用Ribbon请求第一种方式: 我们把地址换成服务id即可
String uri="http://EUREKA-SERVICE.PRODUCER/getProducts";
String vos = restTemplate.getForObject(uri, String.class);
return vos;
}
在配置application.yml中配置如下:
#Hystrix的超时时间
hystrix:
command:
default:
execution:
isolation :
thread :
timeoutInMilliseconds: 3000
也是可以成功的。
五:熔断器的引入
上面的内容,只用到了服务降级,但是这样的话会有一个问题:
如果调用的服务一直存在问题,我们设置的服务降级时间为2s,那么一次请求总是在2s之后才会进行服务降级处理,在高并发的时候就会很占用资源,因为正常情况下明明就几毫秒的响应时间却要2s后返回结果,这个时候就需要熔断处理了。
熔断器有三个状态:
相关英文描述---阈值:Threshold 熔断器(断路器):circuitBreaker
关闭:熔断器关闭,所有请求能正常访问。
打开:熔断器打开,所有请求都会被降级。默认情况下最近20次请求中如果有50%(默认)都失败了,就达到阈值,熔断器就会 打开。
半开:从熔断器打开开始默认持续5s(休眠时间窗),就会进入半开状态。 会放一部分请求通过,来测试请求是否正常,如果失 败,就会又回到打开状态,再打开5s然后进入半开状态,如果测试正常, 熔断器就会关闭,请求恢复正常处理。如此往复。
六:测试熔断器
1)为了能看到效果,我们要控制一些请求成功和失败的情况,我们在consumerController中添加如下逻辑来控制请求是否成功。
生产提供服务者中的sleep(2)可去掉。
//@HystrixCommand(fallbackMethod = "getAllProductFallBack")
/*@HystrixCommand(commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "3000")
})*/
@GetMapping("/getAllProduct/{id}")
@HystrixCommand
public String getAllProduct(@PathVariable int id){
if (id%2==0){
throw new RuntimeException();
}
//使用Ribbon请求第一种方式: 我们把地址换成服务id即可
String uri="http://EUREKA-SERVICE.PRODUCER/getProducts";
String vos = restTemplate.getForObject(uri, String.class);
return vos;
}
2)我们需要更改默认的阈值和时间窗口。我们还要使用@HystrixProperty中的属性。
我还要去看HystrixCommandProperties类,根据英文的大致意思可以找到阈值,休眠时间窗口,以及失败比。的属性
我们根据属性名找到使用他们的地方就找到对应的key值了。
失败比默认是50%就会开启熔断,这个配置我们可以不用改,不影响我们观察结果,我们配置阈值和休眠时间窗口。
@HystrixCommand(commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "3000"),
@HystrixProperty(name="circuitBreaker.requestVolumeThreshold",value = "10"),
@HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds",value = "10000")
})
@GetMapping("/getAllProduct/{id}")
//@HystrixCommand
public String getAllProduct(@PathVariable int id){
if (id%2==0){
throw new RuntimeException();
}
//使用Ribbon请求第一种方式: 我们把地址换成服务id即可
String uri="http://EUREKA-SERVICE.PRODUCER/getProducts";
String vos = restTemplate.getForObject(uri, String.class);
return vos;
}
然后重启访问,我把测试的结果描述一下:
2:访问如下地址:http://localhost:8012/getAllProduct/1 结果正常返回,而且返回结果很快。先调用此地址无论多少次都是成功的(理论上)。
(2) 访问http://localhost:8012/getAllProduct/2地址,是返回服务降级的。一直按F5(强制刷新请求后台),一直返回这个错。
刷新大概十次之后。马上去访问:http://localhost:8012/getAllProduct/1,看到返回了也是服务降级的错,而且响应时间很快,连续刷新几次还是这样,大概过了10s,再刷新就恢复正常访问了。
可以看出熔断器已经起了作用。
对于生产中的配置,这三个数值(阈值,休眠时长,失败百分比)一般用默认就行了。但是那个请求超时时长一般需要配置,默认1s太短了。
上面的请求过程可以用下图描述: