文章目录
1. HystrixCircuitBreaker
Hystrix的熔断器实现在HystrixCircuitBreake中,此接口比较直观明了。整个HystrixCircuitBreaker接口一共有五个方法和三个静态类:
接口中的类:
- Factory: 维护了一个Hystrix命令和HystrixCircuitBreaker的关系的集合ConcurrentHashMap<String, HystrixCircuitBreaker> circuitBreakersByCommand。其中key通过HystrixCommandKey来定义,每一个Hystrix 命令都需要有一个Key来标识,同时根据这个Key可以找到对应的断路器实例。
- NoOpCircuitBreaker: 一个啥都不做的断路器,它允许所有请求通过,并且断路器始终处于闭合状态
- HystrixCircuitBreakerImpl:断路器的一个实现类。
接口中的方法
- allowRequest()方法表示是否允许指令执行,该方法在新版本中放弃
- isOpen()方法表示断路器是否为开启状态
- markSuccess()处于半开状态时,如果尝试请求成功,就调用这个方法。用于将断路器关闭
- markNonSuccess()处于半开状态时,如果尝试请求成功。 用来打开断路器
- attemptExecution()判断每个Hystrix命令的请求都通过它是否被执行。这是非幂等的,它可能会修改内部。旧版本中是allowRequest()。
2. 断路器Factory
Factory静态类相当于Circuit Breaker Factory,用于获取相应的HystrixCircuitBreaker。我们来看一下其实现:
class Factory {
private static ConcurrentHashMap<String, HystrixCircuitBreaker> circuitBreakersByCommand = new ConcurrentHashMap<String, HystrixCircuitBreaker>();
public static HystrixCircuitBreaker getInstance(HystrixCommandKey key, HystrixCommandGroupKey group, HystrixCommandProperties properties, HystrixCommandMetrics metrics) {
// this should find it for all but the first time
HystrixCircuitBreaker previouslyCached = circuitBreakersByCommand.get(key.name());
if (previouslyCached != null) {
return previouslyCached;
}
HystrixCircuitBreaker cbForCommand = circuitBreakersByCommand.putIfAbsent(key.name(), new HystrixCircuitBreakerImpl(key, group, properties, metrics));
if (cbForCommand == null) {
return circuitBreakersByCommand.get(key.name());
} else {
return cbForCommand;
}
}
public static HystrixCircuitBreaker getInstance(HystrixCommandKey key) {
return circuitBreakersByCommand.get(key.name());
}
static void reset() {
circuitBreakersByCommand.clear();
}
}
简单的说就是Hystrix为每个commandKey都维护了一个熔断器,保持着对应的熔断器,所以当new XXXHystrixCommand()的时候依然能够保持着原来熔断器的状态。
正如前面分析,Hystrix在Factory类中维护了一个ConcurrentHashMap用于存储与每一个HystrixCommandKey相对应的HystrixCircuitBreaker。每当我们通过getInstance方法从中获取HystrixCircuitBreaker的时候,Hystrix首先会检查ConcurrentHashMap中有没有对应的缓存的断路器,如果有的话直接返回。如果没有的话就会新创建一个HystrixCircuitBreaker实例,将其添加到缓存中并且返回。关于断路器的实例化细节后面分析。
至于断路器的初始化时机,在Hystrix执行原理分析中提到过,在Hystrix实例化的时候,AbstractHystrix的构造函数中会调用断路器的Factory的上面的getInstance方法,一切都是非常合理自然~,下面看断路器的实现类。
3. 实现类HystrixCircuitBreakerImpl
先看实现类的代码:
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);
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);
}
}
在该实现类中定义了断路器的五个核心属性:
- HystrixCommandProperties properties:断路器对应实例的属性集合对象
- HystrixCommandMetrics metrics:断路器最核心的东西,通过时间窗的形式,记录一段时间范围内(默认是10秒)的接口请求的健康状况(Command的执行状态,包括成功、失败、超时、线程池拒绝等)并得到对应的度量指标。
- AtomicReference status: 用来记录断路器的状态,默认是关闭状态
- 断路器在状态变化时,使用了AtomicReference#compareAndSet来确保当条件满足时,只有一个请求能成功改变状态。
- AtomicLong circuitOpened:断路器打开的时间戳,用于判断休眠期的结束时间。默认-1,表示断路器未打开。
- AtomicReference activeSubscription: 这个是通过Rxjava实现的对HystrixCommandMetrics结果的观察者对象,当HystrixCommandMetrics值发生变化时会通知观察者。
HystrixCommandProperties properties
该属性保存了断路器对应Command的所有属性。和线程池类似,HystrixCommandProperties也是保存到HashMap中,key为Command的key。下面主要介绍远断路器相关的属性。
- circuitBreakerEnabled:熔断器是否启用,默认是true
- circuitBreakerErrorThresholdPercentage 错误率阈值,默认50%,比如(10s)内有100个请求,其中有60个发生异常,那么这段时间的错误率是60,已经超过了错误率阈值,熔断器会自动打开。
- circuitBreakerForceOpen:熔断器强制打开(可以强制设定状态),始终保持打开状态,默认是false
- circuitBreakerForceClosed:熔断器强制关闭,始终保持关闭状态,默认是false
- circuitBreakerRequestVolumeThreshold:滑动窗口内(10s)的请求数阈值,只有达到了这个阈值,才有可能熔断。默认是20,如果这个时间段只有19个请求,就算全部失败了,也不会自动熔断。
- circuitBreakerSleepWindowInMilliseconds:熔断器打开之后,为了能够自动恢复,每隔默认5000ms放一个请求过去,试探所依赖的服务是否恢复。
断路器HystrixCircuitBreaker有三个状态:
- CLOSED关闭状态:允许流量通过。
- OPEN打开状态:不允许流量通过,即处于降级状态,走降级逻辑。
- HALF_OPEN半开状态:允许某些流量通过,并关注这些流量的结果,如果出现超时、异常等情况,将进入OPEN状态,如果成功,那么将进入CLOSED状态。
4. 自动熔断
HystrixBreaker实例化的时候会订阅HystrixCommandMetrics的 HealthCountsStream,每当HealthCountsStream搜集到数据,都会触发 onNext方法,自动熔断就是这里发生。这个过程也可以叫做健康指标的订阅。
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 onNext(HealthCounts hc) {
//首先校验的时在时间窗范围内的请求次数,如果低于阈值(默认是20),不做处理,如果高于阈值,则去判断接口请求的错误率
if (hc.getTotalRequests() < properties.circuitBreakerRequestVolumeThreshold().get()) {
} else {
//判断接口请求的错误率(阈值默认是50),如果高于这个值,则断路器打开
if (hc.getErrorPercentage() < properties.circuitBreakerErrorThresholdPercentage().get()) {
} else {
//打开断路器,同时记录断路器开启时间
if (status.compareAndSet(Status.CLOSED, Status.OPEN)) {
circuitOpened.set(System.currentTimeMillis());
}
}
}
}
});
}
在onNext方法中,参数hc保存了当前接口在前10s之内(时间窗口)的请求状态(请求总数、失败数和失败率),其主要逻辑是判断请求总数是否达到阈值requestVolumeThreshold,失败率是否达到阈值errorThresholdPercentage,如果都满足,说明接口的已经足够的不稳定,需要进行熔断,则设置status为熔断开启状态,并更新circuitOpened为当前时间戳,记录上次熔断开启的时间。
5. 是否通过断路器
每一个Command在execute执行后都会判断断路器是否可以通过,此逻辑实现在AbstractCommand#applyHystrixSemantics中:
private Observable<R> applyHystrixSemantics(final AbstractCommand<R> _cmd) {
//...省略部分代码
if (circuitBreaker.attemptExecution()) {
//...省略部分代码
}
新版本弃用了allowRequest(),取而代之的是attemptExecution()方法。正如方法名的含义,尝试执行。通过此方法的返回值决定执行正常逻辑,还是降级逻辑。
@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;
}
}
}
此方法的实现也比较直观
- 如果circuitBreakerForceOpen=true,说明熔断器已经强制开启,所有请求都会被熔断。
- 如果circuitBreakerForceClosed =true,说明熔断器已经强制关闭,所有请求都会被放行。
- circuitOpened默认-1,用以保存最近一次发生熔断的时间戳。
如果circuitOpened不等于 -1,说明已经发生熔断,通过isAfterSleepWindow()判断当前是否需要进行试探。判断逻辑是当前时间是否已经超过上次熔断的时间戳 + 试探窗口5000ms
// 睡眠窗口是否过去
private boolean isAfterSleepWindow() {
final long circuitOpenTime = circuitOpened.get();
final long currentTime = System.currentTimeMillis();
final long sleepWindowTime = properties.circuitBreakerSleepWindowInMilliseconds().get();
return currentTime > circuitOpenTime + sleepWindowTime;
}
- 接下来进入if分支,通过compareAndSet修改变量status。只有睡眠窗口(5000ms)之后的第一个请求可以执行正常逻辑,且修改当前状态为HALF_OPEN,进入半熔断状态,其它请求执行compareAndSet(Status.OPEN, Status.HALF_OPEN)时都返回false,执行降级逻辑。
上述过程过程就是熔断器自动恢复的逻辑,总结一下:
当进入OPEN状态后,会进入一段睡眠窗口,即只会OPEN一段时间,所以这个睡眠窗口过去,就会从OPEN状态变成HALF_OPEN状态,这种设计是为了能做到弹性恢复,这种状态的变更,并不是由调度线程来做,而是由请求来触发。
5. HALF OPEN
HystrixCommand执行的过程中,当error的时候会触发circuitBreaker.markNonSuccess(),执行成功或者执行完成触发 circuitBreaker.markSuccess()。追溯源码,这两个方法的调用时发生在attemptExecution()调用之后。在
AbstractCommand#executeCommandAndObserve中,下面省略的部分干扰代码。
private Observable<R> executeCommandAndObserve(final AbstractCommand<R> _cmd) {
final Action1<R> markEmits = new Action1<R>() {
@Override
public void call(R r) {
circuitBreaker.markSuccess();
}
};
final Action0 markOnCompleted = new Action0() {
@Override
public void call() {
circuitBreaker.markSuccess();
}
};
final Func1<Throwable, Observable<R>> handleFallback = new Func1<Throwable, Observable<R>>() {
@Override
public Observable<R> call(Throwable t) {
circuitBreaker.markNonSuccess();
}
};
return execution.doOnNext(markEmits)
.doOnCompleted(markOnCompleted)
.onErrorResumeNext(handleFallback)
.doOnEach(setRequestContext);
}
markSuccess中,若为半开状态时,会放第一个请求去执行,并跟踪它的执行结果,如果是成功,那么将由HALF_OPEN状态变成CLOSED状态,
markNonSuccess中,如果第一个被放去执行的请求执行失败(资源获取失败、异常、超时等),就会由HALP_OPEN状态再变为OPEN状态。
markNonSuccess(),通过compareAndSet修改status为熔断开启状态,并更新当前熔断开启的时间戳。
@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());
}
}
markSuccess()
@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);
}
}
如果断路器状态是半开并且成功设置为关闭状态时,执行以下步骤。1.重置度量指标对象,2.取消之前的订阅,发起新的订阅。3.设置断路器的打开时间为-1。
6. 滑动窗口原理
后面补充~