一.Hystrix概叙
Hystrix作为springcloud微服务里面的一个很重要的组件,它的作用就是资源隔离,服务熔断和服务降级,
资源隔离是因为Hystrix能设置信号量或者线程池里面核心线程,最大线程以及任务队列的原因。服务熔断和服务降级是
Hystrix能设置熔断器的开关和熔断器打开关闭的条件来进行控制,当熔断器是关闭的状态或者调用的服务出现异常,Hystrix可以调用备用逻辑来进行处理。避免程序直接出错。
服务熔断我个人的理解是熔断器已经开启,不调用远程服务了,直接调用FallBack的逻辑
服务降级是指在调用远程服务因为调用异常或者服务超时来调用
FallBack的逻辑
二.Hystrix的调用流程
1.在启动类上加@EnbaleHystrix注解,当启动工程的时候
@EnbaleHystrix注解中的@Import注解会发挥作用,读取spring.factories,将Hystrix对应的配置文件注入到容器中,并也将配置文件中的相关bean注入到容器中,其中就有切面HystrixCommandAspect。
2.当我们调用有注解@HystrixCommand或者@HystrixCollapser修饰的方法的时候,就会进入到切面中的环绕通知方法中。
3进入到环绕通知方法后,先获取切入点的信息和@HysrtixCommond中的信息,并进行组织,然后生成HystrixCommand对象并执行,在
HystrixCommand中有通过工厂创建的线程池和熔断器,如果是同一组请求,就用同一个线程池和熔断器进行处理。在用线程池执行任务的时候,会判断线程池队列是否已经满了,如果满了会抛出
Rejected command because thread-pool queueSize is at rejection threshold。
4.执行
HystrixCommand后,先判断是否开启了缓存,要是有缓存,就先从缓存里面取;如果缓存没有开启或者缓存里面没有取到,然后判断是否开启熔断,如果是强制熔断开启,则直接调用FallBack的逻辑,如果熔断器开启,并且还是在滑动窗口时间之内的,则返回调用
FallBack的逻辑,如果熔断器关闭或者当前时间超过了熔断器开启时间加上窗口滑动时间,则判断信号量是否够用,如果够用就执行命令,不用就执行
FallBack的逻辑。
执行命令的话,会将命令放入到线程池中,再由线程池执行,执行过程运用反射的原理
5.最后统计器对执行情况进行统计。
构造一个 HystrixCommand或HystrixObservableCommand对象,用于封装请求,并在构造方法配置请求被执行需要的参数;
执行命令,Hystrix提供了4种执行命令的方法,后面详述;
判断是否使用缓存响应请求,若启用了缓存,且缓存可用,直接使用缓存响应请求。Hystrix支持请求缓存,但需要用户自定义启动;
判断熔断器是否打开,如果打开,跳到第8步;
判断线程池/队列/信号量是否已满,已满则跳到第8步;
执行HystrixObservableCommand.construct()或HystrixCommand.run(),如果执行失败或者超时,跳到第8步;否则,跳到第9步;
统计熔断器监控指标;
走Fallback备用逻辑
三.Hystrix启动流程
如果用注解的话,Hystrix是利用AOP的原理来让Hystrix发挥作用的。
通过
SpringFactoriesLoader读取hystrix对应的spring.factories配置文件
可以看到HystrixCommandAspect作为一个切面类,当有被注解@HystrixCommand和@HystrixCollapser修饰的方法时,这些方法就是切入点,当调用这些方法的时候,就会调用通知方法methodsAnnotatedWithHystrixCommand,其中ProceedingJoinPoint中会包含很多方法运行时的信息,这些信息先被注入到MetaHolder对象中,然后利用这些信息创建一个命令对象,最后就是执行命令。
其中
methodsAnnotatedWithHystrixCommand方法中主要是做了两件事,一件就是根据切入点信息和注解信息构建HystrixCommand信息。第二件就是执行
HystrixCommand。
总结一下这个启动过程:
@EnableHystrix里面包含了@EnableCircuidBreak注解,@EnableCircuidBreaker注解里面会引用@Import注解,通过@Import注解读取spring.factories文件,会将配置HystrixCircuitBreakerConfiguration文件注入到ioc容器中,并将在配置文件中声明的HystrixCommondAspect切面注入到容器中,HystrixCommondAspect切面会让@HystrixCommond或者@HystrixCollapser生效。
四.HystrixComand的构建
先看是怎么组织切入点的信息
可以看出创建命令就是MetaHolder中的信息赋值给了GenericObservableCommand对象,这里面的内容包括@HystrixCommond设置的属性,也包括方法运行的信息,例如入参,方法名,类名等信息
当构造GenericCommand的时候,会调用父类的构造方法,最终定位到AbstractCommand中
下面就是创建线程池的过程
创建线程池,可以看出拒绝策略的话是默认的直接抛出异常。
拒绝策略的创建方式和线程池的创建方式是一样的。
总结一下HystrixCommand的创建过程:在切面中获取切入点的运行信息,注解上的配置信息赋值给命令,也会设置线程池和熔断器,如果线程池通过线程池key(线程池key其实是group key)从线程池工厂的concurrentHashMap中获取线程池,要是获取不到,就创建新的线程池,每一组HystrixCommand请求都是由同一个线程池对应。
五.执行HystrixCommand
熔断器
class HystrixCircuitBreakerImpl implements HystrixCircuitBreaker {
private final HystrixCommandProperties properties;
private final HystrixCommandMetrics metrics;
enum Status {
CLOSED, OPEN, HALF_OPEN;//熔断器三种状态:关闭,打开,半开
}
//熔断器的状态属性
private final AtomicReference<Status> status = new AtomicReference<Status>(Status.CLOSED);
//熔断器打开的时间戳,如果它的值为-1,说明熔断器是关闭的状态,如果不是-1,说明是打开或者半开的状态
private final AtomicLong circuitOpened = new AtomicLong(-1);
//熔断器的订阅函数
private final AtomicReference<Subscription> activeSubscription = new AtomicReference<Subscription>(null);
protected HystrixCircuitBreakerImpl(HystrixCommandKey key, HystrixCommandGroupKey commandGroup, final HystrixCommandProperties properties, HystrixCommandMetrics metrics) {
this.properties = properties;
this.metrics = metrics;
//On a timer, this will set the circuit between OPEN/CLOSED as command executions occur
Subscription s = subscribeToStream();
activeSubscription.set(s);
}
private Subscription subscribeToStream() {
/*
* This stream will recalculate the OPEN/CLOSED status on every onNext from the health stream
*/
return metrics.getHealthCountsStream()
.observe()
.subscribe(new Subscriber<HealthCounts>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
}
//当请求的次数大于20次(默认),或者请求的错误率大于50%(默认),就会将熔断器的状态从关闭改成打开
//并设置熔断器开启时间戳
@Override
public void onNext(HealthCounts hc) {
// check if we are past the statisticalWindowVolumeThreshold
if (hc.getTotalRequests() < properties.circuitBreakerRequestVolumeThreshold().get()) {
// we are not past the minimum volume threshold for the stat window,
// so no change to circuit status.
// if it was CLOSED, it stays CLOSED
// if it was half-open, we need to wait for a successful command execution
// if it was open, we need to wait for sleep window to elapse
} else {
if (hc.getErrorPercentage() < properties.circuitBreakerErrorThresholdPercentage().get()) {
//we are not past the minimum error threshold for the stat window,
// so no change to circuit status.
// if it was CLOSED, it stays CLOSED
// if it was half-open, we need to wait for a successful command execution
// if it was open, we need to wait for sleep window to elapse
} else {
// our failure rate is too high, we need to set the state to OPEN
if (status.compareAndSet(Status.CLOSED, Status.OPEN)) {
circuitOpened.set(System.currentTimeMillis());
}
}
}
}
});
}
//当调用成功后,会将熔断器的状态从半开改成关闭,然后取消订阅,将熔断器开启时间戳设置成-1
@Override
public void markSuccess() {
if (status.compareAndSet(Status.HALF_OPEN, Status.CLOSED)) {
//This thread wins the race to close the circuit - it resets the stream to start it over from 0
metrics.resetStream();
Subscription previousSubscription = activeSubscription.get();
if (previousSubscription != null) {
previousSubscription.unsubscribe();
}
Subscription newSubscription = subscribeToStream();
activeSubscription.set(newSubscription);
circuitOpened.set(-1L);
}
}
//调用失败后,将状态从半开改成全开,并设置熔断器开启时间戳
@Override
public void markNonSuccess() {
if (status.compareAndSet(Status.HALF_OPEN, Status.OPEN)) {
//This thread wins the race to re-open the circuit - it resets the start time for the sleep window
circuitOpened.set(System.currentTimeMillis());
}
}
@Override
public boolean isOpen() {
if (properties.circuitBreakerForceOpen().get()) {
return true;
}
if (properties.circuitBreakerForceClosed().get()) {
return false;
}
return circuitOpened.get() >= 0;
}
@Override
public boolean allowRequest() {
if (properties.circuitBreakerForceOpen().get()) {
return false;
}
if (properties.circuitBreakerForceClosed().get()) {
return true;
}
if (circuitOpened.get() == -1) {
return true;
} else {
if (status.get().equals(Status.HALF_OPEN)) {
return false;
} else {
return isAfterSleepWindow();
}
}
}
private boolean isAfterSleepWindow() {
final long circuitOpenTime = circuitOpened.get();
final long currentTime = System.currentTimeMillis();
final long sleepWindowTime = properties.circuitBreakerSleepWindowInMilliseconds().get();
return currentTime > circuitOpenTime + sleepWindowTime;
}
@Override
public boolean attemptExecution() {
if (properties.circuitBreakerForceOpen().get()) {
return false;
}
if (properties.circuitBreakerForceClosed().get()) {
return true;
}
if (circuitOpened.get() == -1) {
return true;
} else {
if (isAfterSleepWindow()) {
if (status.compareAndSet(Status.OPEN, Status.HALF_OPEN)) {
//only the first request after sleep window should execute
return true;
} else {
return false;
}
} else {
return false;
}
}
}
}
由此可以知道Hystrix的定时判断是通过创建一个抛出超时异常的任务,并将这个任务添加到定时任务线程池中执行来判断的
在HystrixCommond中,run方法是一个抽象方法,在GenericCommand中重写了run方法
到此可以看出Hystrix在进行一些列的判断后,执行到被@HystrixCommond注解修饰的方法时候,使用反射来执行的。
再来看Hystrix是如何运用线程池来执行
命令的
写的比较好的一个文章