Sentinel源碼分析----降級熔斷規(guī)則與DegradeSlot

上篇文章講了流控規(guī)則,而除了流控規(guī)則之后還有降級、熱點、系統(tǒng)、授權(quán)等規(guī)則,這篇文件主要講降級規(guī)則。

降級規(guī)則主要處理節(jié)點是DegradeSlot,其中具體邏輯由DegradeRuleManager.checkDegrade實現(xiàn)

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

        List<DegradeRule> 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);
            }
        }
    }

獲取所有的降級規(guī)則,進行一個個的校驗,校驗邏輯是由DegradeRule實現(xiàn),這里和流控規(guī)則FlowRule類似,先看下內(nèi)部屬性

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的情況下,如果連續(xù)RT_MAX_EXCEED_N個請求都大于配置的值,那么會在窗口時間內(nèi)會進行降級狀態(tài),所有流量都會返回false(拋出 DegradeException);在降級策略異常比例的情況下,總qps且異常數(shù)大于該值才會進行異常比例的判斷
  • count:降級策略RT則表示響應時間;降級策略異常比例則表示異常比例;降級策略異常數(shù)則表示異常數(shù)量
  • timeWindow:降級的時間窗口,在該窗口時間內(nèi)請求都不能通過
  • grade:降級熔斷策略
  • cut:是否被降級熔斷,如果true,則請求過來直接拒絕
  • passCount:降級策略RT的時候用來統(tǒng)計超過配置值的數(shù)量

接下來看下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;
        }

        //省略降級策略的處理....
        
        // 到達這里表示觸發(fā)了降級規(guī)則,需要降級熔斷
        // 這里用鎖是防止多線程更新cut,導致重復創(chuàng)建了ResetTask
        synchronized (lock) {
            if (!cut) {// 如果沒有降級熔斷,則需要設置為true
                // Automatically degrade.
                cut = true;
                // 創(chuàng)建一個延時任務,在時間窗口過后將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已經(jīng)超過閾值,是否返回失敗需要判斷passCount是否大于等于RT_MAX_EXCEED_N
    // 遞增passCount的值,然后判斷是否大于RT_MAX_EXCEED_N
    // 如果小于RT_MAX_EXCEED_N那么還是返回成功
    // 直到連續(xù)超過閾值RT_MAX_EXCEED_N次才返回失敗
    if (passCount.incrementAndGet() < RT_MAX_EXCEED_N) {
        return true;
    }

這種情況下需要注意一種情況:
假設接口平均rt很小,但是某一次請求時間大幅度的上升,這樣會導致整個接口的rt大幅度上升,這樣會導致異常降級,例如某個接口平均rt為1ms,配置的閾值為10ms,例如某一次請求rt達到了1s,導致整個接口的平均rt到了100ms,那么就會導致錯誤降級熔斷

請求量小的接口可能會出現(xiàn)上述情況,如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;
    // 失敗數(shù)小于RT_MAX_EXCEED_N且成功數(shù)小于0的情況則無視
    if (realSuccess <= 0 && exception < RT_MAX_EXCEED_N) {
        return true;
    }
    // 異常比例判斷
    if (exception / success < count) {
        return true;
    }

注意:

  1. clusterNode.successQps()返回的是成功執(zhí)行完了Slot鏈且沒有被規(guī)則攔截的數(shù)量
  2. clusterNode.exceptionQps()返回的是基于1的基礎且業(yè)務處理中出現(xiàn)異常的數(shù)量,該需要需要用Tracer.trace(t)捕獲,才會計入統(tǒng)計
  3. 由12可知,clusterNode.successQps()包含了clusterNode.exceptionQps(),所以realSuccess需要減去重合的部分才是真正成功的數(shù)量

降級策略:異常數(shù)

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

異常數(shù)這個規(guī)則比較簡單,就是判斷一分鐘內(nèi)的異常數(shù)是否大于閾值。這里還有個注意點:在時間窗口小于60s的時候,會導致降級熔斷時間窗口過后,還是會被降級熔斷,是因為這里是判斷的一分鐘的異常數(shù),時間窗口太小會導致恢復熔斷后,異常數(shù)還是大于等于閾值。

測試代碼如下(在官方提供的ExceptionCountDegradeDemo基礎上修改),先配置一個規(guī)則


    private static void initDegradeRule() {
        List<DegradeRule> rules = new ArrayList<DegradeRule>();
        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);
    }

配置一個降級規(guī)則,策略是異常數(shù),數(shù)量為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);
      // 繼續(xù)執(zhí)行
        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循環(huán)執(zhí)行10次邏輯,每次都拋出異常,并且用Tracer.trace記錄我們的業(yè)務異常,由于配置的異常數(shù)為4,所以執(zhí)行第四次結(jié)果過后,就已經(jīng)被降級熔斷了,打印的結(jié)果如下:

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

可以看到后面6次被block了,即被降級規(guī)則降級熔斷了,此時sleep11s,這個時候窗口時間已經(jīng)過了,但是執(zhí)行后續(xù)代碼發(fā)現(xiàn)輸出如下:

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

即這次請求也被block了,因為恢復之后異常數(shù)還是4,仍然不符合exception < count的判斷,這時如果將sleep的時間設置成60s,輸出如下

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

這時候,就正常了,因為統(tǒng)計的時間窗口已經(jīng)往后移動了,統(tǒng)計的原理需要了解一下sentinel的滑動時間窗口的原理

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容