Hystrix介绍
在微服务系统中,服务之间通常会相互依赖,比如现在有三个服务A、B、C;A依赖B,B依赖C,如果C因为网络波动或其他原因导致不可用,B调用C的时候可能会进入长时间的等待,在高并发环境下可能会导致B的资源迅速耗尽从而导致B崩溃,B崩溃后A也会崩溃,这就形成了雪崩效应,因为一个服务不可用导致大量服务崩溃。Hystrix是一个熔断器,熔断器的作用就是熔断不可用的接口,熔断后调用这个接口会执行接口对应的降级方法,而不是执行接口本身,从而避免在不可用接口上浪费时间。
熔断和降级的区别
接口配置了Hystrix后,如果执行接口超时或出现异常,会执行降级方法,降级方法可以理解为try catch中的catch,接口可以理解为try中的代码。熔断和降级的区别在于,熔断后不会去执行接口,而是直接执行降级方法。熔断前,是先执行接口,如果接口有问题再去执行降级方法。
开始配置
本文的代码是在这篇文章的基础上开发的SpringCloud Ribbon教程
添加相关依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
添加启动类注解
添加@EnableHystrix或者@EnableCircuitBreaker,这两个注解是等价的。
@EnableCircuitBreaker//添加此注解
@EnableEurekaClient
@SpringBootApplication
public class RibbonConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(RibbonConsumerApplication.class, args);
}
//上一篇文章,ribbon的相关配置,不用ribbon可以删
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
添加熔断方法和降级方法
在需要熔断的方法上添加@HystrixCommand(fallbackMethod = "testFailure")注解,fallbackMethod指定降级方法名,降级方法的返回值和参数要跟熔断方法一致。
@HystrixCommand(fallbackMethod = "testFailure")
@GetMapping("/test")
public void test() throws Exception {
System.out.println(111);
throw new Exception("test");
}
//fallbackMethod的值要跟这个方法名一致
public void testFailure(){
System.out.println("降级");
}
调用方法,看一下控制台输出,虽然抛出了异常,但控制台并没有异常输出,而是执行了降级方法
111
降级
111
降级
调用20次后,触发熔断,test()方法没有再被调用,而是直接调用降级方法,触发熔断的条件后面会介绍
降级
降级
可以给降级方法添加降级方法(套娃),其实这里非常像try catch中多catch处理
@HystrixCommand(fallbackMethod = "testFailure")
@GetMapping("/test")
public void test() throws Exception {
System.out.println(111);
throw new Exception("test");
}
//降级方法再添加一个降级方法
@HystrixCommand(fallbackMethod = "testFailure2")
public void testFailure(){
//do something
}
public void testFailure2(){
System.out.println("降级2");
}
降级方法可以添加Throwable参数来接收熔断方法抛出的异常
public void testFailure(Throwable e){
e.printStackTrace();
System.out.println("降级");
}
@HystrixCommand详解
至此,我们已经实现了一个简单的熔断降级功能,不过Hystrix还有很多属性可以配置,先来看看@HystrixCommand的属性
ignoreExceptions忽略异常,当方法抛出指定异常时不执行降级方法,由于忽略了某异常,当方法抛出这个异常时,Hystrix会认为方法是正常的,所以也不会触发熔断。commandKey命令名groupKey组名threadPoolKey线程池名 默认情况下,Hystrix命令通过组名来划分线程池,即组名相同的命令放到同一个线程池里,如果通过threadPoolKey设置了线程池名称,则按照线程池名称划分。
Hystrix缓存
Hystrix缓存只在当前请求有效,个人认为比较鸡肋
添加过滤器
使用Hystrix缓存需要在每次请求时初始化上下文,我们用过滤器来实现比较方便
@Component
@WebFilter(filterName = "hystrixRequestContextServletFilter", urlPatterns = "/*", asyncSupported = true)
public class HystrixRequestContextServletFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HystrixRequestContext context = HystrixRequestContext.initializeContext();
filterChain.doFilter(servletRequest, servletResponse);
context.close();
}
@Override
public void destroy() {
}
}
添加@CacheResult注解
- 注意缓存必须和
@HystrixCommand一起用,否则不生效 - 缓存key默认是方法的全部参数,如果方法是无参方法,缓存也不生效
- 因为缓存只在当前请求有效,所以一般把缓存方法写在Service层,然后Controller多次调用就可以利用缓存了
@CacheResult
@HystrixCommand(fallbackMethod = "testFailure")
public String test(Integer id){
System.out.println("test()");
return restTemplate.getForObject("http://Server-Provider/getUser/id={id}", String.class, id);
}
设置缓存key
参数上添加@CacheKey注解,下面的代码用id作为缓存key,age不影响缓存结果
@CacheResult
@HystrixCommand(commandKey = "getUser",fallbackMethod = "testFailure")
public Integer test(@CacheKey Integer id,Integer age){
System.out.println("test()");
return id;
}
@CacheKey如果有值,表示取对象某个属性作为key,下面的代码取User对象的id属性作为key
@CacheResult
@HystrixCommand(commandKey = "getUser",fallbackMethod = "testFailure")
public Integer test(@CacheKey("id") User user){
System.out.println("test()");
return id;
}
通过方法返回缓存key,设置@CacheResult的cacheKeyMethod属性指定方法名,这种方法的优先级高于@CacheKey
public String getUserCacheKey(User user){
return user.getId()+","+user.getName();
}
@CacheResult(cacheKeyMethod = "getUserCacheKey")
@HystrixCommand(fallbackMethod = "testFailure")
public Integer test(@CacheKey("id") User user){
System.out.println("test()");
return id;
}
清除缓存
添加@CacheRemove注解,需要设置commandKey属性指定熔断方法,以及指定缓存key
@CacheRemove(commandKey = "getUser")
@HystrixCommand
public String updateUser(@CacheKey("id") User user){
return "";
}
请求合并
请求合并就是将短时间内的多个请求合并为一个请求,节省请求时间,降低服务提供者负载。
Service代码
- 添加
@HystrixCollapser注解,设置batchMethod指定合并方法 - 添加
collapserProperties = { @HystrixProperty(name = "timerDelayInMilliseconds", value = "100") }属性,指定合并时间。 注意: @HystrixCommand和@HystrixCollapser一起用的时候,会报这个错误
method cannot be annotated with HystrixCommand and HystrixCollapser annotations at the same time
- batchMethod方法必须用
@HystrixCommand注解 - 因为getUser方法实际上不会被执行,所以不能用
@HystrixCommand注解也无所谓,我们可以在batchMethod方法上做熔断降级 下面的代码合并时间是100毫秒,也就是说,100毫秒内的多个请求会合并成一个请求。
//此方法实际上不会被执行,调用此方法,实际上执行的是findUserBatch方法
@HystrixCollapser(batchMethod = "findUserBatch", collapserProperties = {
@HystrixProperty(name = "timerDelayInMilliseconds", value = "100")
})
public Future<String> getUser(Integer id) throws InterruptedException {
//方法不会被执行,所以直接返回null
return null;
}
@HystrixCommand
public List<String> findUserBatch(List<Integer> ids) {
System.out.println("合并请求:"+ids.size());
return Arrays.asList(restTemplate.getForObject("http://provider/getUsers?ids={1}",String[].class, StringUtils.join(ids,",")));
}
Controller代码
@GetMapping("/getUser")
public String getUser(Integer id,Integer a) throws InterruptedException, ExecutionException {
//注意这里不要直接调用Future的get方法,因为get方法是阻塞直到返回结果,这样就无法测试合并效果
Future<String> f1=service.getUser(id);
Future<String> f2=service.getUser(id);
//可以在这里get,f1.get();f2.get();
Thread.sleep(200);
return service.getUser(id).get();
}
效果
控制台输出
合并请求:2
合并请求:1
第一次输出对应f1和f2,第二次输出对应return service.getUser(id).get();,可以看到,不管有没有合并,都是执行batchMethod方法
Hystrix的其他属性
Hystrix属性的两种配置方法
- 配置文件;例如:全局配置
hystrix.command.default.execution.isolation.strategy,实例配置hystrix.command.getUser.execution.isolation.strategy,实例配置只需将default替换成@HystrixCommand注解中的commandKey属性值即可 - 注解;通过
@HystrixProperty配置
@HystrixCommand(fallbackMethod = "testFailure",commandProperties = {
@HystrixProperty(name="execution.isolation.strategy", value="THREAD")
})
属性列表
execution.isolation.strategy: 隔离策略,它有以下两个选项。- THREAD:通过线程池隔离的策略。它在独立的线程上执行,并且它的并发限制受线程池中线程数量的限制。
- SEMAPHORE:通过信号量隔离的策略。它在调用线程上执行,并且它的并发限制受信号量计数的限制。
execution.isolation.thread.timeoutinMilliseconds,超时时间,单位为毫秒,默认1000毫秒。当HystrixCommand执行时间超过该配置值之后, Hystrix会将该执行命令标记为TIMEOUT并进入服务降级处理逻辑。execution.timeout.enabled,是否启用超时时间。默认为true, 如果设置为false, 那么execution.isolation.thread.timeoutinMilliseconds属性的配置将不再起作用。execution.isolation.thread.interruptOnTimeout,执行超时的时候是否要将它中断,默认true。execution.isolation.thread.interruptOnCancel,执行被取消的时候是否要将它中断,默认true。execution.isolation.semaphore.maxConcurrentRequests,当隔离策略是信号量时,该属性用来配置信号量的大小(并发请求数),默认10。当最大并发请求数达到该设置值时,后续的请求将会被拒绝。fallback.enabled,服务降级策略是否启用,默认true,如果设置为false,那么当请求失败或者拒绝发生时,将不会调用降级服务circuitBreaker.enabled当服务请求命令失败时,是否使用断路器来跟踪其健康指标和熔断请求,默认true。circuitBreaker.requestVolumeThreshold设置在滚动时间窗中,断路器熔断的最小请求数。例如,默认该值为20的时候,如果滚动时间窗(默认10秒)内仅收到了19个请求, 即使这19个请求都失败了,断路器也不会打开。circuitBreaker.sleepWindowinMilliseconds设置当断路器打开之后的休眠时间窗。休眠时间窗结束之后,会将断路器置为“半开” 状态,尝试熔断的请求命令,如果依然失败就将断路器继续设置为“打开” 状态,如果成功就设置为“关闭” 状态,默认5000毫秒。circuitBreaker.errorThresholdPercentage设置断路器打开的错误百分比条件。例如,默认值为50的情况下,表示在滚动时间窗中,在请求数量超过circuitBreaker.requestVolumeThreshold阅值的前提下,如果错误请求数的百分比超过50, 就把断路器设置为“打开” 状态, 否则就设置为“关闭” 状态。circuitBreaker.forceOpen如果将该属性设置为true, 断路器将强制进入“打开” 状态,它会拒绝所有请求。该属性优先于circuitBreaker.forceClosed属性。circuitBreaker.forceClosed如果将该属性设置为true, 断路器将强制进入“关闭” 状态, 它会接收所有请求。如果circuitBreaker.forceOpen属性为true, 该属性不会生效。metrics.rollingStats.timeinMilliseconds设置滚动时间窗的长度, 单位为毫秒。该时间用于断路器判断健康度时需要收集信息的持续时间。断路器在收集指标信息的时候会根据设置的时间窗长度拆分成多个“桶” 来累计各度量值,每个“桶” 记录了一段时间内的采集指标。例如,当采用默认值10000毫秒时, 断路器默认将其拆分成10个桶(桶的数量也可通过metrics.rollingStats.numBuckets参数设置),每个桶记录1000毫秒内的指标信息。metrics.rollingstats.numBuckets设置滚动时间窗统计指标信息时划分“桶” 的数量。metrics.rollingPercentile.enabled设置对命令执行的延迟是否使用百分位数来跟踪和计算,默认true。如果设置为false,那么所有的概要统计都将返回-1。metrics.rollingPercentile.timeinMilliseconds设置百分位统计的滚动窗口的持续时间,单位为毫秒,默认60000。metrics.rollingPercentile.numBuckets设置百分位统计滚动窗口中使用“桶”的数量,默认6。metrics.rollingPercentile.bucketSize设置在执行过程中每个“桶” 中保留的最大执行次数,默认100。如果在滚动时间窗内发生超过该设定值的执行次数,就从最初的位置开始重写。例如,将该值设置为100, 滚动窗口为10秒,若在10秒内一个“桶”中发生了500次执行,那么该“桶”中只保留最后的100次执行的统计。另外,增加该值的大小将会增加内存量的消耗,并增加排序百分位数所需的计算时间。metrics.healthSnapshot.intervalinMilliseconds设置采集影响断路器状态的健康快照(请求的成功、错误百分比)的间隔等待时间,默认500。requestCache.enabled设置是否开启请求缓存,默认true。maxRequestsinBatch设置一次请求合并批处理中允许的最大请求数,默认Integer.MAX_VALUE。timerDelayinMilliseconds该参数用来设置批处理过程中每个命令延迟的时间,单位为毫秒,默认10。request Cache.enabled该参数用来设置批处理过程中是否开启请求缓存,默认true。coreSize设置执行命令线程池的核心线程数,该值也就是命令执行的最大并发量,默认10。maxQueueSize设置线程池的最大队列大小,,默认-1。当设置为-1时,线程池将使用SynchronousQueue实现的队列,否则将使用LinkedBlockingQueue实现的队列。queueSizeRejectionThreshold设置队列拒绝阈值,默认5。通过该参数,即使队列没有达到最大值也能拒绝请求。该参数主要是对LinkedBlockingQueue队列的补充, 因为LinkedBlockingQueue队列不能动态修改它的对象大小,而通过该属性就可以调整拒绝请求的队列大小了。