【深入理解SpringCloud微服务】Sentinel源码解析——DegradeSlot熔断规则
Sentinel源码解析——DegradeSlot熔断规则
- 断路器原理复习
- DegradeSlot熔断规则原理
- 源码解析
- DegradeSlot#entry()
- DegradeSlot#exit(Context, ...)
- ResponseTimeCircuitBreaker
- ExceptionCircuitBreaker
断路器原理复习
断路器一般有三个状态:关闭、打开、半开,三种状态间的转换如下图:
首先断路器一开始是闭合状态。
当调用失败达到阈值时,就会切换为打开状态。当断路器处于打开状态时,由于接口的正常逻辑无法执行,此时就会进行降级处理。
当断路器处于打开状态一段时间后,就会变成半开状态,此时断路器允许一次请求调用,如果这次请求调用成功,那么断路器切换为闭合状态,否则重新变成打开状态。
DegradeSlot熔断规则原理
DegradeSlot在entry方法中进行断路器状态的校验。如果断路器状态为关闭,则请求正常通过(执行正常的业务处理逻辑);如果断路器为打开状态,则判断断路器打开的持续时长是否已经大于等于允许重试的时间,如果是则变成半开状态并允许本次请求正常通过;否则本次请求不能正常通过,抛出DegradeException异常。
但是实际上我们可以给一个资源配置多个熔断规则,每个熔断规则对应一个断路器,因此entry方法实际上是这样:
只有所有的断路器都校验通过,才算真正的通过,否则有一个断路器校验不通过,都会抛出DegradeException。
如果都校验通过了,则进入目标业务逻辑的处理(如果其他规则也校验通过)。
业务逻辑执行完后,DegradeSlot的exit方法将被调用。DegradeSlot的exit方法的作用是根据本次调用的情况给断路器做统计,然后根据断路器最新的统计值以及断路器当前状态判断是否需要进行断路器状态切换。
Sentinel的断路器也是利用时间窗进行统计。并且根据我们设置的熔断策略的不同,会使用不同类型的断路器进行统计。
如果我们设置的熔断策略是“慢调用比例”,那么断路器会利用滑动时间窗统计总调用数和慢调用数,如果本次调用超过了设置的最大响应时长,那么就记一次慢调用。
如果我们设置的熔断策略是“异常数”或“异常比例”,那么断路器会利用滑动时间窗统计总调用数和异常调用数,如果本次调用发生了异常,会记一次异常调用。
不管是什么类型的断路器,最后都会根据统计值与当前状态判断是否需要切换状态。
如果当前断路器状态是打开,那么不需要干啥,继续维持打开状态。如果当前断路器是半开状态,当前调用是慢调用或者是异常调用,那么切换为打开状态,否则切换为闭合状态。以上两种状态都不是,那么当前断路器状态就是闭合状态,那么就根据统计值判断是否超过阈值,如果超了则切换为打开状态,否则保持闭合状态。
源码解析
DegradeSlot#entry()
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args) throws Throwable {
// 熔断规则校验
performChecking(context, resourceWrapper);
fireEntry(...);
}
void performChecking(Context context, ResourceWrapper r) throws BlockException {
// 根据资源名称,从DegradeRuleManager中获取对应的所有断路器
List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName());
...
// 遍历每个断路器,进行规则校验,如果其中一个校验失败,则抛出DegradeException
for (CircuitBreaker cb : circuitBreakers) {
if (!cb.tryPass(context)) {
throw new DegradeException(cb.getRule().getLimitApp(), cb.getRule());
}
}
}
DegradeSlot方法会从DegradeRuleManager中根据资源名称取得对应的所有断路器,然后遍历这些断路器,进行规则校验,如果其中有一个校验失败,则抛出DegradeException。
AbstractCircuitBreaker#tryPass(Context)
@Override
public boolean tryPass(Context context) {
// 如果是闭合状态,返回true,表示校验通过
if (currentState.get() == State.CLOSED) {
return true;
}
// 如果是打开状态,判断是否到了重试时间,如果是,则切换为半开状态,允许本次校验通过
if (currentState.get() == State.OPEN) {
return retryTimeoutArrived() && fromOpenToHalfOpen(context);
}
// 否则校验失败
return false;
}
如果当前断路器状态是闭合状态,那么直接放行。
如果当前断路器状态是打开状态,那么调用retryTimeoutArrived()判断是否到了可以重试的时间。如果到了可以重试的时间,那么调用fromOpenToHalfOpen方法尝试切换为半开状态,这里有可能会切换失败,因为有可能有多个线程并发尝试切换,只有成功切换的那一个线程被放行,其他的都会被拦截掉;如果没到可以重试的时间,那么返回false表示校验失败。
除了上面两种情况,还有一种情况是当前断路器本身处于半开状态,也就是上面两个if判断都不通过,那么这种请求也是返回false不允许当前请求通过的。
AbstractCircuitBreaker#retryTimeoutArrived()
protected boolean retryTimeoutArrived() {
return TimeUtil.currentTimeMillis() >= nextRetryTimestamp;
}
retryTimeoutArrived方法很简单,就是判断一下当前时间戳是否大于等于断路器记录的下一次重试的时间戳nextRetryTimestamp。
AbstractCircuitBreaker#fromOpenToHalfOpen(Context)
protected boolean fromOpenToHalfOpen(Context context) {
// 使用CAS尝试切换为半开状态
if (currentState.compareAndSet(State.OPEN, State.HALF_OPEN)) {
...
return true;
}
return false;
}
fromOpenToHalfOpen方法则是通过CAS的方式尝试进行状态切换。
整个entry方法的流程如下:
DegradeSlot#exit(Context, …)
@Override
public void exit(Context context, ResourceWrapper r, int count, Object... args) {
Entry curEntry = context.getCurEntry();
// 如果前面的校验没通过,这里就不需要统计了
if (curEntry.getBlockError() != null) {
fireExit(context, r, count, args);
return;
}
// 根据资源名称获取所有的断路器
List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName());
...
// 遍历每一个断路器,进行统计,以及状态切换
for (CircuitBreaker circuitBreaker : circuitBreakers) {
circuitBreaker.onRequestComplete(context);
}
fireExit(context, r, count, args);
}
exit方法首先判断前面的规则校验是否没有通过,如果是,那么这里就不用做统计以及切换断路器状态了,因为规则校验没通过的话,当前请求是不会正常处理的。
如果请求被正常处理了,那么根据资源名称获取对应的所有断路器,然后遍历每一个断路器,进行统计和状态切换。
onRequestComplete方法又两个实现类,具体是什么类型,视我们选取的熔断策略而定。
ResponseTimeCircuitBreaker#onRequestComplete(Context):选择“慢调用比例”熔断策略时,断路器就是ResponseTimeCircuitBreaker类型,统计的是慢调用数和总调用数。
ExceptionCircuitBreaker#onRequestComplete(Context):选择“异常数”或者“异常比例”熔断策略时,断路器的类型就是ExceptionCircuitBreaker,统计的异常调用数和总调用数。
ResponseTimeCircuitBreaker
@Override
public void onRequestComplete(Context context) {
// slidingCounter.currentWindow()获取当前时间窗,返回的是WindowWrap类型
// value()方法返回WindowWrap里面的泛型对象SlowRequestCounter
SlowRequestCounter counter = slidingCounter.currentWindow().value();
...
// 完成时间
long completeTime = entry.getCompleteTimestamp();
...
// 响应时长 = 完成时间 - 开始时间
long rt = completeTime - entry.getCreateTimestamp();
// 如果响应时长超过了最大允许的响应时长,则慢调用数+1
if (rt > maxAllowedRt) {
counter.slowCount.add(1);
}
// 总调用数+1
counter.totalCount.add(1);
// 处理断路器状态的切换
handleStateChangeWhenThresholdExceeded(rt);
}
- ResponseTimeCircuitBreaker的onRequestComplete首先获取当前时间窗WindowWrap,然后获取WindowWrap里面的泛型对象SlowRequestCounter。
- 然后计算根据entry对象记录的开始时间和结束时间计算响应时长,判断是否超过最大允许响应时长,如果超了,那么慢调用数+1。
- 然后总调用数+1,不管响应时长超没超。
- 最后处理断路器状态的切换。
ResponseTimeCircuitBreaker#handleStateChangeWhenThresholdExceeded(long)
private void handleStateChangeWhenThresholdExceeded(long rt) {
// 如果断路器状态是打开,不需要处理,继续维持打开
if (currentState.get() == State.OPEN) {
return;
}
// 断路器状态是半开
if (currentState.get() == State.HALF_OPEN) {
// 当前调用是慢调用,切换为打开状态
if (rt > maxAllowedRt) {
fromHalfOpenToOpen(1.0d);
} else {
// 当前调用时正常调用,切换为闭合状态
fromHalfOpenToClose();
}
return;
}
// 计算当前的慢调用数和总调用数
List<SlowRequestCounter> counters = slidingCounter.values();
long slowCount = 0;
long totalCount = 0;
for (SlowRequestCounter counter : counters) {
slowCount += counter.slowCount.sum();
totalCount += counter.totalCount.sum();
}
// 总调用数小于最小请求数,熔断规则不生效
if (totalCount < minRequestAmount) {
return;
}
// 计算当前慢调用比例
double currentRatio = slowCount * 1.0d / totalCount;
// 比例是否超过阈值,如果超过阈值,则切换断路器状态为打开
if (currentRatio > maxSlowRequestRatio) {
transformToOpen(currentRatio);
}
...
}
- 如果断路器状态是打开,那么不需要处理,继续维持打开。
- 如果断路器状态是半开,那么判断当前请求是否是慢调用,如果是慢调用,切换为打开状态,否则切换为闭合状态。
- 如果即不是打开状态也不是半开状态,那么计数当前慢调用数和总调用数。
- 判断总调用数是否小于最小请求数,如果是,那么直接返回,不往下处理,熔断规则不生效。
- 如果总调用数大于等于最小请求数,那么计数慢调用比例。
- 如果慢调用比例超过阈值,切换断路器状态为打开。
整个ResponseTimeCircuitBreaker的统计与状态切换流程如下图:
ExceptionCircuitBreaker
ExceptionCircuitBreaker#onRequestComplete(Context):
@Override
public void onRequestComplete(Context context) {
...
Throwable error = entry.getError();
// 获取当前时间窗的计数器
SimpleErrorCounter counter = stat.currentWindow().value();
// 处理过程是否发生异常
if (error != null) {
// 异常调用数+1
counter.getErrorCount().add(1);
}
// 总调用数+1
counter.getTotalCount().add(1);
// 处理断路器状态切换
handleStateChangeWhenThresholdExceeded(error);
}
ExceptionCircuitBreaker类型断路器的处理流程大体上与ResponseTimeCircuitBreaker是类型的。
- 先取得当前时间窗对应的计数器。
- 判断处理过程是否发生异常,如果是,那么异常调用数加1,如果不是则异常调用数不用加1。
- 不管有没有发生异常,总调用数都加1。
- 最后处理断路器状态切换。
ExceptionCircuitBreaker#handleStateChangeWhenThresholdExceeded(Throwable)
private void handleStateChangeWhenThresholdExceeded(Throwable error) {
// 如果断路器状态是打开,不需要处理,继续维持打开
if (currentState.get() == State.OPEN) {
return;
}
// 断路器状态是半开
if (currentState.get() == State.HALF_OPEN) {
if (error == null) {
// 当前调用是正常调用,切换为闭合状态
fromHalfOpenToClose();
} else {
// 当前调用是异常调用,切换为打开状态
fromHalfOpenToOpen(1.0d);
}
return;
}
// 计算异常调用数和总调用数
List<SimpleErrorCounter> counters = stat.values();
long errCount = 0;
long totalCount = 0;
for (SimpleErrorCounter counter : counters) {
errCount += counter.errorCount.sum();
totalCount += counter.totalCount.sum();
}
// 总调用数小于最小请求数,熔断规则不生效
if (totalCount < minRequestAmount) {
return;
}
// 异常调用数
double curCount = errCount;
if (strategy == DEGRADE_GRADE_EXCEPTION_RATIO) {
// 如果熔断策略是“异常比例”,则计算异常比例赋值给curCount
// 异常比例 = 异常调用数 / 总调用数
curCount = errCount * 1.0d / totalCount;
}
// 如果大于阈值,则切换为打开状态
if (curCount > threshold) {
transformToOpen(curCount);
}
}
ExceptionCircuitBreaker处理状态切换的方法与ResponseTimeCircuitBreaker是高度相似的,与ResponseTimeCircuitBreaker不同的有以下两点:
- 如果熔断策略是“异常数”,那么curCount取值就是当前断路器记录的异常调用数;如果熔断策略是“异常比例”,那么curCount就要修改为计算得出的异常比例。
- 阈值threshold根据熔断策略的不同,记录的数值有不一样的意义。如果熔断策略是“异常数”,threshold记录的是异常数阈值;如果熔断策略是“异常比例”,threshold记录的时慢调用比例阈值。
整个ExceptionCircuitBreaker的统计与状态切换处理流程如下:
exit方法的整体处理流程如下: