前面一篇文章介绍了sentinel的基本工作原理和限流原理。本文将从源码的角度对熔断降级的原理进行分析。熔断操作位于slot处理链的末尾,由名为DegradeSlot的类来处理的,来看看其源码
@SpiOrder(-1000)
public class DegradeSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args)
throws Throwable {
//熔断降级判断
DegradeRuleManager.checkDegrade(resourceWrapper, context, node, count);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
fireExit(context, resourceWrapper, count, args);
}
}
源码很简单,委托DegradeRuleManager来处理,进入DegradeRuleManager的checkDegrade方法
public static void checkDegrade(ResourceWrapper resource, Context context, DefaultNode node, int count)
throws BlockException {
//获取资源熔断规则
Set<DegradeRule> rules = degradeRules.get(resource.getName());
if (rules == null) {
return;
}
//遍历每个熔断规则,校验是否满足熔断条件
for (DegradeRule rule : rules) {
//如果达到了熔断条件,就会抛出DegradeException的异常
if (!rule.passCheck(context, node, count)) {
throw new DegradeException(rule.getLimitApp(), rule);
}
}
}
熔断的判断就是针对资源设置的规则,逐一判断处理。如果有一个条件不满足的话,就会抛出DegradeException异常。那么熔断判断具体是怎么做的呢?继续深入DegradeRule类中的passCheck方法,在分析passCheck方法之前,先介绍DegradeRule类几个比较重要的字段。
//慢请求或异常请求的计数
private double count;
//熔断窗口
private int timeWindow;
//熔断策略 (0: 慢调用, 1: 异常率, 2: 异常数)
private int grade = RuleConstant.DEGRADE_GRADE_RT;
/**
* 针对慢调用,如果慢调用数小于其值(默认为5),是不会触发熔断的
*
* @since 1.7.0
*/
private int rtSlowRequestAmount = RuleConstant.DEGRADE_DEFAULT_SLOW_REQUEST_AMOUNT;
/**
* 针对异常率,如果异常数小于其值(默认为5),是不会触发熔断的
*
* @since 1.7.0
*/
private int minRequestAmount = RuleConstant.DEGRADE_DEFAULT_MIN_REQUEST_AMOUNT;
熔断的实现原理简单说来就是在一个设定的窗口时间内,根据设置的具体熔断策略,判断相应的计数统计是否超过了门限值,如果超过了则会触发熔断机制。深入passCheck的源码
//慢调用计数
private AtomicLong passCount = new AtomicLong(0);
//熔断降级标记位,如果为true,则表示触发了熔断
private final AtomicBoolean cut = new AtomicBoolean(false);
public boolean passCheck(Context context, DefaultNode node, int acquireCount, Object... args) {
//如果标记位为真,表示已触发熔断
if (cut.get()) {
return false;
}
//获取资源计数统计node
ClusterNode clusterNode = ClusterBuilderSlot.getClusterNode(this.getResource());
if (clusterNode == null) {
return true;
}
//如果熔断降级策略为慢调用
if (grade == RuleConstant.DEGRADE_GRADE_RT) {
//获取慢调用平均响应时间
double rt = clusterNode.avgRt();
//如果调用平均响应时间小于设定的门限值,则重置慢调用计数统计
if (rt < this.count) {
passCount.set(0);
return true;
}
//如果慢调用数小于默认的最小门限数(5),则不进行熔断降级
if (passCount.incrementAndGet() < rtSlowRequestAmount) {
return true;
}
//如果熔断降级策略是异常率
} else if (grade == RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO) {
//每秒的异常数
double exception = clusterNode.exceptionQps();
//每秒成功调用数
double success = clusterNode.successQps();
//每秒总调用数
double total = clusterNode.totalQps();
//如果总调用数小于默认的门限值(5),则不会触发熔断降级
if (total < minRequestAmount) {
return true;
}
//此句需要好好理解下,它表达的意思是:在异常数小于最小门限的条件是不进行熔断降级的,但前提是所用调用都不能全是异常调用
double realSuccess = success - exception;
if (realSuccess <= 0 && exception < minRequestAmount) {
return true;
}
//异常率小于设置的门限,则不熔断降级
if (exception / success < count) {
return true;
}
//如果熔断降级策略是异常数
} else if (grade == RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT) {
//注意,这个异常数是每分钟统计的
double exception = clusterNode.totalException();
//小于设置的门限值,则不熔断
if (exception < count) {
return true;
}
}
//如果走到了这里,则表示将要触发熔断降级了
//重置慢调用统计时间窗口,此处用了CAS的方法来设置标志位,防止并发。时间窗口的重置是依赖于定时任务来完成的,当timeWindow时间后,会重置熔断标志位和计数统计
if (cut.compareAndSet(false, true)) {
ResetTask resetTask = new ResetTask(this);
pool.schedule(resetTask, timeWindow, TimeUnit.SECONDS);
}
return false;
}
//重置时间窗口
private static final class ResetTask implements Runnable {
private DegradeRule rule;
ResetTask(DegradeRule rule) {
this.rule = rule;
}
@Override
public void run() {
//重置慢调用计数
rule.passCount.set(0);
//熔断标志位
rule.cut.set(false);
}
}
上面的代码描述了熔断降级核心流程,针对上面代码需要注意的是:
- 慢调用是通过一个时间窗口来计数慢调用的次数来实现的
- 异常率是针对每秒的异常数和成功数的比值来判断是否满足触发条件的
- 异常数是针对每分钟的异常数统计来实现的
当熔断被触发后,标志位会被设置为true,并会持续timeWindow长的时间,这个时间就是开发者在设置熔断降级规则时设置的。上述就是整个熔断降级的实现过程,从代码来看,熔断窗口通过一个定时任务来更新,设计的还是比较新颖的。