【深入理解SpringCloud微服务】Hystrix作用与原理剖析
前面三篇文章,我们已经了解了微服务的熔断、限流、降级,并且也手写了一个熔断限流框架。从本篇文章开始,我们了解一下相关的开源框架Hystrix与Sentinel,本篇文章先解析Hystrix,后面的文章解析Sentinel。
Hystrix其实已经算是一个过期了的技术,说实话不学也没什么问题,毕竟现在都有Sentinel了,但是我们简单了解一下,有助于与Sentinel作对比。
Hystrix的作用
Hystrix是Netflix开发的用于服务容错保护的开源框架,然后Spring Cloud Netflix在此基础上做了些包装,添加了自动配置的功能,使得Hystrix的配置和使用变得更加简单。Hystrix提供了熔断、降级和隔离等功能。
熔断
熔断的作用在之前的文章已经介绍过了,它类似于电路中的保险丝。
熔断的功能由断路器实现,在Hystrix中断路器是HystrixCircuitBreaker。当断路器处于闭合状态时,请求可以被正常处理;如果断路器处于打开状态,请求将不会被正常处理,而是被降级处理或抛出异常(如果没有设置降级逻辑的话)。
除此以外,断路器还有一个半开状态。半开状态是指当断路器打开了一段时间后,会允许一个请求通过,测试一下服务是否恢复正常。如果服务恢复正常,那么断路器状态切换为闭合状态;否则再次切换为打开状态。
Hystrix中断路器的状态切换如下图:
降级
降级是指当断路器处于打开状态,或者接口的调用本身出现异常或超时时,转而执行的备用逻辑。Hystrix的降级处理也是如此。
只要我们的方法被 @HystrixCommand 注解修改,并在注解的 fallbackMethod 属性设置降级处理方法的方法名,当断路器打开、接口执行超时或失败等情况发生时,Hystrix就会执行我们指定的那个降级处理方法。
隔离
隔离是指不同接口间的资源隔离,这个被隔离的资源一般是线程资源。由于每个接口的请求都需要当前服务开辟一个线程去处理,如果接口间没有资源隔离的话,一旦某个接口由于某种原因导致请求处理长时间无响应,该接口就有可能吃光当前服务的所有线程资源(特别是接口并发量比较高时),进而影响到别的接口也无法处理请求,那么整个服务就瘫痪了。
解决这种问题的办法就是实现资源隔离。资源隔离使用的是“仓壁模式”,就像Docker那样,Docker通过舱壁模式,实现进程间的资源隔离,把每个进程包裹在一个容器里面,并为他配置一定的资源上限,使得进程与进程之间不会互相影响。
Hystrix则是通过线程池(默认)进行资源隔离。Hystrix为每一个受保护的接口或方法都配备一个独立的线程池,并限定池内的线程数。如此一来,即使一个接口有问题,它也只能耗光自己线程池内的所有线程资源,其他接口不会受影响。
当然,这种为每一个受保护的接口或方法单独分配一个线程池的方式,会给系统带来一定的负载和开销,但是这种消耗是微乎其微的。如果接口的延迟本身非常小,相对而言这部分的开销有点大,那么可以改用信号量隔离的方式,信号量默认值是10。
线程池隔离与信号量隔离有一个不同点在于,使用线程池隔离时,请求处理是在线程中异步执行的,而信号量隔离则是在当前线程。
Hystrix有限流的功能吗?
Hystrix没有直接提供限流的功能,但是它通过线程池隔离或信号量隔离的方式间接实现了限流。
比如我们使用线程池隔离,每个为某个接口或方法而独立配备的线程池都有一个线程数的上限,一旦达到了这个线程数上限,请求就会被拒绝,这就达到了限流的效果。
比如现在有一个接口,我们设置了该接口的线程池最大线程数是3,那么该接口最多只能同时处理3个请求。
如果此时有第4个请求到达,那么该请求将会被拒绝处理,如此也就达到了限流的效果。
通过信号量隔离的方式,也可以实现限流,只要设置信号量的最大数量即可。
Hystrix的原理
@HystrixCommand注解是如何起作用的
Hystrix的使用就是在接口或方法上声明@HystrixCommand(当然还有引入maven依赖,在启动类上添加注解等前置工作),那这个@HystrixCommand是如何起作用的呢?
除了在接口上添加@HystrixCommand注解外,我们还有在启动类上添加 @EnableHystrix 或 @EnableCircuitBreaker注解,实际上@EnableHystrix里面就是@EnableCircuitBreaker注解。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@EnableCircuitBreaker
public @interface EnableHystrix {
}
那么这个@EnableCircuitBreaker注解有何作用呢?
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({EnableCircuitBreakerImportSelector.class})
public @interface EnableCircuitBreaker {
}
@EnableCircuitBreaker注解通过@Import注解导入了一个EnableCircuitBreakerImportSelector。
public class EnableCircuitBreakerImportSelector extends SpringFactoryImportSelector<EnableCircuitBreaker> {
...
}
然后EnableCircuitBreakerImportSelector又继承了Spring的SpringFactoryImportSelector,并且泛型指定为EnableCircuitBreaker。熟悉SpringBoot自动装配的都知道,SpringBoot启动时会去扫描所有的spring.factories文件并加载里面key的类型匹配的类。
因此我们看一下spring-cloud-netflix-hystrix工程的spring.factories文件
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.hystrix.HystrixAutoConfiguration,\
org.springframework.cloud.netflix.hystrix.security.HystrixSecurityAutoConfiguration
org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker=\
org.springframework.cloud.netflix.hystrix.HystrixCircuitBreakerConfiguration
EnableCircuitBreaker对应的类是HystrixCircuitBreakerConfiguration,于是HystrixCircuitBreakerConfiguration就会被Spring加载进来。
@Configuration
public class HystrixCircuitBreakerConfiguration {
@Bean
public HystrixCommandAspect hystrixCommandAspect() {
return new HystrixCommandAspect();
}
...
}
然后HystrixCircuitBreakerConfiguration通过@Bean引入了一个HystrixCommandAspect。
@Aspect
public class HystrixCommandAspect {
private static final Map<HystrixCommandAspect.HystrixPointcutType, HystrixCommandAspect.MetaHolderFactory> META_HOLDER_FACTORY_MAP;
public HystrixCommandAspect() {
}
@Pointcut("@annotation(com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand)")
public void hystrixCommandAnnotationPointcut() {
}
...
@Around("hystrixCommandAnnotationPointcut() || hystrixCollapserAnnotationPointcut()")
public Object methodsAnnotatedWithHystrixCommand(ProceedingJoinPoint joinPoint) throws Throwable {
...
}
...
}
然后我们可以看到HystrixCommandAspect被@Aspect注解,因此HystrixCommandAspect是一个切面类,而HystrixCommandAspect通过 @Pointcut(“@annotation(com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand)”) 注解指定扫描被@HystrixCommand注解修饰的接口或方法进行代理增强。
然后通过 @Around 注解声明了环绕增加的处理逻辑是methodsAnnotatedWithHystrixCommand(ProceedingJoinPoint) 方法。
于是当我们调用被@HystrixCommand注解修饰的方法时,实际上调用的是代理对象,然后就会进入到methodsAnnotatedWithHystrixCommand(ProceedingJoinPoint)这个增强处理方法当中,而Hystrix的功能就在这里面实现。
工作流程
下面我们来了解一下,当一个请求被Hystrix的增强逻辑拦截到时,整个工程流程是怎么样的。
1、构建命令对象
Hystrix内部使用了命令模式,Hystrix接收到请求时,首先构建一个命令对象。命令对象的类型是HystrixCommand或HystrixObservableCommand。
如果我们的接口返回一个结果,那么构建的命令就是HystrixCommand;如果我们的结果返回多个结果,构建的命令就是HystrixObservableCommand。
这里说的多个结果不是指数组或List这种类型,而是Observable,我们一般不会返回这种东西,因此就不多研究了,我们只要知道,通过Observable,接口就可以给调用者返回多个结果。
2、执行命令
构建了命令对象之后,就要去执行该命令。根据命令类型的不同会调用不同方法。
如果是HystrixCommand,则调用HystrixCommand的execute()方法或queue()方法。execute()是用于执行同步请求的,而queue()本来是用于异步请求的,然后返回一个Future对象。
如果我们的接口或方法返回的类型是Future,那么这里就会调用HystrixCommand的queue()方法,否则就会调用execute()方法。
但实际上execute()方法内部也是调用了queue()方法,只是在queue()方法返回Future之后,立刻调用Future的get()方法进行异步转同步。
而queue()方法内部则是调用了HystrixObservableCommand的toObservable()方法返回一个Observable对象,然后立刻调用Observable的toBlocking()方法把该Observable转换成BlockingObservable对象,BlockingObservable与Observable的区别是当订阅BlockingObservable时当前线程会被阻塞。然后得到BlockingObservable后立刻调用toFuture()方法转成Future对象。
因此命令的执行最终都会走到HystrixObservableCommand的toObservable()方法。
3、检查缓存是否开启并且是否命中
Hystrix是可以开启缓存功能的,如果我们配置了该接口开启缓存,并且缓存命中,就会从缓存中返回Observable对象。
4、检查断路器是否打开
如果缓存没有开启,或者开启了但是没有命中。那么就检查断路器是否打开,如果打开了,那么执行降级逻辑;否则进行下一步处理。
5、检查线程池或信号量是否已满
如果断路器时闭合状态或半开状态,那么就会检查线程池或信号量是否已经满了,如果满了,那也执行降级逻辑;否则进行下一步处理。
6、执行目标方法
然后就是调用HystrixCommand的run()方法或HystrixObservableCommand的construct()方法,这两个方法就会执行到我们的目标接口或方法。
如果我们的接口执行发生异常或超时,就会执行降级逻辑;否则就会返回执行的结果。
7、报告断路器
方法执行成功或失败(包括线程池或信号量已满)等信息报告给断路器,断路器要根据这些信息进行统计,计算QPS和失败率是否达到一定的阈值,如果是的话那么就要把断路器打开了。
8、降级处理
如果线程池或信号量已满、目标方法执行失败或异常等情况发生,Hystrix就会执行降级方法。
降级方法如果执行成功,那么以Observable的形式返回降级处理的结果,订阅该Observable将会获取到降级方法返回的结果;如果降级方法执行出现了异常,也是返回一个Observable,但是该Observable不会有结果,而是会调用订阅者onError()方法通知订阅者终止请求。
由于我们的方法一般都是同步处理并返回单一结果,因此Observable会被转成Future,然后调用Future的get()方法异步转同步,这里调用Future的get()方法就会抛出异常。
9、返回成功的结果
如果目标方法正常执行并且没有超时,那么就会返回正常的处理结果。
其实这里并不是直接返回目标方法执行的结果,而是返回一个Observable。但是由于我们的方法一般都是同步处理且返回单一结果,因此当返回Observable之后,就会被转成Future然后调用get()方法获取到目标方法处理的结果,因此返回给我们的就是目标方法返回的结果。
断路器原理
断路器的接口是HystrixCircuitBreaker,它有三个方法:
public interface HystrixCircuitBreaker {
boolean allowRequest();
boolean isOpen();
void markSuccess();
...
}
这三个方法的作用如下:
HystrixCircuitBreakerImpl是HystrixCircuitBreaker的实现类。HystrixCircuitBreakerImpl的 allowRequest() 方法首先会调用 this.isOpen() 方法判断断路器是否打开,如果是闭合状态,那么请求能正常通行;如果是打开状态,那么调用 this.allowSingleTest() 判断是否是半开状态,如果是半开状态,那么也允许当前请求通行;否则当前请求就不能走正常的逻辑了。
public boolean allowRequest() {
...
return !this.isOpen() || this.allowSingleTest();
}
HystrixCircuitBreakerImpl的isOpen() 方法会判断HystrixCircuitBreakerImpl的断路器开关状态AtomicBoolean circuitOpen是否是true(打开),如果是那么返回true表示断路器打开;否则根据断路器根据度量指标进行统计计算,判断QPS是否达到阈值(默认20)并且错误率是否达到阈值(默认50%),如果是则切换断路器状态为打开(circuitOpen修改为true)并记录打开时的时间戳,否则返回false。
HystrixCircuitBreakerImpl的allowSingleTest()方法则是根据记录的最后一次打开时的时间戳判断断路器打开是否已经超过预设的时间。如果是,那么就允许这一次请求通行,这就是所谓的半开状态。
最后,还有一个静态内部工厂类 HystrixCircuitBreaker.Factory,用于创建断路器HystrixCircuitBreaker,并通过一个 ConcurrentHashMap<String, HystrixCircuitBreaker> 记录每个被@HystrixCommand注解修饰的接口与对应的断路器的映射关系。
HystrixCircuitBreaker.Factory的getInstance(key)方法用于创建HystrixCircuitBreaker,如果ConcurrentHashMap中已有对应的HystrixCircuitBreaker创建好,那么返回它;否则创建一个并缓存到ConcurrentHashMap中,然后返回。
标签:Hystrix,SpringCloud,接口,剖析,断路器,线程,方法,HystrixCommand From: https://blog.csdn.net/weixin_43889578/article/details/137409692