前言:
上一篇我对 Sentinel 中的 FlowSlot 的相关源码进行了分析,本篇我们开始分析 DegradeSlot相关的源码。
Sentinel 系列文章传送门:
Spring Cloud 整合 Nacos、Sentinel、OpenFigen 实战【微服务熔断降级实战】
Sentinel 源码分析入门【Entry、Chain、Context】
Sentine 源码分析之–NodeSelectorSlot、ClusterBuilderSlot、StatisticSlot
Sentine 源码分析之–AuthoritySlot、SystemSlot、GatewayFlowSlot
DegradeSlot
DegradeSlot 是 Slot 责任链上的最后一环,是用来熔断降级的,可以根据慢调用、异常比例和异常数进行熔断,并自定义持续时间以实现系统保护。
DegradeSlot 规则配置如下:
规则中各个字段的含义在 DegradeRule 源码中都有描述,如下:
public class DegradeRule extends AbstractRule {
//熔断策略 (0: 平均RT 1: 异常比例 2: 异常计数)
private int grade = RuleConstant.DEGRADE_GRADE_RT;
//阈值 含义取决于所选择的熔断策略
private double count;
//断路器断开后恢复时间(单位秒) 超时后断路器转换成半开状态 允许部分请求通过
private int timeWindow;
//触发熔断最低请求数 DEGRADE_DEFAULT_MIN_REQUEST_AMOUNT = 5
private int minRequestAmount = RuleConstant.DEGRADE_DEFAULT_MIN_REQUEST_AMOUNT;
//RT模式下慢请求比例的阈值
private double slowRatioThreshold = 1.0d;
//间隔统计持续时间 (毫秒)
private int statIntervalMs = 1000;
}
断路器的分类
通过上面的规则配置截图,我们可以把断路器分为两类,如下:
- 异常类型断路器:异常数、异常比例都是异常类型断路器来实现的,在请求结束时统计异常数和请求总数,计算二者比例,判断是否达到阈值,达到阈值更改断路器状态。
- RT类型断路器:是慢比例调用的断路器的实现,计算请求结束和请求开始的差值,和阈值比较,得到慢调用请求数和请求总数,计算二者比例,判断是否达到阈值,达到阈值更改断路器状态。
断路器的状态
- OPEN: 断路器打开状态, 系统进入熔断状态。
- HALF_OPEN:断路器半开状态,系统放行部分请求,如果请求通过,断路器切回到关闭状态, 如果请求出现异常,断路器切回到打开状态。
- CLAOSE:断路器关闭状态,系统正常。
DegradeSlot#entry 方法源码解析
DegradeSlot#entry 方法同样是执行规则校验,然后执行下一个 Slot(这里其实没有下一个 Slot 了)。
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args) throws Throwable {
//执行检查
performChecking(context, resourceWrapper);
//下一个 slot
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
DegradeSlot#performChecking 方法源码解析
DegradeSlot#performChecking 方法主要是获取所有断路器,遍历所有短路器,执行规则校验。
//com.alibaba.csp.sentinel.slots.block.degrade.DegradeSlot#performChecking
void performChecking(Context context, ResourceWrapper r) throws BlockException {
//根据资源名称获取短路器
List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName());
if (circuitBreakers == null || circuitBreakers.isEmpty()) {
//断路器为空 直接返回
return;
}
//遍历 断路器
for (CircuitBreaker cb : circuitBreakers) {
//断路器判断
if (!cb.tryPass(context)) {
throw new DegradeException(cb.getRule().getLimitApp(), cb.getRule());
}
}
}
AbstractCircuitBreaker#tryPass 方法源码解析
AbstractCircuitBreaker#tryPass 主要是判断断路器状态,如果短路器状态是关闭的直接返回规则通过,如果短路器是打开的,则执行规则检测。
//com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.AbstractCircuitBreaker#tryPass
@Override
public boolean tryPass(Context context) {
// Template implementation.
//断路器是否是关闭状态
if (currentState.get() == State.CLOSED) {
//断路器关闭状态 直接返回 通过
return true;
}
if (currentState.get() == State.OPEN) {
// For half-open state we allow a request for probing.
//retryTimeoutArrived() 判断当前时间是否大于尝试恢复的时间
//fromOpenToHalfOpen(context) 尝试将断路器的状态从打开 OPEN 更改为半开启 HALF_OPEN
return retryTimeoutArrived() && fromOpenToHalfOpen(context);
}
return false;
}
AbstractCircuitBreaker#retryTimeoutArrived 方法源码解析
AbstractCircuitBreaker#retryTimeoutArrived 方法主要判断当前时间是否大于尝试恢复的时间,如果大于则返回 true 否则返回 false。
//com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.AbstractCircuitBreaker#retryTimeoutArrived
protected boolean retryTimeoutArrived() {
//如果当前系统时间大于等于下一次尝试恢复的时间 也就是说已经到达了可以尝试恢复的时间 则返回 true 否则返回 false
return TimeUtil.currentTimeMillis() >= nextRetryTimestamp;
}
AbstractCircuitBreaker#fromOpenToHalfOpen 方法源码解析
AbstractCircuitBreaker#fromOpenToHalfOpen 尝试将断路器的状态从打开 OPEN 更改为半开启 HALF_OPEN,如果状态切换成功,返回 true 表示请求放行,否则返回 false 表示拒绝请求。
//com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.AbstractCircuitBreaker#fromOpenToHalfOpen
protected boolean fromOpenToHalfOpen(Context context) {
//设置开放到半开放
if (currentState.compareAndSet(State.OPEN, State.HALF_OPEN)) {
//通知观察者 当前断路器模式从 OPEN 状态变更成了 HALF_OPEN
notifyObservers(State.OPEN, State.HALF_OPEN, null);
//获取 从上下文 中获取 entry 对象
Entry entry = context.getCurEntry();
//向 Entry 实例注册一个 exit 回调处理器 在 Entry 实例的 exit 方法被调用时回调
entry.whenTerminate(new BiConsumer<Context, Entry>() {
@Override
public void accept(Context context, Entry entry) {
// Note: This works as a temporary workaround for https://github.com/alibaba/Sentinel/issues/1638
// Without the hook, the circuit breaker won't recover from half-open state in some circumstances
// when the request is actually blocked by upcoming rules (not only degrade rules).
if (entry.getBlockError() != null) {
// Fallback to OPEN due to detecting request is blocked
//由于检测到请求被阻止而回退到 OPEN
//如果当前请求被拒绝 不仅包括熔断器拒绝的 也包括限流、系统自适应等拒绝的 将熔断器从 HALF_OPEN 状态变为 OPEN 状态
currentState.compareAndSet(State.HALF_OPEN, State.OPEN);
//通知观察者 当前断路器模式从 HALF_OPEN 状态变更成了 OPEN
notifyObservers(State.HALF_OPEN, State.OPEN, 1.0d);
}
}
});
return true;
}
return false;
}
熔断降级的时机是触发配置阈值时,那数据是如何收集的呢?DegradeSlot#entry 方法是请求入口,此时请求还没有结束,无法获取到异常请求数量、 RT相关信息, DegradeSlot#exit 作为请求出口方法,此时请求已经结束,相关指标数据都可以收集到了,下面我们来分析一下 DegradeSlot#exit 方法。
DegradeSlot#exit 方法源码解析
DegradeSlot#exit 方法会获取当前 Entry 对象,判断 Entry 对象中是否有阻塞错误,如果有则执行执行下一个 exit 方法,否则获取所有断路器并进行为空判断,如果 Entry 对象中没有阻塞错误,且断路器也不为空,则遍历断路器执行 onRequestComplete 方法进行计数。
//com.alibaba.csp.sentinel.slots.block.degrade.DegradeSlot#exit
@Override
public void exit(Context context, ResourceWrapper r, int count, Object... args) {
//获取当前 Entry
Entry curEntry = context.getCurEntry();
//判断 entry 中是否有阻塞错误
if (curEntry.getBlockError() != null) {
//如果有就直接下一个 exit 方法 不用在执行断路器的逻辑了
fireExit(context, r, count, args);
return;
}
//获取所有断路器
List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName());
//为空 判断
if (circuitBreakers == null || circuitBreakers.isEmpty()) {
//如果断路器集合为空 也不需要走入校逻辑了
fireExit(context, r, count, args);
return;
}
//判断 entry 中是否有阻塞错误
if (curEntry.getBlockError() == null) {
//阻塞错误为空 遍历执行 onRequestComplete 方法计数
// passed request
for (CircuitBreaker circuitBreaker : circuitBreakers) {
circuitBreaker.onRequestComplete(context);
}
}
//执行下一个出口方法
fireExit(context, r, count, args);
}
ExceptionCircuitBreaker#onRequestComplete 方法源码解析
ExceptionCircuitBreaker#onRequestComplete 方法的主要作用是计数,当请求结束时候会根据熔断策略来更新计数器,如果达到阀值,则会打开断路器,并通知观察者,重点关注,handleStateChangeWhenThresholdExceeded 方法。
//com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.ExceptionCircuitBreaker#onRequestComplete
@Override
public void onRequestComplete(Context context) {
//获取当前 Entry 对象
Entry entry = context.getCurEntry();
//entry 为空判断
if (entry == null) {
return;
}
//获取 error 信息
Throwable error = entry.getError();
//获取当前窗口中的值
SimpleErrorCounter counter = stat.currentWindow().value();
//error 为空判断
if (error != null) {
//不为空 error 数量 +1
counter.getErrorCount().add(1);
}
//总数 +1
counter.getTotalCount().add(1);
//超过阀值时 状态变化
handleStateChangeWhenThresholdExceeded(error);
}
ExceptionCircuitBreaker#handleStateChangeWhenThresholdExceeded 方法源码解析
ExceptionCircuitBreaker#handleStateChangeWhenThresholdExceeded 方法通过计算异常请求的占比来判断是否需要打开断路器,具体逻辑如下:
- 如果断路器是打开状态,则直接返回,无需判断异常占比。
- 如果断路器是半开状态,判断 error 是否为空,如果 error 为空则可以关闭断路器,否则打开断路器。
- 获取所有 SimpleErrorCounter,计算异常请求数量和请求总数量。
- 如果请求总数没有达到达到最小请求数,规则直接通过。
- 计算异常比例,如果异常比例大于阀值,则会打开断路器并通知各个观察者。
//com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.ExceptionCircuitBreaker#handleStateChangeWhenThresholdExceeded
private void handleStateChangeWhenThresholdExceeded(Throwable error) {
//断路器状态判断
if (currentState.get() == State.OPEN) {
//状态已经打开了 直接返回
return;
}
if (currentState.get() == State.HALF_OPEN) {
// In detecting request
//短路器半开状态
if (error == null) {
//error 为空 表示可以关闭断路器了 并通知各个观察者
fromHalfOpenToClose();
} else {
//error 不为空 继续打开断路器 并通知各个观察者
fromHalfOpenToOpen(1.0d);
}
return;
}
//走到这里说明断路器是关闭状态
//获取所有错误计数器
List<SimpleErrorCounter> counters = stat.values();
//异常请求数量
long errCount = 0;
//总请求数量
long totalCount = 0;
//遍历错误计数器
for (SimpleErrorCounter counter : counters) {
//error 数增加
errCount += counter.errorCount.sum();
//总数增加
totalCount += counter.totalCount.sum();
}
//如果总数没有超过 最小请求数(规则配置的时候 有配置该值) 直接放行
if (totalCount < minRequestAmount) {
return;
}
double curCount = errCount;
//熔断策略是否是异常比例
if (strategy == DEGRADE_GRADE_EXCEPTION_RATIO) {
// Use errorRatio
//熔断策略为异常比例 计算异常比例
curCount = errCount * 1.0d / totalCount;
}
//异常比例
if (curCount > threshold) {
//异常比例大于阀值 会打开断路器 通知各个观察者
transformToOpen(curCount);
}
}
ConfigCacheService#dumpBeta 方法源码解析
ResponseTimeCircuitBreaker#onRequestComplete 方法的主要作用是记录慢请求数量,具体的慢请求占比的计算逻辑在 handleStateChangeWhenThresholdExceeded 方法中,我们重点关注该方法。
//com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.ResponseTimeCircuitBreaker#onRequestComplete
@Override
public void onRequestComplete(Context context) {
//获取慢请求计数器
SlowRequestCounter counter = slidingCounter.currentWindow().value();
//获取当前 entry
Entry entry = context.getCurEntry();
//entry 为空判断
if (entry == null) {
return;
}
//获取请求完成湿巾
long completeTime = entry.getCompleteTimestamp();
if (completeTime <= 0) {
completeTime = TimeUtil.currentTimeMillis();
}
//完成时间-创建时间 计算 rt
long rt = completeTime - entry.getCreateTimestamp();
//rt 是否大于最大允许 rt
if (rt > maxAllowedRt) {
//大于最大允许 rt 慢请求数+1
counter.slowCount.add(1);
}
//请求总数+1
counter.totalCount.add(1);
//超过阀值时候的处理
handleStateChangeWhenThresholdExceeded(rt);
}
ConfigCacheService#dumpBeta 方法源码解析
ResponseTimeCircuitBreaker#handleStateChangeWhenThresholdExceeded 方法通过计算慢请求的占比来判断是否需要打开断路器,具体逻辑如下:
- 如果断路器是打开状态,则直接返回,无需判断慢请求占比。
- 如果断路器是半开状态,判断 error 是否为空,如果 error 为空则可以关闭断路器,否则打开断路器。
- 获取所有 SlowRequestCounter,计算慢请求数量和请求总数量。
- 如果请求总数没有达到达到最小请求数,规则直接通过。
- 计算慢请求比例,如果慢请求比例大于阀值,则会打开断路器并通知各个观察者。
//com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.ResponseTimeCircuitBreaker#handleStateChangeWhenThresholdExceeded
private void handleStateChangeWhenThresholdExceeded(long rt) {
//断路器状态判断
if (currentState.get() == State.OPEN) {
//断路器打开的状态 直接返回
return;
}
//断路器是否半开
if (currentState.get() == State.HALF_OPEN) {
// In detecting request
// TODO: improve logic for half-open recovery
if (rt > maxAllowedRt) {
//rt 大于最大允许 rt 断路器打开
fromHalfOpenToOpen(1.0d);
} else {
//断路器关闭
fromHalfOpenToClose();
}
return;
}
//获取所有慢请求计数器
List<SlowRequestCounter> counters = slidingCounter.values();
//慢请求数量
long slowCount = 0;
//请求总数
long totalCount = 0;
//遍历慢请求计数器
for (SlowRequestCounter counter : counters) {
//慢请求书+1
slowCount += counter.slowCount.sum();
//请求总数+1
totalCount += counter.totalCount.sum();
}
//请求总数是否小于 最小请求数量
if (totalCount < minRequestAmount) {
//是 直接放行
return;
}
//计算慢请求占比
double currentRatio = slowCount * 1.0d / totalCount;
if (currentRatio > maxSlowRequestRatio) {
//慢请求占比大于阀值 断路器打开 通知观察者
transformToOpen(currentRatio);
}
}
如有不正确的地方请各位指出纠正。