目录
- 前言
- 断路器
- HystrixCircuitBreaker
-
- HystrixCircuitBreakerImpl
-
- 属性
- 熔断器打开
- markSuccess
- markNonSuccess
- allowRequest
- 尝试执行-attemptExecution
- 总结
前言
我们前面对
Hystrix
的指标统计方式做了详细的介绍。
1.5.0
版本之前使用的是
HystrixRollingNumber
环形数组来作为指标的收集的模块,而
1.5.0
版本之后使用的是更灵活的响应式收集模式。有了各项请求的指标那么这一篇文章我们就来看看
Hystrix
的断路器是如何使用这些指标来作为依据的。
断路器
Hystrix的断路器有三种状态:打开、半开、关闭。
- 关闭状态:请求正常进入
- 打开状态:拒绝所有的请求,如果这时有降级的逻辑走降级逻辑
- 半开状态:打开状态的默认过5s状态会自动变成半开状态,该状态下允许一个请求进入,如果请求成功关闭断路器 如果请求失败重新进入打开状态。
如下借用官网的一张图:
我们下面主要是来看Hystrix是如何实现这三种状态的转换的。
HystrixCircuitBreaker
该接口定义了如下方法:
/**
* 每次请求都会判断是否允许请求 该方法具有幂等性
*/
boolean allowRequest();
/**
* 断路器是否处于打开的状态
*/
boolean isOpen();
/**
* 标记请求成功
*/
void markSuccess();
/**
* 标记请求失败
*/
void markNonSuccess();
/**
* 尝试执行
*/
boolean attemptExecution();
上面方法都很直接 并且我们可以认为
HystrixCircuitBreaker
有且仅有一个实现
HystrixCircuitBreakerImpl
(
NoOpCircuitBreaker
也是一种实现 但是没有太大意义)
HystrixCircuitBreakerImpl
作为
HystrixCircuitBreaker
的唯一的实现
HystrixCircuitBreakerImpl
重要程度不言而喻。
(注意看源码的注释)
属性
//配置
private final HystrixCommandProperties properties;
//指标统计器 这个我们前面没有说到 但是你可以简单的理解它是一个聚合(对各种统计流的聚合)
//特别是聚合了HealthCountsStream
private final HystrixCommandMetrics metrics;
//断路器的三种状态
enum Status {
CLOSED, OPEN, HALF_OPEN;
}
//保存当前断路器的状态默认是关闭的
private final AtomicReference<Status> status = new AtomicReference<Status>(Status.CLOSED);
//熔断器关闭的记录为-1
private final AtomicLong circuitOpened = new AtomicLong(-1);
//保存对HealthCountsStream的订阅结果 用于重订阅。那可能会问 为什么要重订阅
//其实就是为了初始化统计数据 例如从打开状态的进入关闭状态 这个时候需要重新统计指标来为下一次判定做准备
private final AtomicReference<Subscription> activeSubscription = new AtomicReference<Subscription>(null);
熔断器打开
//这个方法很重要 通过订阅HealthCountsStream来实时判断是否要打开熔断开关
private Subscription subscribeToStream() {
return metrics.getHealthCountsStream()
.observe()
.subscribe(new Subscriber<HealthCounts>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(HealthCounts hc) {
//判断当前的请求总量是否超过我们设置的请求量的阈值
//可以通过hystrix.command.default.circuitBreaker.requestVolumeThreshold来配置请求阈值
//默认是 20个
if (hc.getTotalRequests() < properties.circuitBreakerRequestVolumeThreshold().get()) {
} else {
//如果失败率大于我们配置的失败率 就把熔断状态改为打开的状态并记录打开的时间
//失败率默认是50% 10s 20个请求 50%都失败了 才会打开熔断的开关
if (hc.getErrorPercentage() < properties.circuitBreakerErrorThresholdPercentage().get()) {
} else {
if (status.compareAndSet(Status.CLOSED, Status.OPEN)) { circuitOpened.set(System.currentTimeMillis());
}
}
}
}
});
}
上面的方法主要是一个订阅的逻辑 然后根据订阅的数据来判定是否需要打开 断路器
- 先判断10s内的总请求数量是否大于
可通过默认值20
来配置hystrix.command.default.circuitBreaker.requestVolumeThreshold
- 其次判断失败率是否超过
可通过默认值 50%
来配置hystrix.command.default.circuitBreaker.errorThresholdPercentage
- 如果上面条件都满足就打开断路器 并记录时间
markSuccess
@Override
public void markSuccess() {
//通过CAS关闭断路器
if (status.compareAndSet(Status.HALF_OPEN, Status.CLOSED)) {
//重置统计流
metrics.resetStream();
//如果当前activeSubscription也有订阅 那么重新订阅
Subscription previousSubscription = activeSubscription.get();
if (previousSubscription != null) {
previousSubscription.unsubscribe();
}
Subscription newSubscription = subscribeToStream();
activeSubscription.set(newSubscription);
circuitOpened.set(-1L);
}
}
markNonSuccess
@Override
public void markNonSuccess() {
if (status.compareAndSet(Status.HALF_OPEN, Status.OPEN)) {
circuitOpened.set(System.currentTimeMillis());
}
}
标记不成功 将断路器从半开状态变为打开状态。
circuitOpened
记录打开的时间戳
allowRequest
//isAfterSleepWindow 这个方法的逻辑主要判断在断路器打开的状态下 默认5s 要尝试去发送一笔请求
//判断打开的时间是否超过5s
@Override
private boolean isAfterSleepWindow() {
//circuitOpened 这个变量 在断路器打开的状态下存储的是时间戳
final long circuitOpenTime = circuitOpened.get();
final long currentTime = System.currentTimeMillis();
//可以通过hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds来配置
final long sleepWindowTime = properties.circuitBreakerSleepWindowInMilliseconds().get();
return currentTime > circuitOpenTime + sleepWindowTime;
}
public boolean allowRequest() {
//判断是否配置断路器强制打开
if (properties.circuitBreakerForceOpen().get()) {
return false;
}
//判断是否配置断路器强制关闭
if (properties.circuitBreakerForceClosed().get()) {
return true;
}
//如果断路器是关闭的状态 返回true
if (circuitOpened.get() == -1) {
return true;
} else {
//进到这里 说明断路器是打开的状态(可能是半开 可能是打开)
//如果断路器是半开的状态返回false
if (status.get().equals(Status.HALF_OPEN)) {
return false;
} else {
//如果是打开的状态 要检查当前状态停留的时间是否超过了阈值
return isAfterSleepWindow();
}
}
}
尝试执行-attemptExecution
@Override
public boolean attemptExecution() {
if (properties.circuitBreakerForceOpen().get()) {
return false;
}
if (properties.circuitBreakerForceClosed().get()) {
return true;
}
if (circuitOpened.get() == -1) {
return true;
} else {
//如果断路器处于打开的状态 并且如果打开状态超过阈值 就将状态设置成半开状态
//设置成功之后 true 让当前请求可以执行
if (isAfterSleepWindow()) {
if (status.compareAndSet(Status.OPEN, Status.HALF_OPEN)) {
return true;
} else {
return false;
}
} else {
return false;
}
}
}
上面的代码设置
if (status.compareAndSet(Status.OPEN, Status.HALF_OPEN))
这个分支很关键。将打开状态下的断路器设置成半打开状态 设置成功之后立马返回true 允许一笔请求通过。设置失败说明当前状态是半开状态,所以保证了只有一笔请求能通过。
总结
断路器的所有的逻辑都是在
HystrixCircuitBreakerImpl
中,所以它是断路器的核心。幸运的是它的实现并不复杂 理解指标获取这一块是关键。