Sentinel學習(五) —— 控制檯使用和源碼

通過 sentinel 的控制檯,我們可以對規則進行查詢和修改,也可以查看到實時監控,機器列表等信息,所以我們需要對 sentinel 的控制檯做個完整的瞭解。

啓動控制檯

從github上下載源碼後,啓動sentinel-dashboard模塊。默認地址是8080。用戶名和密碼配置到了application.properties中,可以自行修改,默認用戶名和密碼都是 sentinel
在這裏插入圖片描述
可以看到當前控制檯中沒有任何的應用,因爲還沒有應用接入。

接入控制檯

要想在控制檯中操作我們的應用,除了需要部署一個控制檯的服務外,還需要將我們的應用接入到控制檯中去。

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>

增加配置參數:控制檯地址。

spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080

控制檯使用懶加載,在第一次訪問的時候纔會開始進行初始化,並向控制檯發送心跳和客戶端規則等信息。
在這裏插入圖片描述
下面讓我們對控制檯的功能做具體的介紹。

流控規則

在簇點鏈路中可以配置流控規則,我們看下每個選項的含義。
在這裏插入圖片描述

  • 資源名:默認使用請求路徑,也可以修改成其他名稱。
  • 針對來源:可以對服務的調用來源進行限流。default爲默認,會限流所有來源,也可以設置成其他服務名。
  • 閥值類型:分爲QPS和線程數,指的是到達限流的判斷條件。
  • 是否集羣:設置單機還是集羣限流,這個之後再講解。
  • 流控模式:
    • 直接:指的是限流的目標就是當前的資源。
    • 關聯:需要填寫關聯資源。當關聯資源的訪問達到限流閥值,就會限制當前資源的訪問。這是對關聯資源的一種保護策略。
    • 鏈路:這個是對來源更細粒度的配置。需要配置入口資源,也就是說從某個請求URL的入口進入纔會進行流控判斷。比如:/test-a 和 /test-b 都調用 /common這個資源。如果入口資源配置成 /test-a,那麼 /test-b 不會進行流量控制。
  • 流控效果:
    • 快速失敗:如果流控就拋出異常。
    • Warm Up:先進行預熱,根據codeFactor(默認是3)的值,從閥值/codeFactor,經過預熱時長(秒)纔到達設置的QPS閥值。
    • 排隊等待:勻速排隊,讓請求以均勻的速度通過,閥值類型必須設置成QPS,否則無效。需要設置超時時間,如果超出超時時間,請求才會被丟棄。
源碼解析

流控是用FlowSlot進行的判斷。它也是責任鏈上的其中一個節點。我們知道這些節點的統一處理入口都是 entry方法。

FlowSlot#entry

public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                  boolean prioritized, Object... args) throws Throwable {
    checkFlow(resourceWrapper, context, node, count, prioritized);

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

void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized)
    throws BlockException {
    checker.checkFlow(ruleProvider, resource, context, node, count, prioritized);
}

FlowSlot在實例化的時候會實例化一個FlowRuleChecker實例作爲checker。在checkFlow方法裏面會繼續調用FlowRuleChecker的checkFlow方法,其中ruleProvider實例是用來根據根據resource來從flowRules中獲取相應的FlowRule。

我們進入到FlowRuleChecker的checkFlow方法中

FlowRuleChecker#checkFlow

public void checkFlow(Function<String, Collection<FlowRule>> ruleProvider, ResourceWrapper resource,
                      Context context, DefaultNode node, int count, boolean prioritized) throws BlockException {
    if (ruleProvider == null || resource == null) {
        return;
    }
    //返回FlowRuleManager裏面註冊的所有規則
    Collection<FlowRule> rules = ruleProvider.apply(resource.getName());
    if (rules != null) {
        for (FlowRule rule : rules) {
            //如果當前的請求不能通過,那麼就拋出FlowException異常
            if (!canPassCheck(rule, context, node, count, prioritized)) {
                throw new FlowException(rule.getLimitApp(), rule);
            }
        }
    }
}

這裏是調用ruleProvider來獲取所有FlowRule,然後遍歷rule集合通過canPassCheck方法來進行過濾,如果不符合條件則會拋出FlowException異常。

我們跟進去直接來到passLocalCheck方法:

private static boolean passLocalCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount,
                                      boolean prioritized) {
    //節點選擇
    Node selectedNode = selectNodeByRequesterAndStrategy(rule, context, node);
    if (selectedNode == null) {
        return true;
    }
    //根據設置的規則來攔截
    return rule.getRater().canPass(selectedNode, acquireCount, prioritized);
}

這個方法裏面會選擇好相應的節點後調用rater的canPass方法來判斷是否需要阻塞。

Rater有四個,分別是:DefaultController、RateLimiterController、WarmUpController、WarmUpRateLimiterController。它們是什麼時候創建的呢?主要是調用了下面的這個方法。

FlowRuleUtil#generateRater

private static TrafficShapingController generateRater(/*@Valid*/ FlowRule rule) {
    if (rule.getGrade() == RuleConstant.FLOW_GRADE_QPS) {
        switch (rule.getControlBehavior()) {
            case RuleConstant.CONTROL_BEHAVIOR_WARM_UP:
                //warmUpPeriodSec默認是10 
                return new WarmUpController(rule.getCount(), rule.getWarmUpPeriodSec(),
                    ColdFactorProperty.coldFactor);
            case RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER:
                //rule.getMaxQueueingTimeMs()默認是500
                return new RateLimiterController(rule.getMaxQueueingTimeMs(), rule.getCount());
            case RuleConstant.CONTROL_BEHAVIOR_WARM_UP_RATE_LIMITER:
                return new WarmUpRateLimiterController(rule.getCount(), rule.getWarmUpPeriodSec(),
                    rule.getMaxQueueingTimeMs(), ColdFactorProperty.coldFactor);
            case RuleConstant.CONTROL_BEHAVIOR_DEFAULT:
            default:
                // Default mode or unknown mode: default traffic shaping controller (fast-reject).
        }
    }
    return new DefaultController(rule.getCount(), rule.getGrade());
}

這個方法裏面如果設置的是按QPS的方式來限流的話,可以設置一個ControlBehavior屬性,用來做流量控制分別是:直接拒絕、Warm Up、勻速排隊。

RateLimiterController勻速排隊

它的中心思想是,以固定的間隔時間讓請求通過。當請求到來的時候,如果當前請求距離上個通過的請求通過的時間間隔不小於預設值,則讓當前請求通過;否則,計算當前請求的預期通過時間,如果該請求的預期通過時間小於規則預設的 timeout 時間,則該請求會等待直到預設時間到來通過(排隊等待處理);若預期的通過時間超出最大排隊時長,則直接拒接這個請求。

這種方式適合用於請求以突刺狀來到,這個時候我們不希望一下子把所有的請求都通過,這樣可能會把系統壓垮;同時我們也期待系統以穩定的速度,逐步處理這些請求,以起到“削峯填谷”的效果,而不是拒絕所有請求。

要想使用這個策略需要在實例化FlowRule的時候設置rule1.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)這樣的一句代碼。

在實例化Rater的時候會調用FlowRuleUtil#generateRater創建一個實例:

new RateLimiterController(rule.getMaxQueueingTimeMs(), rule.getCount());

MaxQueueingTimeMs默認是500 ,Count在我們這個例子中傳入的是20。

我們看一下具體的canPass方法是怎麼實現限流的:

public boolean canPass(Node node, int acquireCount, boolean prioritized) {
    // Pass when acquire count is less or equal than 0.
    if (acquireCount <= 0) {
        return true;
    }
    // Reject when count is less or equal than 0.
    // Otherwise,the costTime will be max of long and waitTime will overflow in some cases.
    if (count <= 0) {
        return false;
    }

    long currentTime = TimeUtil.currentTimeMillis();
    //兩個請求預期通過的時間,也就是說把請求平均分配到1秒上
    // Calculate the interval between every two requests.
    long costTime = Math.round(1.0 * (acquireCount) / count * 1000);

    //latestPassedTime代表的是上一次調用請求的時間
    // Expected pass time of this request.
    long expectedTime = costTime + latestPassedTime.get();
    //如果預期通過的時間加上上次的請求時間小於當前時間,則通過
    if (expectedTime <= currentTime) {
        // Contention may exist here, but it's okay.
        latestPassedTime.set(currentTime);
        return true;
    } else {
        //默認是maxQueueingTimeMs
        // Calculate the time to wait.
        long waitTime = costTime + latestPassedTime.get() - TimeUtil.currentTimeMillis();

        //如果預提時間比當前時間大maxQueueingTimeMs那麼多,那麼就阻塞
        if (waitTime > maxQueueingTimeMs) {
            return false;
        } else {
            //將上次時間加上這次請求要耗費的時間
            long oldTime = latestPassedTime.addAndGet(costTime);
            try {
                waitTime = oldTime - TimeUtil.currentTimeMillis();
                //再次判斷一下是否超過maxQueueingTimeMs設置的時間
                if (waitTime > maxQueueingTimeMs) {
                    //如果是的話就阻塞,並重置上次通過時間
                    latestPassedTime.addAndGet(-costTime);
                    return false;
                }
                //如果需要等待的時間大於零,那麼就sleep
                // in race condition waitTime may <= 0
                if (waitTime > 0) {
                    Thread.sleep(waitTime);
                }
                return true;
            } catch (InterruptedException e) {
            }
        }
    }
    return false;
}

這個方法一開始會計算一下costTime這個值,將請求平均分配到一秒中。例如:當 count 設爲 10 的時候,則代表一秒勻速的通過 10 個請求,也就是每個請求平均間隔恆定爲 1000 / 10 = 100 ms。

但是這裏有個小bug,如果count設置的比較大,比如設置成10000,那麼costTime永遠都會等於0,整個QPS限流將會失效。

然後會將costTime和上次的請求時間相加,如果大於當前時間就表明請求的太頻繁了,會將latestPassedTime這個屬性加上這次請求的costTime,並調用sleep方法讓這個線程先睡眠一會再請求。

這裏有個細節,如果多個請求同時一起過來,那麼每個請求在設置oldTime的時候都會通過addAndGet這個原子性的方法將latestPassedTime依次相加,並賦值給oldTime,這樣每個線程的sleep的時間都不會相同,線程也不會同時醒來。

WarmUpController限流 冷啓動

當系統長期處於低水位的情況下,當流量突然增加時,直接把系統拉昇到高水位可能瞬間把系統壓垮。通過"冷啓動",讓通過的流量緩慢增加,在一定時間內逐漸增加到閾值上限,給冷系統一個預熱的時間,避免冷系統被壓垮。

//默認爲3
private int coldFactor;
//轉折點的令牌數
protected int warningToken = 0;
//最大的令牌數
private int maxToken;
//斜線斜率
protected double slope;
//累積的令牌數
protected AtomicLong storedTokens = new AtomicLong(0);
//最後更新令牌的時間
protected AtomicLong lastFilledTime = new AtomicLong(0);

public WarmUpController(double count, int warmUpPeriodInSec, int coldFactor) {
    construct(count, warmUpPeriodInSec, coldFactor);
}

private void construct(double count, int warmUpPeriodInSec, int coldFactor) {

    if (coldFactor <= 1) {
        throw new IllegalArgumentException("Cold factor should be larger than 1");
    }

    this.count = count;
    //默認是3
    this.coldFactor = coldFactor;

    // thresholdPermits = 0.5 * warmupPeriod / stableInterval.
    // 10*20/2 = 100
    // warningToken = 100;
    warningToken = (int) (warmUpPeriodInSec * count) / (coldFactor - 1);
    // / maxPermits = thresholdPermits + 2 * warmupPeriod /
    // (stableInterval + coldInterval)
    // maxToken = 200
    maxToken = warningToken + (int) (2 * warmUpPeriodInSec * count / (1.0 + coldFactor));

    // slope
    // slope = (coldIntervalMicros - stableIntervalMicros) / (maxPermits
    // - thresholdPermits);
    slope = (coldFactor - 1.0) / count / (maxToken - warningToken);
}

我們接下來看看WarmUpController的canpass方法:

WarmUpController#canpass

public boolean canPass(Node node, int acquireCount, boolean prioritized) {
    //獲取當前時間窗口的流量大小
    long passQps = (long) node.passQps();
    //獲取上一個窗口的流量大小
    long previousQps = (long) node.previousPassQps();
    //設置 storedTokens 和 lastFilledTime 到正確的值
    syncToken(previousQps);

    // 開始計算它的斜率
    // 如果進入了警戒線,開始調整他的qps
    long restToken = storedTokens.get();
    if (restToken >= warningToken) {
        //通過計算當前的restToken和警戒線的距離來計算當前的QPS
        //離警戒線越接近,代表這個程序越“熱”,從而逐步釋放QPS
        long aboveToken = restToken - warningToken;
        //當前狀態下能達到的最高 QPS
        // current interval = restToken*slope+1/count
        double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));

        // 如果不會超過,那麼通過,否則不通過
        if (passQps + acquireCount <= warningQps) {
            return true;
        }
    } else {
        // count 是最高能達到的 QPS
        if (passQps + acquireCount <= count) {
            return true;
        }
    }
    return false;
}

這個方法裏通過syncToken(previousQps)設置storedTokens的值後,與警戒值做判斷,如果沒有達到警戒值,那麼通過計算和警戒值的距離再加上slope計算出一個當前的QPS值,storedTokens越大當前的QPS越小。

如果當前的storedTokens已經小於警戒值了,說明已經預熱完畢了,直接用count判斷就好了。

WarmUpController#syncToken

protected void syncToken(long passQps) {
    long currentTime = TimeUtil.currentTimeMillis();
    //去掉毫秒的時間
    currentTime = currentTime - currentTime % 1000;
    long oldLastFillTime = lastFilledTime.get();
    if (currentTime <= oldLastFillTime) {
        return;
    }

    // 令牌數量的舊值
    long oldValue = storedTokens.get();
    // 計算新的令牌數量,往下看
    long newValue = coolDownTokens(currentTime, passQps);

    if (storedTokens.compareAndSet(oldValue, newValue)) {
        // 令牌數量上,減去上一分鐘的 QPS,然後設置新值
        long currentValue = storedTokens.addAndGet(0 - passQps);
        if (currentValue < 0) {
            storedTokens.set(0L);
        }
        lastFilledTime.set(currentTime);
    } 
}

這個方法通過coolDownTokens方法來獲取一個新的value,然後通過CAS設置到storedTokens中,然後將storedTokens減去上一個窗口的QPS值,併爲lastFilledTime設置一個新的值。

private long coolDownTokens(long currentTime, long passQps) {
    long oldValue = storedTokens.get();
    long newValue = oldValue;

    // 添加令牌的判斷前提條件:
    // 當令牌的消耗程度遠遠低於警戒線的時候
    if (oldValue < warningToken) {
        // 根據count數每秒加上令牌
        newValue = (long) (oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
    } else if (oldValue > warningToken) {
        //如果還在冷啓動階段
        // 如果當前通過的 QPS 大於 count/coldFactor,說明系統消耗令牌的速度,大於冷卻速度
        //    那麼不需要添加令牌,否則需要添加令牌
        if (passQps < (int) count / coldFactor) {
            newValue = (long) (oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
        }
    }
    return Math.min(newValue, maxToken);
}

這個方法主要是用來做添加令牌的操作,如果是流量比較小或者是已經預熱完畢了,那麼就需要根據count數每秒加上令牌,如果是在預熱階段那麼就不進行令牌添加。

WarmUpRateLimiterController就是結合了冷啓動和勻速排隊,代碼非常的簡單。就不做分析了。

降級規則

當達到降級規則時,會觸發斷路器打開,進行降級。需要注意的是,sentinel斷路器沒有半開的功能。
在這裏插入圖片描述

  • RT

RT指的是平均響應時長。

在這裏插入圖片描述

  • 異常比例
    在這裏插入圖片描述
  • 異常數

需要注意異常數是分鐘基本的,所以時間窗口最好 > 60秒。
在這裏插入圖片描述

源碼解析

Sentinel的降級策略全部都是在DegradeSlot中進行操作的。

DegradeSlot

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

DegradeSlot會直接調用DegradeRuleManager進行降級的操作,我們直接進入到DegradeRuleManager.checkDegrade方法中。

DegradeRuleManager#checkDegrade

public static void checkDegrade(ResourceWrapper resource, Context context, DefaultNode node, int count)
    throws BlockException {
    //根據resource來獲取降級策略
    Set<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);
        }
    }
}

這個方法邏輯也是非常的清晰,首先是根據資源名獲取到註冊過的降級規則,然後遍歷規則集合調用規則的passCheck,如果返回false那麼就拋出異常進行降級。

DegradeRule#passCheck

public boolean passCheck(Context context, DefaultNode node, int acquireCount, Object... args) {
    //返回false直接進行降級
    if (cut.get()) {
        return false;
    }
    //降級是根據資源的全局節點來進行判斷降級策略的
    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;
        }
        //rtSlowRequestAmount默認是5
        // Sentinel will degrade the service only if count exceeds.
        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();
        // If total amount is less than minRequestAmount, the request will pass.
        if (total < minRequestAmount) {
            return true;
        }

        // In the same aligned statistic time window,
        // "success" (aka. completed count) = exception count + non-exception count (realSuccess)
        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;
        }
    }
    //根據設置的時間窗口進行重置
    if (cut.compareAndSet(false, true)) {
        ResetTask resetTask = new ResetTask(this);
        pool.schedule(resetTask, timeWindow, TimeUnit.SECONDS);
    }

    return false;
}

這個方法首先會去獲取cut的值,如果是true那麼就直接進行限流操作。然後會根據resource獲取ClusterNode全局節點。往下分別根據三種不同的策略來進行降級。

DEGRADE_GRADE_RT根據響應時間進行降級

if (grade == RuleConstant.DEGRADE_GRADE_RT) {
    //獲取節點的平均響應時間
    double rt = clusterNode.avgRt();
    if (rt < this.count) {
        passCount.set(0);
        return true;
    }
    //rtSlowRequestAmount默認是5
    // Sentinel will degrade the service only if count exceeds.
    if (passCount.incrementAndGet() < rtSlowRequestAmount) {
        return true;
    } 
}

如果是根據響應時間進行降級,那麼會獲取clusterNode的平均響應時間,如果平均響應時間大於所設定的count(默認是毫秒),那麼就調用passCount加1,如果passCount大於5,那麼直接降級。

所以看到這裏我們應該知道根據平均響應時間降級前幾個請求即使響應過長也不會立馬降級,而是要等到第六個請求到來纔會進行降級。

我們進入到clusterNode的avgRt方法中看一下是如何獲取到clusterNode的平均響應時間的。

clusterNode是StatisticNode的實例

StatisticNode#avgRt

public double avgRt() {
    //獲取當前時間窗口內調用成功的次數
    long successCount = rollingCounterInSecond.success();
    if (successCount == 0) {
        return 0;
    }
    //獲取窗口內的響應時間
    return rollingCounterInSecond.rt() * 1.0 / successCount;
}

這個方法主要是調用rollingCounterInSecond獲取成功次數,然後再獲取窗口內的響應時間,用總響應時間除以次數得到平均每次成功調用的響應時間。

我們再回到DegradeRule的passCheck方法中的響應時間降級策略中:

if (grade == RuleConstant.DEGRADE_GRADE_RT) {
    //獲取節點的平均響應時間
    double rt = clusterNode.avgRt();
    if (rt < this.count) {
        passCount.set(0);
        return true;
    }
    //rtSlowRequestAmount默認是5
    // Sentinel will degrade the service only if count exceeds.
    if (passCount.incrementAndGet() < rtSlowRequestAmount) {
        return true;
    }
    //    根據異常比例降級
}
//省略
return false;

如果求得的平均響應時間小於設置的count時間,那麼就重置passCount並返回true,表示不拋出異常;如果有連續5次的響應時間都超過了count,那麼就返回false拋出異常進行降級。

DEGRADE_GRADE_EXCEPTION_RATIO根據異常比例降級

if (grade == RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO) {
    //獲取每秒異常的次數
    double exception = clusterNode.exceptionQps();
    //獲取每秒成功的次數
    double success = clusterNode.successQps();
    //獲取每秒總調用次數
    double total = clusterNode.totalQps();
    // If total amount is less than minRequestAmount, the request will pass.
    // 如果總調用次數少於5,那麼不進行降級
    if (total < minRequestAmount) {
        return true;
    }

    // In the same aligned statistic time window,
    // "success" (aka. completed count) = exception count + non-exception count (realSuccess)
    double realSuccess = success - exception;
    if (realSuccess <= 0 && exception < minRequestAmount) {
        return true;
    }

    if (exception / success < count) {
        return true;
    } 
}
。。。
return false;

這個方法中獲取成功調用的Qps和異常調用的Qps,驗證後,然後求一下比率,如果沒有大於count,那麼就返回true,否則返回false拋出異常。

我們再進入到exceptionQps方法中看一下:

StatisticNode#exceptionQps

public double exceptionQps() {
    return rollingCounterInSecond.exception() / rollingCounterInSecond.getWindowIntervalInSec();
}

rollingCounterInSecond.getWindowIntervalInSec方法是表示窗口的時間長度,用秒來表示。這裏返回的是1。

ArrayMetric#exception

public long exception() {
    data.currentWindow();
    long exception = 0;
    List<MetricBucket> list = data.values();
    for (MetricBucket window : list) {
        exception += window.exception();
    }
    return exception;
}

這個方法和我上面分析的差不多,大家看看就好了。

根據異常數降級DEGRADE_GRADE_EXCEPTION_COUNT

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

根據異常數降級是非常的直接的,直接根據統計的異常總次數判斷是否超過count。

熱點規則

比如我們想要對一段時間內頻繁訪問的用戶 ID 進行限制,又或者我們想統計一段時間內最常購買的商品 ID 並針對商品 ID 進行限制。那這裏的用戶 ID 和商品 ID 都是可變的資源,通過原先的固定資源已經無法滿足我們的需求了,這時我們就可以通過 Sentinel 爲我們提供的 熱點參數限流 來達到這樣的效果。

熱點規則的配置,需要根據接口參數進行匹配規則。

    @GetMapping("/testHot")
    @SentinelResource("hot")
    public String testHot(@RequestParam(required = false) String a,
                          @RequestParam(required = false) String b) {
        return a + "" + b;
    }

在這裏插入圖片描述

  • 參數索引:0代表接口參數的第一個,1代表接口參數的第二個。
  • 單機閥值:代表單個接口的閥值。
  • 參數例外項:可以對參數的特殊值單獨配置。
源碼解析

熱點參數攔截需要sentinel-extension這個項目。它會幫你在責任鏈中增加com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowSlot。進行對熱點資源的限流。暫時不做分析。

系統規則

  • Load(系統負載)

    當系統load1(1分鐘的load)超過閥值,且併發線程數超過系統容量時觸發,建議設置爲CPU核心數*2.5。(僅對Linux/Unix-like 機器生效)。

  • RT
    所有入口流量的平均RT達到閥值觸發。

  • 線程數
    所有入口流量的併發線程數達到閥值觸發。

  • 入口QPS
    所有入口流量的QPS達到閥值觸發。

源碼解析

系統限流是在SystemSlot中處理的。

SystemSlot#entry

public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                  boolean prioritized, Object... args) throws Throwable {
	  //檢查一下是否符合限流條件,符合則進行限流
    SystemRuleManager.checkSystem(resourceWrapper);
    fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

SystemRuleManager#checkSystem

public static void checkSystem(ResourceWrapper resourceWrapper) throws BlockException {
    // Ensure the checking switch is on.
    if (!checkSystemStatus.get()) {
        return;
    }
    //如果不是入口流量,那麼直接返回
    // for inbound traffic only
    if (resourceWrapper.getType() != EntryType.IN) {
        return;
    }

    // total qps
    double currentQps = Constants.ENTRY_NODE == null ? 0.0 : Constants.ENTRY_NODE.successQps();
    if (currentQps > qps) {
        throw new SystemBlockException(resourceWrapper.getName(), "qps");
    }

    // total thread
    int currentThread = Constants.ENTRY_NODE == null ? 0 : Constants.ENTRY_NODE.curThreadNum();
    if (currentThread > maxThread) {
        throw new SystemBlockException(resourceWrapper.getName(), "thread");
    }

    double rt = Constants.ENTRY_NODE == null ? 0 : Constants.ENTRY_NODE.avgRt();
    if (rt > maxRt) {
        throw new SystemBlockException(resourceWrapper.getName(), "rt");
    }

    // load. BBR algorithm.
    if (highestSystemLoadIsSet && getCurrentSystemAvgLoad() > highestSystemLoad) {
        if (!checkBbr(currentThread)) {
            throw new SystemBlockException(resourceWrapper.getName(), "load");
        }
    }

    // cpu usage
    if (highestCpuUsageIsSet && getCurrentCpuUsage() > highestCpuUsage) {
        if (!checkBbr(currentThread)) {
            throw new SystemBlockException(resourceWrapper.getName(), "cpu");
        }
    }
}

這個方法首先會校驗一下checkSystemStatus狀態和EntryType是不是IN,如果不是則直接返回。

然後對Constants.ENTRY_NODE進行操作。這個對象是一個final static 修飾的變量,代表是全局對象。

public final static ClusterNode ENTRY_NODE = new ClusterNode();

所以這裏的限流操作都是對全局其作用的,而不是對資源起作用。ClusterNode還是繼承自StatisticNode,所以最後都是調用StatisticNode的successQps、curThreadNum、avgRt,這幾個方法前面已經介紹過了。

在下面調用getCurrentSystemAvgLoad方法和getCurrentCpuUsage方法調用到SystemStatusListener設置的全局變量currentLoad和currentCpuUsage。這兩個參數是SystemRuleManager的定時任務定時收集的。

在做load判斷和cpu usage判斷的時候會還會調用checkBbr方法來判斷:

private static boolean checkBbr(int currentThread) {
    if (currentThread > 1 &&
        currentThread > Constants.ENTRY_NODE.maxSuccessQps() * Constants.ENTRY_NODE.minRt() / 1000) {
        return false;
    }
    return true;
}

也就是說:當系統 load1 超過閾值,且系統當前的併發線程數超過系統容量時纔會觸發系統保護。系統容量由系統的 maxQps * minRt 計算得出。

StatisticNode#maxSuccessQps

public double maxSuccessQps() {
    return rollingCounterInSecond.maxSuccess() * rollingCounterInSecond.getSampleCount();
}

maxSuccessQps方法是用窗口內的最大成功調用數和窗口數量相乘rollingCounterInSecond的窗口1秒的窗口數量是2,最大成功調用數如下得出:

ArrayMetric#maxSuccess

public long maxSuccess() {
    data.currentWindow();
    long success = 0;

    List<MetricBucket> list = data.values();
    for (MetricBucket window : list) {
        if (window.success() > success) {
            success = window.success();
        }
    }
    return Math.max(success, 1);
}

最大成功調用數是通過整個遍歷整個窗口,獲取所有窗口裏面最大的調用數。所以這樣的最大的併發量是一個預估值,不是真實值。

授權規則

授權規則是指定資源訪問的權限控制規則。流控應用指的是方法當前資源的服務。白名單指可以進行訪問,黑名單是不可以進行訪問。
在這裏插入圖片描述

源碼解析

AuthorizationSlot則根據黑白名單,來做黑白名單控制;
如果該resource配置了AuthorityRule,則根據策略判斷該資源請求的請求來源(origin)是否在配置規則LimitApp中((,)隔開)和策略判斷,是否檢查通過。

  • 如果是白名單
    判斷origin是否在limitApp中,如果在,則返回true,否則返回false
  • 如果爲黑名單
    判斷origin是否在limitApp中,如果在,則返回false,否則返回true
public class AuthoritySlot extends AbstractLinkedProcessorSlot<DefaultNode> {
    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args)
        throws Throwable {
        //檢查黑白名單
        checkBlackWhiteAuthority(resourceWrapper, context);
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }

    @Override
    public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
        fireExit(context, resourceWrapper, count, args);
    }

    void checkBlackWhiteAuthority(ResourceWrapper resource, Context context) throws AuthorityException {
        //獲取認證的規則
        Map<String, List<AuthorityRule>> authorityRules = AuthorityRuleManager.getAuthorityRules();
        if (authorityRules == null) {
            return;
        }
        //根據resourceName獲取該資源下對應的規則
        List<AuthorityRule> rules = authorityRules.get(resource.getName());
        if (rules == null) {
            return;
        }
        for (AuthorityRule rule : rules) {
            //認證檢查
            if (!AuthorityRuleChecker.passCheck(rule, context)) {
                throw new AuthorityException(context.getOrigin(), rule);
            }
        }
    }
}

檢查邏輯在AuthorityRuleChecker:

final class AuthorityRuleChecker {

    static boolean passCheck(AuthorityRule rule, Context context) {

        String requester = context.getOrigin();
        // 獲取orgin請求來源,如果爲請求來源爲null或者limitApp爲null則直接返回通過
        if (StringUtil.isEmpty(requester) || StringUtil.isEmpty(rule.getLimitApp())) {
            return true;
        }

        //判斷limitApp是否含有origin
        int pos = rule.getLimitApp().indexOf(requester);
        boolean contain = pos > -1;
        if (contain) {
            boolean exactlyMatch = false;
            String[] appArray = rule.getLimitApp().split(",");
            for (String app : appArray) {
                if (requester.equals(app)) {
                    exactlyMatch = true;
                    break;
                }
            }

            contain = exactlyMatch;
        }
        //根據策略處理是否包含,判斷是否通過
        int strategy = rule.getStrategy();
        if (strategy == RuleConstant.AUTHORITY_BLACK && contain) {
            return false;
        }

        if (strategy == RuleConstant.AUTHORITY_WHITE && !contain) {
            return false;
        }
        return true;
    }

    private AuthorityRuleChecker() {}
}

AuthorityRule的配置更新和SystemSlot一樣,更新依賴於AuthorityRuleManager的loadRules方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章