Sentinel源码分析----降级熔断规则与DegradeSlot

上篇文章讲了流控规则,而除了流控规则之后还有降级、热点、系统、授权等规则,这篇文件主要讲降级规则。

降级规则主要处理节点是DegradeSlot,其中具体逻辑由DegradeRuleManager.checkDegrade实现

    public static void checkDegrade(ResourceWrapper resource, Context context, DefaultNode node, int count)
        throws BlockException {
        if (degradeRules == null) {
            return;
        }

        List rules = degradeRules.get(resource.getName());
        if (rules == null) {
            return;
        }

        for (DegradeRule rule : rules) {
            if (!rule.passCheck(context, node, count)) {
                throw new DegradeException(rule.getLimitApp(), rule);
            }
        }
    }

获取所有的降级规则,进行一个个的校验,校验逻辑是由DegradeRule实现,这里和流控规则FlowRule类似,先看下内部属性

public class DegradeRule extends AbstractRule {
    //
    private static final int RT_MAX_EXCEED_N = 5;

    private double count;

    private int timeWindow;

    private int grade = RuleConstant.DEGRADE_GRADE_RT;

    private volatile boolean cut = false;
    private AtomicLong passCount = new AtomicLong(0);
  • RT_MAX_EXCEED_N:在降级策略RT的情况下,如果连续RT_MAX_EXCEED_N个请求都大于配置的值,那么会在窗口时间内会进行降级状态,所有流量都会返回false(抛出 DegradeException);在降级策略异常比例的情况下,总qps且异常数大于该值才会进行异常比例的判断
  • count:降级策略RT则表示响应时间;降级策略异常比例则表示异常比例;降级策略异常数则表示异常数量
  • timeWindow:降级的时间窗口,在该窗口时间内请求都不能通过
  • grade:降级熔断策略
  • cut:是否被降级熔断,如果true,则请求过来直接拒绝
  • passCount:降级策略RT的时候用来统计超过配置值的数量

接下来看下DegradeRule的处理

    @Override
    public boolean passCheck(Context context, DefaultNode node, int acquireCount, Object... args) {
        //是否降级
        if (cut) {
            return false;
        }

        ClusterNode clusterNode = ClusterBuilderSlot.getClusterNode(this.getResource());
        if (clusterNode == null) {
            return true;
        }

        //省略降级策略的处理....
        
        // 到达这里表示触发了降级规则,需要降级熔断
        // 这里用锁是防止多线程更新cut,导致重复创建了ResetTask
        synchronized (lock) {
            if (!cut) {// 如果没有降级熔断,则需要设置为true
                // Automatically degrade.
                cut = true;
                // 创建一个延时任务,在时间窗口过后将cut设为false,将passCount设为0
                ResetTask resetTask = new ResetTask(this);
                pool.schedule(resetTask, timeWindow, TimeUnit.SECONDS);
            }
            // 返回false表示当前操作失败
            return false;
        }
    }

接下来看下具体策略的处理

降级策略:RT

    double rt = clusterNode.avgRt();
    //从node中获取平均rt
    if (rt < this.count) {// 如果小于配置的值,则可以直接返回成功
        // 将passCount重置
        passCount.set(0);
        return true;
    }
    // 到达这里表示当前请求rt已经超过阈值,是否返回失败需要判断passCount是否大于等于RT_MAX_EXCEED_N
    // 递增passCount的值,然后判断是否大于RT_MAX_EXCEED_N
    // 如果小于RT_MAX_EXCEED_N那么还是返回成功
    // 直到连续超过阈值RT_MAX_EXCEED_N次才返回失败
    if (passCount.incrementAndGet() < RT_MAX_EXCEED_N) {
        return true;
    }

这种情况下需要注意一种情况:
假设接口平均rt很小,但是某一次请求时间大幅度的上升,这样会导致整个接口的rt大幅度上升,这样会导致异常降级,例如某个接口平均rt为1ms,配置的阈值为10ms,例如某一次请求rt达到了1s,导致整个接口的平均rt到了100ms,那么就会导致错误降级熔断

请求量小的接口可能会出现上述情况,如qps只有10,某一次接口达到了1s会导致整个接口平均rt上升到100ms左右

降级策略:失败比例

    // 异常qps
    double exception = clusterNode.exceptionQps();
    // 成功qps
    double success = clusterNode.successQps();
    // 总qps=passQps+blockQps
    long total = clusterNode.totalQps();
    // 总qps小于RT_MAX_EXCEED_N则无视
    if (total < RT_MAX_EXCEED_N) {
        return true;
    }

    double realSuccess = success - exception;
    // 失败数小于RT_MAX_EXCEED_N且成功数小于0的情况则无视
    if (realSuccess <= 0 && exception < RT_MAX_EXCEED_N) {
        return true;
    }
    // 异常比例判断
    if (exception / success < count) {
        return true;
    }

注意:

  1. clusterNode.successQps()返回的是成功执行完了Slot链且没有被规则拦截的数量
  2. clusterNode.exceptionQps()返回的是基于1的基础且业务处理中出现异常的数量,该需要需要用Tracer.trace(t)捕获,才会计入统计
  3. 由12可知,clusterNode.successQps()包含了clusterNode.exceptionQps(),所以realSuccess需要减去重合的部分才是真正成功的数量

降级策略:异常数

    double exception = clusterNode.totalException();
    if (exception < count) {
        return true;
    }

异常数这个规则比较简单,就是判断一分钟内的异常数是否大于阈值。这里还有个注意点:在时间窗口小于60s的时候,会导致降级熔断时间窗口过后,还是会被降级熔断,是因为这里是判断的一分钟的异常数,时间窗口太小会导致恢复熔断后,异常数还是大于等于阈值。

测试代码如下(在官方提供的ExceptionCountDegradeDemo基础上修改),先配置一个规则


    private static void initDegradeRule() {
        List rules = new ArrayList();
        DegradeRule rule = new DegradeRule();
        rule.setResource(KEY);
        // set limit exception count to 4
        rule.setCount(4);
        rule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT);
        /**
         * When degrading by {@link RuleConstant#DEGRADE_GRADE_EXCEPTION_COUNT}, time window
         * less than 60 seconds will not work as expected. Because the exception count is
         * summed by minute, when a short time window elapsed, the degradation condition
         * may still be satisfied.
         */
        rule.setTimeWindow(10);
        rules.add(rule);
        DegradeRuleManager.loadRules(rules);
    }

配置一个降级规则,策略是异常数,数量为4,熔断时间窗口是10s(代码中的注释是官方提交的,从这里也看出降级熔断窗口太小是会有问题的)
运行代码如下:

    private static final String KEY = "abc";

    private static AtomicInteger total = new AtomicInteger();
    private static AtomicInteger pass = new AtomicInteger();
    private static AtomicInteger block = new AtomicInteger();
    private static AtomicInteger bizException = new AtomicInteger();

    private static volatile boolean stop = false;
    private static final int threadCount = 1;
    private static int seconds = 60 + 40;

    public static void main(String[] args) throws Exception {
        initDegradeRule();
        // 运行10次,每次都抛出异常
        for (int i = 0; i < 10; i++) {
            Entry entry = null;
            try {
                entry = SphU.entry(KEY);
                pass.addAndGet(1);
                throw new RuntimeException("throw runtime ");
            } catch (BlockException e) {
                block.addAndGet(1);
            } catch (Throwable t) {
                bizException.incrementAndGet();
                Tracer.trace(t);
            } finally {
                total.addAndGet(1);
                if (entry != null) {
                    entry.exit();
                }
            }
        }
        System.out.println("total:" + total.get() + ", pass:" + pass.get()
                + ", block:" + block.get() + ", bizException:" + bizException.get());
        // 上面运行后,会被降级熔断,窗口时间为10s,这里睡眠11s,等待窗口时间过去
        Thread.sleep(11000);
      // 继续执行
        Entry entry = null;
        try {
            entry = SphU.entry(KEY);
            pass.addAndGet(1);
        } catch (BlockException e) {
            block.addAndGet(1);
        } catch (Throwable t) {
            bizException.incrementAndGet();
            Tracer.trace(t);
        } finally {
            total.addAndGet(1);
            if (entry != null) {
                entry.exit();
            }
        }
        System.out.println("total:" + total.get() + ", pass:" + pass.get()
                + ", block:" + block.get() + ", bizException:" + bizException.get());

    }

上面代码中,第一次for循环执行10次逻辑,每次都抛出异常,并且用Tracer.trace记录我们的业务异常,由于配置的异常数为4,所以执行第四次结果过后,就已经被降级熔断了,打印的结果如下:

total:10, pass:4, block:6, bizException:4

可以看到后面6次被block了,即被降级规则降级熔断了,此时sleep11s,这个时候窗口时间已经过了,但是执行后续代码发现输出如下:

total:11, pass:4, block:7, bizException:4

即这次请求也被block了,因为恢复之后异常数还是4,仍然不符合exception < count的判断,这时如果将sleep的时间设置成60s,输出如下

total:11, pass:5, block:6, bizException:4

这时候,就正常了,因为统计的时间窗口已经往后移动了,统计的原理需要了解一下sentinel的滑动时间窗口的原理

你可能感兴趣的:(Sentinel源码分析----降级熔断规则与DegradeSlot)