Sentinel源码10-熔断降级DegradeSlot

欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈

1. 概述

DegradeSlot是用于服务降级熔断:

  • 在执行entry的过程中,对于处于熔断open状态的情况则判断是否已经过了熔断期且设置半开成功,那么就通过.否则不通过报DegradeException

  • 对于处于降级状态即half-open的时候,则直接抛出DegradeException.

2. 熔断器

Sentinel的熔断器一共有两种ExceptionCircuitBreaker 和  ResponseTimeCircuitBreakerextends  AbstractCircuitBreaker  implements CircuitBreaker

在上一个之前slot执行过程中,如果发生了非BlockException即一些未知的throw,那么在exit内会判断error是否达到配置的erro数量或者错误比例。

如果整个调用过程超过了配置的超时时间 则也会触发熔断。

熔断的目的是将熔断器的状态设置到半开或者全开,这样在tryPass校验的时候就可以返回通过或者异常了。

配置面板如下:

根据配置项,可以具体看一下熔断器的接口 CircuitBreaker:

public interface CircuitBreaker {

    /**
     *  降级熔断规则
     */
    DegradeRule getRule();

    /**
     * true  判断需要降级
     */
    boolean tryPass(Context context);

    /**
     * 当前熔断器的状态
     */
    State currentState();

    /**
     * 回调方法   当请求pass通过后触发
     */
    void onRequestComplete(Context context);

    /**
     * Circuit breaker state.
     */
    enum State {
      
        OPEN,
        
        HALF_OPEN,
       
        CLOSED
    }
}
复制代码

了解了熔断规则以后,下面将具体阐述熔断流程。

3. 熔断机制的处理流程

image.png 达到熔断触发条件(假设触发条件为当接口每秒钟超过20%的处理产生异常,具体熔断规则由用户配置),便会开启熔断,在熔断状态下,X秒内所有该接口访问都会被Blocked快速失败(服务降级)

X秒后,下一次请求接口,此时为半开状态:

  • 如果请求接口成功,恢复到正常状态
  • 如果请求接口失败,回到熔断状态继续Blocked X秒

4. 熔断器状态

先看一下这整个熔断器的状态转化图,其中open到half-open的状态只发生在熔断器检验过程: image.png

5. 源码解析

Sentinel的熔断是由责任链中的最后一个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 {
        //在触发后续slot前执行熔断的检查
        performChecking(context, resourceWrapper);

        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }

    void performChecking(Context context, ResourceWrapper r) throws BlockException {
      	//通过资源名称获取所有的熔断CircuitBreaker
        List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName());
        if (circuitBreakers == null || circuitBreakers.isEmpty()) {
            return;
        }
        for (CircuitBreaker cb : circuitBreakers) {
              //cb.tryPass里面只做了状态检查,熔断是否关闭或者打开
            if (!cb.tryPass(context)) {
                throw new DegradeException(cb.getRule().getLimitApp(), cb.getRule());
            }
        }
    }

    @Override
    public void exit(Context context, ResourceWrapper r, int count, Object... args) {
        Entry curEntry = context.getCurEntry();
      	//如果当前其他solt已经有了BlockException直接调用fireExit,不用继续走熔断逻辑了
        if (curEntry.getBlockError() != null) {
            fireExit(context, r, count, args);
            return;
        }
      	//通过资源名称获取所有的熔断CircuitBreaker
        List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName());
        if (circuitBreakers == null || circuitBreakers.isEmpty()) {
            fireExit(context, r, count, args);
            return;
        }

        if (curEntry.getBlockError() == null) {
            //调用CircuitBreaker的onRequestComplete()方法
            for (CircuitBreaker circuitBreaker : circuitBreakers) {
                circuitBreaker.onRequestComplete(context);
            }
        }

        fireExit(context, r, count, args);
    }
}
复制代码

进入DegradeSlot时,只会检查断路器是否已经打开,再根据是否超过了重试时间来开启半开状态,然后就直接返回是否通过。而真正判断是否需要开启断路器的地方时在exit()方法里面,因为这个方法是在业务方法执行后调用的,断路器需要收集业务异常或者业务方法的执行时间来判断是否打开断路器

先来看进入DegradeSlot的entry()方法,这里调用了CircuitBreaker.tryPass()方法,CircuitBreakerExceptionCircuitBreakerResponseTimeCircuitBreaker两种类型的断路器,CircuitBreaker继承关系图如下:

image.png

entry()方法实际上调用了AbstractCircuitBreaker.tryPass()方法,这里只做了一个处理,如果断路器开启,但是上一个请求距离现在已经过了重试间隔时间就开启半启动状态。

public abstract class AbstractCircuitBreaker implements CircuitBreaker {    

		@Override
    public boolean tryPass(Context context) {
        if (currentState.get() == State.CLOSED) {
            return true;
        }
        if (currentState.get() == State.OPEN) {
            //如果断路器开启,但是上一个请求距离现在已经过了重试间隔时间就开启半启动状态
            return retryTimeoutArrived() && fromOpenToHalfOpen(context);
        }
        return false;
    } 
}
复制代码

exit()方法调用了ExceptionCircuitBreakerResponseTimeCircuitBreakeronRequestComplete()方法。

5.1 ExceptionCircuitBreaker

下面分析下比较简单的ExceptionCircuitBreaker,其对应的熔断策略为异常比例和异常数:

image.png

详细代码如下:

public class ExceptionCircuitBreaker extends AbstractCircuitBreaker {
   
    // 策略,异常比例还是异常数
    private final int strategy;
    // 最小请求数
    private final int minRequestAmount;
    // 比例阈值
    private final double threshold;

    private final LeapArray<SimpleErrorCounter> stat;

    @Override
    public void onRequestComplete(Context context) {
        Entry entry = context.getCurEntry();
        if (entry == null) {
            return;
        }
        Throwable error = entry.getError();
        //异常时间窗口计数器
        SimpleErrorCounter counter = stat.currentWindow().value();
        //异常数加1
        if (error != null) {
            counter.getErrorCount().add(1);
        }
        //总数加1
        counter.getTotalCount().add(1);

        handleStateChangeWhenThresholdExceeded(error);
    }

    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();
        }
        //请求总数小于minRequestAmount时不做熔断处理 minRequestAmount时配置在熔断规则里面的
        if (totalCount < minRequestAmount) {
            return;
        }
        double curCount = errCount;
        if (strategy == DEGRADE_GRADE_EXCEPTION_RATIO) {
            //如果熔断策略配置的是窗口时间内错误率就需要做百分比的计算
            curCount = errCount * 1.0d / totalCount;
        }
        //错误率或者错误数大于阈值就开启断路器
        if (curCount > threshold) {
            transformToOpen(curCount);
        }
    }
}
复制代码

ExceptionCircuitBreaker在业务方法执行后被调用,主要做了如下处理:

  • 断路器处于半开状态

    • 本次请求没有出现异常,关掉断路器
    • 本次请求出现了异常,打开断路器
  • Sentinel Dashboard降级规则中会配置最小请求数,如果请求总数小于最小请求数时不做熔断处理

  • 如果错误率或者错误数大于阈值就开启断路器

5.2 ResponseTimeCircuitBreaker

下面分析ResponseTimeCircuitBreaker

image.png

public class ResponseTimeCircuitBreaker extends AbstractCircuitBreaker {

    private static final double SLOW_REQUEST_RATIO_MAX_VALUE = 1.0d;

    // 最大RT
    private final long maxAllowedRt;
    // 最大 慢请求比例
    private final double maxSlowRequestRatio;
    // 最小请求数量
    private final int minRequestAmount;

    private final LeapArray<SlowRequestCounter> slidingCounter;

    public ResponseTimeCircuitBreaker(DegradeRule rule) {
        this(rule, new SlowRequestLeapArray(1, rule.getStatIntervalMs()));
    }

    ResponseTimeCircuitBreaker(DegradeRule rule, LeapArray<SlowRequestCounter> stat) {
        super(rule);
        AssertUtil.isTrue(rule.getGrade() == RuleConstant.DEGRADE_GRADE_RT, "rule metric type should be RT");
        AssertUtil.notNull(stat, "stat cannot be null");
        this.maxAllowedRt = Math.round(rule.getCount());
        this.maxSlowRequestRatio = rule.getSlowRatioThreshold();
        this.minRequestAmount = rule.getMinRequestAmount();
        this.slidingCounter = stat;
    }

    @Override
    public void resetStat() {
        // Reset current bucket (bucket count = 1).
        slidingCounter.currentWindow().value().reset();
    }

    @Override
    public void onRequestComplete(Context context) {
        SlowRequestCounter counter = slidingCounter.currentWindow().value();
        Entry entry = context.getCurEntry();
        if (entry == null) {
            return;
        }
        long completeTime = entry.getCompleteTimestamp();
        if (completeTime <= 0) {
            completeTime = TimeUtil.currentTimeMillis();
        }
        long rt = completeTime - entry.getCreateTimestamp();
        if (rt > maxAllowedRt) {
            counter.slowCount.add(1);
        }
        counter.totalCount.add(1);

        handleStateChangeWhenThresholdExceeded(rt);
    }

    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) {
                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);
        }
        if (Double.compare(currentRatio, maxSlowRequestRatio) == 0 &&
                Double.compare(maxSlowRequestRatio, SLOW_REQUEST_RATIO_MAX_VALUE) == 0) {
            transformToOpen(currentRatio);
        }
    }

    static class SlowRequestCounter {
        private LongAdder slowCount;
        private LongAdder totalCount;

        public SlowRequestCounter() {
            this.slowCount = new LongAdder();
            this.totalCount = new LongAdder();
        }

        public LongAdder getSlowCount() {
            return slowCount;
        }

        public LongAdder getTotalCount() {
            return totalCount;
        }

        public SlowRequestCounter reset() {
            slowCount.reset();
            totalCount.reset();
            return this;
        }

        @Override
        public String toString() {
            return "SlowRequestCounter{" +
                "slowCount=" + slowCount +
                ", totalCount=" + totalCount +
                '}';
        }
    }

    static class SlowRequestLeapArray extends LeapArray<SlowRequestCounter> {

        public SlowRequestLeapArray(int sampleCount, int intervalInMs) {
            super(sampleCount, intervalInMs);
        }

        @Override
        public SlowRequestCounter newEmptyBucket(long timeMillis) {
            return new SlowRequestCounter();
        }

        @Override
        protected WindowWrap<SlowRequestCounter> resetWindowTo(WindowWrap<SlowRequestCounter> w, long startTime) {
            w.resetTo(startTime);
            w.value().reset();
            return w;
        }
    }
}
复制代码

代码比较简单,就不做解释了。

6. 规则设置的参数

这些参数的设置,我们再直接贴官网的吧。

Field 说明 默认值
resource 资源名,即规则的作用对象
grade 熔断策略,支持慢调用比例/异常比例/异常数策略 慢调用比例
count 慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值
timeWindow 熔断时长,单位为 s
minRequestAmount 熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入) 5
statIntervalMs 统计时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入) 1000 ms
slowRatioThreshold 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入)

参考文章

Sentinel1.8.5源码github地址(注释)
Sentinel源码解析
Sentinel官网
Sentinel DegradeSlot熔断源码解析

猜你喜欢

转载自juejin.im/post/7150475442263162893