來自阿里內(nèi)部員工 Sentinel 源碼解析非常細(xì)致

本文介紹阿里開源的 Sentinel 源碼鄙煤,GitHub: alibaba/Sentinel耻煤,基于當(dāng)前(2019-10-21)最新的 release 版本 1.7.0具壮。

總體來說,Sentinel 的源碼比較簡單哈蝇,復(fù)雜的部分在于它的模型對于初學(xué)者來說不好理解棺妓。

雖然本文不是很長,最后兩節(jié)還和主流程無關(guān)炮赦,但是怜跑,本文對于源碼分析已經(jīng)非常細(xì)致了。

閱讀建議:在閱讀本文前吠勘,你應(yīng)該至少了解過 Sentinel 是什么性芬,如果使用過 Sentinel 或已經(jīng)閱讀過部分源碼那就更好了。

另外剧防,本文不涉及到集群流控植锉。由于很多讀者也沒使用過 Hystrix,所以本文也不做任何對比峭拘。

更新 2019-12-11:更新了滑動(dòng)窗口秒級數(shù)據(jù)統(tǒng)計(jì) OccupiableBucketLeapArray 的分析俊庇。

簡介

Sentinel 的定位是流量控制狮暑、熔斷降級,你應(yīng)該把它理解為一個(gè)第三方 Jar 包暇赤。

這個(gè) Jar 包會(huì)進(jìn)行流量統(tǒng)計(jì)心例,執(zhí)行流量控制規(guī)則。而統(tǒng)計(jì)數(shù)據(jù)的展示和規(guī)則的設(shè)置在 sentinel-dashboard 項(xiàng)目中鞋囊,這是一個(gè) Spring MVC 應(yīng)用止后,有后臺管理界面,我們通過這個(gè)管理后臺和各個(gè)應(yīng)用進(jìn)行交互溜腐。

當(dāng)然译株,你不一定需要 dashboard,很長一段時(shí)間挺益,我僅僅使用 sentinel-core歉糜,它會(huì)將統(tǒng)計(jì)信息寫入到指定的文件中,我通過該文件內(nèi)容來了解每個(gè)接口的流量情況望众。當(dāng)然匪补,這種情況下,我只是使用到了 Sentinel 的流量監(jiān)控功能而已烂翰。

image

從左側(cè)我們可以看到這個(gè) dashboard 可以管理很多應(yīng)用夯缺,而對于每個(gè)應(yīng)用,我們還可以有很多機(jī)器實(shí)例(見機(jī)器列表)甘耿。我們在這個(gè)后臺踊兜,可以非常直觀地了解到每個(gè)接口的 QPS 數(shù)據(jù),我們可以對每個(gè)接口設(shè)置流量控制規(guī)則佳恬、降級規(guī)則等捏境。

這個(gè) dashboard 默認(rèn)是不持久化數(shù)據(jù)的,它的所有數(shù)據(jù)都是在內(nèi)存中的毁葱,所以 dashboard 重啟意味著所有的數(shù)據(jù)都會(huì)丟失垫言。你應(yīng)該按照自己的需要來使用 dashboard,如至少你應(yīng)該要持久化規(guī)則設(shè)置头谜,QPS 數(shù)據(jù)非常適合存放在時(shí)序數(shù)據(jù)庫中骏掀,當(dāng)然如果你的數(shù)據(jù)量不大,存 MySQL 也問題不大柱告,定期清理一下過期數(shù)據(jù)即可,因?yàn)榇蟛糠秩藨?yīng)該不會(huì)關(guān)心一個(gè)月以前的 QPS 數(shù)據(jù)笑陈。

sentinel-dashboard 并沒有定位為一個(gè)功能強(qiáng)大的管理后臺际度,一般來說,我們需要基于它來進(jìn)行二次開發(fā)涵妥,甚至于你也可以不使用這個(gè) Java 項(xiàng)目乖菱,自己使用其他的語言來實(shí)現(xiàn)。在最后一小節(jié),我介紹了業(yè)務(wù)應(yīng)用是怎么和 dashboard 應(yīng)用交互的窒所。

Sentinel 的數(shù)據(jù)統(tǒng)計(jì)

在正式開始介紹 Sentinel 的流程源碼之前鹉勒,我想先和大家介紹一下 Sentinel 的數(shù)據(jù)統(tǒng)計(jì)模塊的內(nèi)容,這樣讀者在后面看到相應(yīng)的內(nèi)容的時(shí)候心里有一些底吵取。這節(jié)內(nèi)容還是比較簡單的禽额,當(dāng)然,如果你希望立馬進(jìn)入 Sentinel 的主流程皮官,可以先跳過這一節(jié)脯倒。

Sentinel 的定位是流量控制,它有兩個(gè)維度的控制捺氢,一個(gè)是控制并發(fā)線程數(shù)藻丢,另一個(gè)是控制 QPS,它們都是針對某個(gè)具體的接口來設(shè)置的摄乒,其實(shí)說資源比較準(zhǔn)確悠反,Sentinel 把控制的粒度定義為 Resource。

既然要做控制馍佑,那么首先斋否,Sentinel 就要先做統(tǒng)計(jì),它要知道當(dāng)前接口的 QPS 和并發(fā)是多少挤茄,進(jìn)而判斷一個(gè)新的請求能不能讓它通過如叼。

數(shù)據(jù)統(tǒng)計(jì)的代碼在 StatisticNode 中,對于 QPS 數(shù)據(jù)穷劈,它使用了滑動(dòng)窗口的設(shè)計(jì):

<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">
private transient volatile Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT,
    IntervalProperty.INTERVAL);

private transient Metric rollingCounterInMinute = new ArrayMetric(60, 60 * 1000, false);

private AtomicInteger curThreadNum = new AtomicInteger(0);

AtomicInteger 用于統(tǒng)計(jì)并發(fā)量(curThreadNum)非常簡單笼恰,就是原子加、原子減的操作歇终,這里不浪費(fèi)篇幅了社证,下面僅介紹 QPS 的統(tǒng)計(jì)。

從上面的代碼可以知道评凝,Sentinel 統(tǒng)計(jì)了 兩個(gè)維度的數(shù)據(jù)追葡,下面我們簡單說說實(shí)現(xiàn)類 ArrayMetric 的源碼設(shè)計(jì)。

<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">

public class ArrayMetric implements Metric {

    private final LeapArray<MetricBucket> data;

    public ArrayMetric(int sampleCount, int intervalInMs) {
        this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
    }

    public ArrayMetric(int sampleCount, int intervalInMs, boolean enableOccupy) {
        if (enableOccupy) {
            this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
        } else {
            this.data = new BucketLeapArray(sampleCount, intervalInMs);
        }
    }
    ......
}

ArrayMetric 的內(nèi)部是一個(gè) LeapArray奕短,我們以分鐘維度統(tǒng)計(jì)的使用來說宜肉,它使用子類 BucketLeapArray 實(shí)現(xiàn)。

這里先介紹較為簡單的 BucketLeapArray 的實(shí)現(xiàn)翎碑,然后在最后一節(jié)會(huì)介紹 OccupiableBucketLeapArray谬返。

<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">

public abstract class LeapArray<T> {

    protected int windowLengthInMs;
    protected int sampleCount;
    protected int intervalInMs;

    protected final AtomicReferenceArray<WindowWrap<T>> array;

    // 對于分鐘維度的設(shè)置,sampleCount 為 60日杈,intervalInMs 為 60 * 1000
    public LeapArray(int sampleCount, int intervalInMs) {
                // 單個(gè)窗口長度遣铝,這里是 1000ms
        this.windowLengthInMs = intervalInMs / sampleCount;
        // 一輪總時(shí)長 60,000 ms
        this.intervalInMs = intervalInMs;
        // 60 個(gè)窗口
        this.sampleCount = sampleCount;

        this.array = new AtomicReferenceArray<>(sampleCount);
    }
    // ......
}

它的內(nèi)部核心是一個(gè)數(shù)組 array佑刷,它的長度為 60,也就是有 60 個(gè)窗口酿炸,每個(gè)窗口長度為 1 秒瘫絮,剛好一分鐘走完一輪。然后下一輪開啟“覆蓋”操作填硕。

image

每個(gè)窗口是一個(gè) WindowWrap 類實(shí)例麦萤。

  • 添加數(shù)據(jù)的時(shí)候,先判斷當(dāng)前走到哪個(gè)窗口了(當(dāng)前時(shí)間(s) % 60 即可)廷支,然后需要判斷這個(gè)窗口是否是過期數(shù)據(jù)频鉴,如果是過期數(shù)據(jù)(窗口代表的時(shí)間距離當(dāng)前已經(jīng)超過 1 分鐘),需要先重置這個(gè)窗口實(shí)例的數(shù)據(jù)恋拍。
  • 統(tǒng)計(jì)數(shù)據(jù)同理垛孔,如統(tǒng)計(jì)過去一分鐘的 QPS 數(shù)據(jù),就是將每個(gè)窗口的值相加施敢,當(dāng)中需要判斷窗口數(shù)據(jù)是否是過期數(shù)據(jù)周荐,即判斷窗口的 WindowWrap 實(shí)例是否是一分鐘內(nèi)的數(shù)據(jù)。

核心邏輯都封裝在了 currentWindow(long timeMillis) 和 values(long timeMillis)方法中僵娃。

添加數(shù)據(jù)的時(shí)候概作,我們要先獲取操作的目標(biāo)窗口,也就是 currentWindow 這個(gè)方法默怨,Sentinel 在這里處理初始化和過期重置的情況:

<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">

public WindowWrap<T> currentWindow(long timeMillis) {
    if (timeMillis < 0) {
        return null;
    }
    // 獲取窗口下標(biāo)
    int idx = calculateTimeIdx(timeMillis);
    // 計(jì)算該窗口的理論開始時(shí)間
    long windowStart = calculateWindowStart(timeMillis);

    // 嵌套在一個(gè)循環(huán)中讯榕,因?yàn)橛胁l(fā)的情況
    while (true) {
        WindowWrap<T> old = array.get(idx);
        if (old == null) {
            // 窗口未實(shí)例化的情況,使用一個(gè) CAS 來設(shè)置該窗口實(shí)例
            WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
            if (array.compareAndSet(idx, null, window)) {
                return window;
            } else {
                // 存在競爭
                Thread.yield();
            }
        } else if (windowStart == old.windowStart()) {
            // 當(dāng)前數(shù)組中的窗口沒有過期
            return old;
        } else if (windowStart > old.windowStart()) {
            // 該窗口已過期匙睹,重置窗口的值愚屁。使用一個(gè)鎖來控制并發(fā)。
            if (updateLock.tryLock()) {
                try {
                    return resetWindowTo(old, windowStart);
                } finally {
                    updateLock.unlock();
                }
            } else {
                Thread.yield();
            }
        } else if (windowStart < old.windowStart()) {
            // 正常情況都不會(huì)走到這個(gè)分支痕檬,異常情況其實(shí)就是時(shí)鐘回?fù)荟保@里返回一個(gè) WindowWrap 是容錯(cuò)
            return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
        }
    }
}

獲取數(shù)據(jù),使用的是 values 方法梦谜,這個(gè)方法返回“有效的”窗口中的數(shù)據(jù):

<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">

public List<T> values(long timeMillis) {
    if (timeMillis < 0) {
        return new ArrayList<T>();
    }
    int size = array.length();
    List<T> result = new ArrayList<T>(size);

    for (int i = 0; i < size; i++) {
        WindowWrap<T> windowWrap = array.get(i);
        // 過濾掉過期數(shù)據(jù)
        if (windowWrap == null || isWindowDeprecated(timeMillis, windowWrap)) {
            continue;
        }
        result.add(windowWrap.value());
    }
    return result;
}

// 判斷當(dāng)前窗口的數(shù)據(jù)是否是 60 秒內(nèi)的
public boolean isWindowDeprecated(long time, WindowWrap<T> windowWrap) {
    return time - windowWrap.windowStart() > intervalInMs;
}

這個(gè) values 方法很簡單丘跌,就是過濾掉那些過期數(shù)據(jù)就可以了。

到這里唁桩,我們就說完了 維度數(shù)據(jù)統(tǒng)計(jì)的問題闭树。至于秒維度的數(shù)據(jù)統(tǒng)計(jì),有些不一樣荒澡,稍微復(fù)雜一些蔼啦,我在后面單獨(dú)起了一節(jié)。跳過這部分內(nèi)容對閱讀 Sentinel 源碼沒有影響仰猖。

Sentinel 源碼分析

下面捏肢,我們正式開始 Sentinel 的源碼介紹。

官方文檔中饥侵,它的最簡單的使用是下面這樣的鸵赫,這里用了 try-with-resource 的寫法:

<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">

try (Entry entry = SphU.entry("HelloWorld")) {
    // Your business logic here.
    System.out.println("hello world");
} catch (BlockException e) {
    // Handle rejected request.
    e.printStackTrace();
}

這個(gè)例子對于理解源碼其實(shí)不是很好,我們來寫一個(gè)復(fù)雜一些的例子躏升,這樣對理解源碼有很大的幫助:

image

1辩棒、紅色部分,Context 代表一個(gè)調(diào)用鏈的入口膨疏,Context 實(shí)例設(shè)置在 ThreadLocal 中一睁,所以它是跟著線程走的,如果要切換線程佃却,需要手動(dòng)切換者吁。ContextUtil#enter 有兩個(gè)參數(shù):

第一個(gè)參數(shù)是 context name,它代表調(diào)用鏈的入口饲帅,作用是為了區(qū)分不同的調(diào)用鏈路复凳,個(gè)人感覺沒什么用,默認(rèn)是 Constants.CONTEXT_DEFAULT_NAME 的常量值 "sentinel_default_context"灶泵;

第二個(gè)參數(shù)代表調(diào)用方標(biāo)識 origin育八,目前它有兩個(gè)作用,一是用于黑白名單的授權(quán)控制赦邻,二是可以用來統(tǒng)計(jì)諸如從應(yīng)用 application-a 發(fā)起的對當(dāng)前應(yīng)用 interfaceXxx() 接口的調(diào)用髓棋,目前這個(gè)數(shù)據(jù)會(huì)被統(tǒng)計(jì),但是 dashboard 中并不展示惶洲。

2按声、進(jìn)入 BlockException 異常分支,代表該次請求被流量控制規(guī)則限制了湃鹊,我們一般會(huì)讓代碼走入到熔斷降級的邏輯里面儒喊。當(dāng)然,BlockException 其實(shí)有好多個(gè)子類币呵,如 DegradeException怀愧、FlowException 等,我們也可以 catch 具體的子類來進(jìn)行處理余赢。

3芯义、Entry 是我們的重點(diǎn),對于 SphU#entry 方法:

第一個(gè)參數(shù)標(biāo)識資源妻柒,通常就是我們的接口標(biāo)識扛拨,對于數(shù)據(jù)統(tǒng)計(jì)、規(guī)則控制等举塔,我們一般都是在這個(gè)粒度上進(jìn)行的绑警,根據(jù)這個(gè)字符串來唯一標(biāo)識求泰,它會(huì)被包裝成 ResourceWrapper 實(shí)例,大家要先看下它的 hashCode 和 equals 方法计盒;

第二個(gè)參數(shù)標(biāo)識資源的類型渴频,我們左邊的代碼使用了 EntryType.IN 代表這個(gè)是入口流量,比如我們的接口對外提供服務(wù)北启,那么我們通常就是控制入口流量卜朗;EntryType.OUT 代表出口流量,比如上面的 getOrderInfo 方法(沒寫默認(rèn)就是 OUT)咕村,它的業(yè)務(wù)需要調(diào)用訂單服務(wù)场钉,像這種情況,壓力其實(shí)都在訂單服務(wù)中懈涛,那么我們就指定它為出口流量逛万。這個(gè)流量類型有什么用呢?答案在 SystemSlot 類中肩钠,它用于實(shí)現(xiàn)自適應(yīng)限流泣港,根據(jù)系統(tǒng)健康狀態(tài)來判斷是否要限流,如果是 OUT 類型价匠,由于壓力在外部系統(tǒng)中当纱,所以就不需要執(zhí)行這個(gè)規(guī)則。

4踩窖、上面的代碼坡氯,我們在 getOrderInfo 中嵌套使用了 Entry,也是為了我們后面的源碼分析需要洋腮。如果我們在一個(gè)方法中寫的話箫柳,要注意內(nèi)層的 Entry 先 exit,才能做外層的 exit啥供,否則會(huì)拋出異常悯恍。源碼角度來看,是在 Context 實(shí)例中伙狐,保存了當(dāng)前的 Entry 實(shí)例涮毫。

5、實(shí)際開發(fā)過程中贷屎,我們當(dāng)然不會(huì)每個(gè)接口都像上面的代碼這么寫罢防,Sentinel 提供了很多的擴(kuò)展和適配器,這里只是為了源碼分析的需要唉侄。

Sentinel 提供了很多的 adapter 用于諸如 dubbo咒吐、grpc、網(wǎng)關(guān)等環(huán)境,它們其實(shí)都是封裝了上述的代碼恬叹。你只要認(rèn)真看完本文候生,那些包裝都很容易看懂。

image

這里我們介紹了 Sentinel 的接口使用妄呕,不過它的類名字我現(xiàn)在都沒懂是什么意思陶舞,SphU、CtSph绪励、CtEntry 這些名字有什么特殊含義,有知道的讀者請不吝賜教唠粥。

下面疏魏,我們按照上面的代碼,開始源碼分析晤愧。這里我不會(huì)像之前分析 Spring IOC 和 Netty 源碼一樣大莫,一行一行代碼說,所以大家一定要打開源碼配合著看官份。

ContextUtil#enter

我們先看 Context#enter 方法只厘,這行代碼我們是可以不寫的,下面我們就會(huì)看到舅巷,如果我們不顯式調(diào)用這個(gè)方法羔味,那么會(huì)進(jìn)入到默認(rèn)的 context 中。

<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">

ContextUtil.enter("user-center", "app-A"); 

進(jìn)入到 ContextUtil 類钠右,大家可能會(huì)漏看它的 static 代碼塊赋元,這里會(huì)添加一個(gè)默認(rèn)的 EntranceNode 實(shí)例。

然后上面的這個(gè)方法會(huì)走到 ContextUtil#trueEnter 中飒房,這里會(huì)添加名為 "user-center" 的 EntranceNode 節(jié)點(diǎn)搁凸。根據(jù)源碼,我們可以得出下面這棵樹:

image

這里的源碼非常簡單狠毯,如果我們從來不顯式調(diào)用 ContextUtil#enter 方法的話护糖,那 root 就只有一個(gè) default 子節(jié)點(diǎn)。

context 很好理解嚼松,它代表線程執(zhí)行的上下文嫡良,在各種開源框架中都有類似的語義,在 Sentinel 中惜颇,我們可以看到皆刺,對于一個(gè)新的 context name,Sentinel 會(huì)往樹中添加一個(gè) EntranceNode 實(shí)例凌摄。它的作用是為了區(qū)分調(diào)用鏈路羡蛾,標(biāo)識調(diào)用入口。在 sentinel-board 中锨亏,我們可以很直觀地看出調(diào)用鏈路:

image

SphU#entry

接下來痴怨,我們看 SphU#entry忙干。自己跟進(jìn)去,我們會(huì)來到 CtSph#entryWithPriority 方法浪藻,這個(gè)方法是 Sentinel 的骨架捐迫,非常重要。

<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">

private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
    throws BlockException {
    // 從 ThreadLocal 中獲取 Context 實(shí)例
    Context context = ContextUtil.getContext();
    // 如果是 NullContext爱葵,那么說明 context name 超過了 2000 個(gè)施戴,參見 ContextUtil#trueEnter
    // 這個(gè)時(shí)候,Sentinel 不再接受處理新的 context 配置萌丈,也就是不做這些新的接口的統(tǒng)計(jì)赞哗、限流熔斷等
    if (context instanceof NullContext) {
        return new CtEntry(resourceWrapper, null, context);
    }

    // 我們前面說了,如果我們不顯式調(diào)用 ContextUtil#enter辆雾,這里會(huì)進(jìn)入到默認(rèn)的 context 中
    if (context == null) {
        context = MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME, "", resourceWrapper.getType());
    }

    // Sentinel 的全局開關(guān)肪笋,Sentinel 提供了接口讓用戶可以在 dashboard 開啟/關(guān)閉
    if (!Constants.ON) {
        return new CtEntry(resourceWrapper, null, context);
    }

    // 設(shè)計(jì)模式中的責(zé)任鏈模式。
    // 下面這行代碼用于構(gòu)建一個(gè)責(zé)任鏈度迂,入?yún)⑹?resource藤乙,前面我們說過資源的唯一標(biāo)識是 resource name
    ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);

    // 根據(jù) lookProcessChain 方法,我們知道惭墓,當(dāng) resource 超過 Constants.MAX_SLOT_CHAIN_SIZE坛梁,
    // 也就是 6000 的時(shí)候,Sentinel 開始不處理新的請求诅妹,這么做主要是為了 Sentinel 的性能考慮
    if (chain == null) {
        return new CtEntry(resourceWrapper, null, context);
    }

    // 執(zhí)行這個(gè)責(zé)任鏈罚勾。如果拋出 BlockException,說明鏈上的某一環(huán)拒絕了該請求吭狡,
    // 把這個(gè)異常往上層業(yè)務(wù)層拋粘茄,業(yè)務(wù)層處理 BlockException 應(yīng)該進(jìn)入到熔斷降級邏輯中
    Entry e = new CtEntry(resourceWrapper, chain, context);
    try {
        chain.entry(context, resourceWrapper, null, count, prioritized, args);
    } catch (BlockException e1) {
        e.exit(count, args);
        throw e1;
    } catch (Throwable e1) {
        // This should not happen, unless there are errors existing in Sentinel internal.
        RecordLog.info("Sentinel unexpected exception", e1);
    }
    return e;
}

這里說一說 lookProcessChain(resourceWrapper) 這個(gè)方法偷溺。Sentinel 的處理核心都在這個(gè)責(zé)任鏈中护锤,鏈中每一個(gè)節(jié)點(diǎn)是一個(gè) Slot 實(shí)例蹭劈,這個(gè)鏈通過異常來告知調(diào)用入口最終的執(zhí)行情況。

大家自己點(diǎn)進(jìn)去源碼弛秋,這個(gè)責(zé)任鏈由 SlotChainProvider#newSlotChain 生產(chǎn)器躏,Sentinel 提供了 SPI 端點(diǎn),讓我們可以自己定制 Builder蟹略,如添加一個(gè) Slot 進(jìn)去登失。由于 SlotChainBuilder 接口設(shè)計(jì)的問題,我們只能全局所有的 resource 使用相同的責(zé)任鏈配置挖炬。

<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">

public class DefaultSlotChainBuilder implements SlotChainBuilder {

    @Override
    public ProcessorSlotChain build() {
        ProcessorSlotChain chain = new DefaultProcessorSlotChain();
        chain.addLast(new NodeSelectorSlot());
        chain.addLast(new ClusterBuilderSlot());
        chain.addLast(new LogSlot());
        chain.addLast(new StatisticSlot());
        chain.addLast(new AuthoritySlot());
        chain.addLast(new SystemSlot());
        chain.addLast(new FlowSlot());
        chain.addLast(new DegradeSlot());
        return chain;
    }
}

接下來揽浙,我們就按照默認(rèn)的 DefaultSlotChainBuilder 生成的責(zé)任鏈往下看源碼。

這里要強(qiáng)調(diào)一點(diǎn),對于相同的 resource馅巷,使用同一個(gè)責(zé)任鏈實(shí)例膛虫,不同的 resource,使用不同的責(zé)任鏈實(shí)例钓猬。

NodeSelectorSlot

image

首先稍刀,鏈中第一個(gè)處理節(jié)點(diǎn)是 NodeSelectorSlot。

<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">
// key 是 context name, value 是 DefaultNode 實(shí)例
private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(10);

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
    throws Throwable {
    DefaultNode node = map.get(context.getName());
    if (node == null) {
        synchronized (this) {
            node = map.get(context.getName());
            if (node == null) {
                node = new DefaultNode(resourceWrapper, null);
                HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size());
                cacheMap.putAll(map);
                cacheMap.put(context.getName(), node);
                map = cacheMap;
                // Build invocation tree
                ((DefaultNode) context.getLastNode()).addChild(node);
            }

        }
    }

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

</pre>

我們前面說了敞曹,責(zé)任鏈實(shí)例和 resource name 相關(guān)账月,和線程無關(guān),所以當(dāng)處理同一個(gè) resource 的時(shí)候异雁,會(huì)進(jìn)入到同一個(gè) NodeSelectorSlot 實(shí)例中捶障。

所以這塊代碼主要就是要處理:不同的 context name,同一個(gè) resource name 的情況纲刀。如:

image

上面的代碼示例了同一個(gè)資源 getUserInfo,在兩個(gè) context name 中進(jìn)入担平。然后我們再結(jié)合前面的那棵樹示绊,我們可以得出下面這棵樹:

image

NodeSelectorSlot 還是比較簡單的,只要讀者搞清楚 NodeSelectorSlot 實(shí)例是跟著 resource 一一對應(yīng)的就很清楚了暂论。

ClusterBuilderSlot

image

接下來面褐,我們來到了 ClusterBuilderSlot 這一環(huán),這一環(huán)的主要作用是構(gòu)建 ClusterNode取胎。

這里不貼源碼展哭,根據(jù)上面的樹,然后在經(jīng)過該類的處理以后闻蛀,我們可以得出下面這棵樹:

image

從上圖可以看到匪傍,對于每一個(gè) resource,這里會(huì)對應(yīng)一個(gè) ClusterNode 實(shí)例觉痛,如果不存在役衡,就創(chuàng)建一個(gè)實(shí)例。

這個(gè) ClusterNode 非常有用薪棒,因?yàn)槲覀兙褪鞘褂盟鼇碜鰯?shù)據(jù)統(tǒng)計(jì)的手蝎。比如 getUserInfo 這個(gè)接口,由于從不同的 context name 中開啟調(diào)用鏈俐芯,它有多個(gè) DefaultNode 實(shí)例棵介,但是只有一個(gè) ClusterNode,通過這個(gè)實(shí)例吧史,我們可以知道這個(gè)接口現(xiàn)在的 QPS 是多少邮辽。

另外,這個(gè)類還處理了 origin 不是默認(rèn)值的情況:

<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">

if (!"".equals(context.getOrigin())) {
    Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin());
    context.getCurEntry().setOriginNode(originNode);
}

我們可以看到,當(dāng)設(shè)置了 origin 的時(shí)候逆巍,會(huì)額外生成一個(gè) StatisticsNode 實(shí)例及塘,掛在 ClusterNode 上。

我們把前面的代碼改改锐极,看紅色部分:

image

我們的 getUserInfo 接收到了來自 application-aapplication-b 兩個(gè)應(yīng)用的請求笙僚,那么樹會(huì)變成下面這樣:

image

它的作用是用來統(tǒng)計(jì)從 application-a 過來的訪問 getUserInfo 這個(gè)接口的信息。目前這個(gè)信息在 dashboard 中是不展示的灵再,畢竟也沒什么用肋层。

LogSlot

image

這個(gè)類比較簡單,我們看到它直接 fire 出去了翎迁,也就是說栋猖,先處理責(zé)任鏈上后面的那些節(jié)點(diǎn),如果它們拋出了 BlockException汪榔,那么這里才做處理蒲拉。

image

這里調(diào)用了 EagleEyeLogUtil#log 方法,它其實(shí)就是痴腌,將被設(shè)置的規(guī)則 block 的信息記錄到日志文件 sentinel-block.log 中雌团。

StatisticSlot

image

這個(gè) slot 非常重要,它負(fù)責(zé)進(jìn)行數(shù)據(jù)統(tǒng)計(jì)士聪。

它也是先 fire 出去锦援,等后面的節(jié)點(diǎn)處理完畢以后,它再進(jìn)行統(tǒng)計(jì)數(shù)據(jù)剥悟。之所以這么設(shè)計(jì)灵寺,是因?yàn)楹竺娴墓?jié)點(diǎn)是做控制的,執(zhí)行的時(shí)候可能是正常通過的区岗,也可能是拋出 BlockException 異常的略板。

源碼非常簡單,對于 QPS 統(tǒng)計(jì)躏尉,使用前面介紹的滑動(dòng)窗口蚯根,而對于線程并發(fā)的統(tǒng)計(jì),它使用了 LongAdder胀糜。

大家一定要看一遍這個(gè)類的源碼颅拦,這里沒有什么特別的內(nèi)容需要強(qiáng)調(diào),所以我就不展開說了教藻。

接下來距帅,我們后面要介紹的幾個(gè) Slot,需要通過 dashboard 進(jìn)行開啟括堤,因?yàn)樾枰渲靡?guī)則碌秸。

AuthoritySlot

image

這個(gè)類非常簡單绍移,根據(jù) origin 做黑白名單的控制:

image

在 dashboard 中,是這么配置的:

image

這里的調(diào)用方就是我們前面介紹的 origin讥电。

SystemSlot

image
image

規(guī)則校驗(yàn)都在 SystemRuleManager#checkSystem 中:

image

我們先說說上面的代碼中的 RT蹂窖、線程數(shù)、入口 QPS 這三項(xiàng)系統(tǒng)保護(hù)規(guī)則恩敌。dashboard 配置界面:

image

在前面介紹的 StatisticSlot 類中瞬测,有下面一段代碼:

image

Sentinel 針對所有的入口流量,使用了一個(gè)全局的 ENTRY_NODE 進(jìn)行統(tǒng)計(jì)纠炮,所以我們也要知道月趟,系統(tǒng)保護(hù)規(guī)則是全局的,和具體的某個(gè)資源沒有關(guān)系恢口。

由于系統(tǒng)的平均 RT孝宗、當(dāng)前線程數(shù)、QPS 都可以從 ENTRY_NODE 中獲得耕肩,所以限制代碼非常簡單因妇,比較一下大小就可以了。如果超過閾值猿诸,拋出 SystemBlockException沙峻。

ENTRY_NODE 是 ClusterNode 類型的,而 ClusterNode 對于 rt两芳、qps 都是統(tǒng)計(jì)的維度的數(shù)據(jù)。

當(dāng)然去枷,對于 SystemSlot 類來說怖辆,最重要的其實(shí)并不是上面的這些,因?yàn)樵趯?shí)際使用過程中删顶,對于 RT竖螃、線程數(shù)、QPS 每一項(xiàng)逗余,我們其實(shí)都很難設(shè)置一個(gè)確定的閾值特咆。

我們往下看它的對于系統(tǒng)負(fù)載和 CPU 資源的保護(hù):

image

我們可以看到,Sentinel 通過調(diào)用 MBean 中的方法獲取當(dāng)前的系統(tǒng)負(fù)載和 CPU 使用率录粱,Sentinel 起了一個(gè)后臺線程腻格,每秒查詢一次。

<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">

OperatingSystemMXBean osBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class);

currentLoad = osBean.getSystemLoadAverage();

currentCpuUsage = osBean.getSystemCpuLoad();

</pre>

下圖展示 dashboard 中對于 CPU 使用率的規(guī)則配置:

image

FlowSlot

image

Flow Control 是 Sentinel 的核心啥繁, 因?yàn)?Sentinel 本身定位就是一個(gè)流控工具菜职,所以 FlowSlot 非常重要。

對于讀者來說旗闽,最大的挑戰(zhàn)應(yīng)該也是這部分代碼酬核,因?yàn)榍懊娴拇a蜜另,只要讀者理得清楚里面各個(gè)類的關(guān)系,就不難嫡意。而這部分代碼由于涉及到限流算法举瑰,會(huì)稍微復(fù)雜一點(diǎn)點(diǎn)。

DegradeSlot

image

恭喜大家蔬螟,終于到最后一個(gè) slot 了此迅。

它有三個(gè)策略,我們首先說說根據(jù) RT 降級:

image

如果按照上面的配置:對于 getUserInfo 這個(gè)資源促煮,正常情況下邮屁,它只需要 50ms 就夠了,如果它的 RT 超過了 100ms菠齿,那么它會(huì)進(jìn)入半降級狀態(tài)佑吝,接下來的 5 次訪問,如果都超過了 100ms绳匀,那么在接下來的 10 秒內(nèi)芋忿,所有的請求都會(huì)被拒絕。

其實(shí)這個(gè)描述不是百分百準(zhǔn)確疾棵,打開 DegradeRule#passCheck 源碼戈钢,我們用代碼來描述:

image

Sentinel 使用了 cut 作為開關(guān),開啟這個(gè)開關(guān)以后是尔,會(huì)啟動(dòng)一個(gè)定時(shí)任務(wù)殉了,過了 10秒 以后關(guān)閉這個(gè)開關(guān)。

<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">

if (cut.compareAndSet(false, true)) {
    ResetTask resetTask = new ResetTask(this);
    pool.schedule(resetTask, timeWindow, TimeUnit.SECONDS);
}

對于異常比例和異常數(shù)的控制拟枚,非常簡單薪铜,大家看一下源碼就懂了。同理恩溅,達(dá)到閾值隔箍,開啟斷路器,之后由定時(shí)任務(wù)關(guān)閉脚乡,這里就不浪費(fèi)篇幅了蜒滩。

應(yīng)用和 sentinel-dashboard 的交互

這里花點(diǎn)篇幅介紹一下客戶端是怎么和 dashboard 進(jìn)行交互的。

在 Sentinel 的源碼中奶稠,打開 sentinel-transport 工程俯艰,可以看到三個(gè)子工程,common 是基礎(chǔ)包和接口定義窒典。

image

如果客戶端要接入 dashboard蟆炊,可以使用 netty-http 或 simple-http 中的一個(gè)。為什么不直接使用 Netty瀑志,而要同時(shí)提供 http 的選項(xiàng)呢涩搓?那是因?yàn)槟悴灰欢ㄊ褂?Java 來實(shí)現(xiàn) dashboard污秆,如果我們使用其他語言來實(shí)現(xiàn) dashboard 的話,使用 http 協(xié)議比較容易適配昧甘。

下面我們只介紹 http 的使用良拼,首先,添加 simple-http 依賴:

<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">

<dependency>
   <groupId>com.alibaba.csp</groupId>
   <artifactId>sentinel-transport-simple-http</artifactId>
   <version>1.6.3</version>
</dependency>

然后在應(yīng)用啟動(dòng)參數(shù)中添加 dashboard 服務(wù)器地址充边,同時(shí)可以指定當(dāng)前應(yīng)用的名稱:

<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">

-Dcsp.sentinel.dashboard.server=127.0.0.1:8080 -Dproject.name=sentinel-learning

這個(gè)時(shí)候我們打開 dashboard 是看不到這個(gè)應(yīng)用的庸推,因?yàn)闆]有注冊。

當(dāng)我們在第一次使用 Sentinel 以后浇冰,Sentinel 會(huì)自動(dòng)注冊贬媒。

下面帶大家看看過程是怎樣的。首先肘习,我們在使用 Sentinel 的時(shí)候會(huì)調(diào)用 SphU#entry:

<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">

public static Entry entry(String name) throws BlockException {
    return Env.sph.entry(name, EntryType.OUT, 1, OBJECTS0);
}

這里使用了 Env 類际乘,其實(shí)就是這個(gè)類做的事情:

<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">

public class Env {
    public static final Sph sph = new CtSph();
    static {
        // If init fails, the process will exit.
        InitExecutor.doInit();
    }
}

進(jìn)到 InitExecutor.doInit 方法:

<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">

public static void doInit() {
    if (!initialized.compareAndSet(false, true)) {
        return;
    }
    try {
        ServiceLoader<InitFunc> loader = ServiceLoader.load(InitFunc.class);
        List<OrderWrapper> initList = new ArrayList<OrderWrapper>();
        for (InitFunc initFunc : loader) {
            insertSorted(initList, initFunc);
        }
        for (OrderWrapper w : initList) {
            w.func.init();
        }
        // ...
}

這里使用 SPI 加載 InitFunc 的實(shí)現(xiàn),大家可以在這里斷個(gè)點(diǎn)漂佩,可以發(fā)現(xiàn)這里加載了 CommandCenterInitFunc 類和 HeartbeatSenderInitFunc 類脖含。

前者是客戶端啟動(dòng)的接口服務(wù),提供給 dashboard 查詢數(shù)據(jù)和規(guī)則設(shè)置使用的投蝉。后者用于客戶端主動(dòng)發(fā)送心跳信息給 dashboard养葵。

我們看 HeartbeatSenderInitFunc#init 方法:

<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">

@Override
public void init() {
    HeartbeatSender sender = HeartbeatSenderProvider.getHeartbeatSender();
    if (sender == null) {
        RecordLog.warn("[HeartbeatSenderInitFunc] WARN: No HeartbeatSender loaded");
        return;
    }

    initSchedulerIfNeeded();
    long interval = retrieveInterval(sender);
    setIntervalIfNotExists(interval);
    // 啟動(dòng)一個(gè)定時(shí)器,發(fā)送心跳信息
    scheduleHeartbeatTask(sender, interval);
}

這里看到瘩缆,init 方法的第一行就是去加載 HeartbeatSender 的實(shí)現(xiàn)類关拒,這里又用到了 SPI 的機(jī)制,如果我們添加了 sentinel-transport-simple-http 這個(gè)依賴庸娱,那么 SimpleHttpHeartbeatSender 就會(huì)被加載夏醉。

之后在上面的最后一行代碼,啟動(dòng)了一個(gè)定時(shí)器涌韩,以一定的間隔(默認(rèn)10秒)不斷地發(fā)送心跳信息到 dashboard 應(yīng)用,這個(gè)心跳信息中就包含應(yīng)用的名稱氯夷、ip臣樱、port、Sentinel 版本 等信息腮考。

而對于 dashboard 來說雇毫,有了這些信息,就可以對應(yīng)用進(jìn)行規(guī)則設(shè)置踩蔚、到應(yīng)用拉取數(shù)據(jù)用于頁面展示等棚放。

Sentinel 在客戶端并沒有使用第三方 http 包,而是自己基于 JDK 的 Socket 和 ServerSocket 接口實(shí)現(xiàn)了簡單的客戶端和服務(wù)端馅闽,主要也是為了不增加依賴飘蚯。

Sentinel 中秒級 QPS 的統(tǒng)計(jì)問題

以下內(nèi)容建立在你對于滑動(dòng)窗口有了較為深入的了解的基礎(chǔ)上馍迄,如果你覺得有點(diǎn)吃力,說明你對于 Sentinel 還不是完全熟悉局骤,可以選擇性放棄這一節(jié)的內(nèi)容攀圈。

我們前面介紹了滑動(dòng)窗口用在 維度的數(shù)據(jù)統(tǒng)計(jì)上,當(dāng)我們在說 QPS 的時(shí)候峦甩,當(dāng)然我們一般指的是秒維度的數(shù)據(jù)赘来。當(dāng)然,你在很多地方看到的 QPS 數(shù)據(jù)凯傲,其實(shí)都是通過分維度的數(shù)據(jù)來得到的犬辰,包括 metrics 日志文件、dashboard 中的 QPS冰单。

下面幌缝,我們深入分析秒維度數(shù)據(jù)統(tǒng)計(jì)的一些問題。

在開始的時(shí)候球凰,我們說了 Sentinel 統(tǒng)計(jì)了 兩個(gè)維度的數(shù)據(jù):

1狮腿、對于 來說,一輪是 60 秒呕诉,分為 60 個(gè)時(shí)間窗口缘厢,每個(gè)時(shí)間窗口是 1 秒;

2甩挫、對于 來說贴硫,一輪是 1 秒,分為 2 個(gè)時(shí)間窗口伊者,每個(gè)時(shí)間窗口是 0.5 秒英遭;

如果我們用上面介紹的統(tǒng)計(jì)分維度的 BucketLeapArray 來統(tǒng)計(jì)秒維度數(shù)據(jù)可以嗎?答案當(dāng)然是不行亦渗,因?yàn)闀?huì)不準(zhǔn)確挖诸。

設(shè)想一個(gè)場景,我們的一個(gè)資源法精,訪問的 QPS 穩(wěn)定是 10多律,假設(shè)請求是均勻分布的,在相對時(shí)間 0.0 - 1.0 秒?yún)^(qū)間搂蜓,通過了 10 個(gè)請求狼荞,我們在 1.1 秒的時(shí)候,觀察到的 QPS 可能只有 5帮碰,因?yàn)榇藭r(shí)第一個(gè)時(shí)間窗口被重置了相味,只有第二個(gè)時(shí)間窗口有值。

這個(gè)大家應(yīng)該很容易理解殉挽,如果你覺得不理解丰涉,可以不用浪費(fèi)時(shí)間在這節(jié)了

所以拓巧,我們可以知道,如果用 BucketLeapArray 來實(shí)現(xiàn)昔搂,會(huì)有 0~50% 的數(shù)據(jù)誤差玲销,這肯定是不能接受的。

那能不能增加窗口的數(shù)量來降低誤差到一個(gè)合理的范圍內(nèi)呢摘符?這個(gè)大家可以思考一下贤斜,考慮一下它對于性能是否有較大的損失。

大家翻開 StatisticNode 的源碼逛裤,對于秒維度數(shù)據(jù)統(tǒng)計(jì)瘩绒,Sentinel 使用下面的構(gòu)造方法:

<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">

// 2 個(gè)時(shí)間窗口,每個(gè)窗口長度 0.5 秒
public ArrayMetric(int sampleCount, int intervalInMs) {
    this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
}

OccupiableBucketLeapArray 實(shí)現(xiàn)類的源碼并不長带族,我們大概看一眼锁荔,可以發(fā)現(xiàn)它的 newEmptyBucket 和 resetWindowTo 這兩個(gè)方法和 BucketLeapArray 有點(diǎn)不一樣,也就是在重置的時(shí)候蝙砌,它不是直接重置成 0 的阳堕。

所以,我們要大膽猜測一下择克,這個(gè)類里面的 borrowArray 做了一些事情恬总,它是 FutureBucketLeapArray 的實(shí)例,這個(gè)類和前面接觸的 BucketLeapArray 差不多肚邢,但是加了一個(gè) Future 單詞壹堰。這里我們先仔細(xì)看看它。

它和 BucketLeapArray 唯一的不同是骡湖,它覆寫了下面這個(gè)方法:

<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">

@Override
public boolean isWindowDeprecated(long time, WindowWrap<MetricBucket> windowWrap) {
    // Tricky: will only calculate for future.
    return time >= windowWrap.windowStart();
}

</pre>

我們發(fā)現(xiàn)贱纠,如果按照它的這種定義,在調(diào)用 values() 方法的時(shí)候响蕴,所有的 2 個(gè)窗口都是過期的谆焊,將得不到任何的值。所以浦夷,我們大概可以判斷懊渡,給這個(gè)數(shù)組添加值的時(shí)候,使用的時(shí)間應(yīng)該不是當(dāng)前時(shí)間军拟,而是一個(gè)未來的時(shí)間點(diǎn)。這大概就是 Future 要表達(dá)的意思誓禁。

我們再回到 OccupiableBucketLeapArray 這個(gè)類懈息,可以看到在重置的時(shí)候,它使用了 borrowArray 的值:

<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">

@Override
protected WindowWrap<MetricBucket> resetWindowTo(WindowWrap<MetricBucket> w, long time) {
    // Update the start time and reset value.
    w.resetTo(time);
    MetricBucket borrowBucket = borrowArray.getWindowValue(time);
    if (borrowBucket != null) {
        w.value().reset();
        w.value().addPass((int)borrowBucket.pass());
    } else {
        w.value().reset();
    }
    return w;
}

所以我們大概可以猜一猜它是怎么利用這個(gè) FutureBucketLeapArray 實(shí)例的:borrowArray 存儲了未來的時(shí)間窗口的值摹恰。當(dāng)主線到達(dá)某個(gè)時(shí)間窗口的時(shí)候辫继,如果發(fā)現(xiàn)當(dāng)前時(shí)間窗口是過期的怒见,前面介紹過,會(huì)需要重置這個(gè)窗口姑宽,這個(gè)時(shí)候遣耍,它會(huì)檢查一下 borrowArray 是否有值,如果有炮车,將其作為這個(gè)窗口的初始值填充進(jìn)來舵变,而不是簡單重置為 0 值。

有了這個(gè)思路瘦穆,我們再看 borrowArray 中的值是怎么進(jìn)來的纪隙。

我們很容易可以找到,只可能通過這里的 addWaiting 方法設(shè)置:

<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">

@Override
public void addWaiting(long time, int acquireCount) {
    WindowWrap<MetricBucket> window = borrowArray.currentWindow(time);
    window.value().add(MetricEvent.PASS, acquireCount);
}

接下來扛或,我們找這個(gè)方法被哪里調(diào)用了绵咱,找到最后,我們發(fā)現(xiàn)只有 DefaultController 這個(gè)類中有調(diào)用熙兔。

這個(gè)類是流控中的 “快速失敗” 規(guī)則控制器悲伶,我們簡單看一下代碼:

<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">

@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
    int curCount = avgUsedTokens(node);
    if (curCount + acquireCount > count) {
        // 只有設(shè)置了 prioritized 的情況才會(huì)進(jìn)入到下面的 if 分支
        // 也就是說,對于一般的場景住涉,被限流了麸锉,就快速失敗
        if (prioritized && grade == RuleConstant.FLOW_GRADE_QPS) {
            long currentTime;
            long waitInMs;
            currentTime = TimeUtil.currentTimeMillis();
            // 下面的這行 tryOccupyNext 非常復(fù)雜,大意就是說去占有"未來的"令牌
            // 可以看到秆吵,下面做了 sleep淮椰,為了保證 QPS 不會(huì)因?yàn)轭A(yù)占而撐大
            waitInMs = node.tryOccupyNext(currentTime, acquireCount, count);
            if (waitInMs < OccupyTimeoutProperty.getOccupyTimeout()) {
                // 就是這里設(shè)置了 borrowArray 的值
                node.addWaitingRequest(currentTime + waitInMs, acquireCount);
                node.addOccupiedPass(acquireCount);
                sleep(waitInMs);

                // PriorityWaitException indicates that the request will pass after waiting for {@link @waitInMs}.
                throw new PriorityWaitException(waitInMs);
            }
        }
        return false;
    }
    return true;
}

看到這里,我其實(shí)還有很多疑問沒有被解開

首先纳寂,這里解開了一個(gè)問題主穗,就是這個(gè)類為什么叫 OccupiableBucketLeapArray?

  • Occupiable 這里代表可以被預(yù)占的意思毙芜,結(jié)合上面 DefaultController 的源碼忽媒,可以知道它原來是用來滿足 prioritized 類型的資源的,我們可以認(rèn)為這類請求有較高的優(yōu)先級腋粥。如果 QPS 達(dá)到閾值晦雨,這類資源通常不能用快速失敗返回, 而是讓它去預(yù)占未來的 QPS 容量隘冲。

當(dāng)然闹瞧,令人失望的是,這里根本沒有解開 QPS 是怎么準(zhǔn)確計(jì)算的這個(gè)問題展辞。

下面奥邮,我思路倒回來,我來證明 Sentinel 的秒維度的 QPS 統(tǒng)計(jì)是不準(zhǔn)確的:

<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">

public static void main(String[] args) {
    // 下面幾行代碼設(shè)置了 QPS 閾值是 100
    FlowRule rule = new FlowRule("test");
    rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
    rule.setCount(100);
    rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);
    List<FlowRule> list = new ArrayList<>();
    list.add(rule);
    FlowRuleManager.loadRules(list);

    // 先通過一個(gè)請求,讓 clusterNode 先建立起來
    try (Entry entry = SphU.entry("test")) {
    } catch (BlockException e) {
    }

    // 起一個(gè)線程一直打印 qps 數(shù)據(jù)
    new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                System.out.println(ClusterBuilderSlot.getClusterNode("test").passQps());
            }
        }
    }).start();

    while (true) {
        try (Entry entry = SphU.entry("test")) {
            Thread.sleep(5);
        } catch (BlockException e) {
            // ignore
        } catch (InterruptedException e) {
            // ignore
        }
    }
}

大家跑一下代碼洽腺,然后觀察下輸出脚粟,QPS 數(shù)據(jù)在 50~100 這個(gè)區(qū)間一直變化,印證了我前面說的蘸朋,秒級 QPS 統(tǒng)計(jì)是極度不準(zhǔn)確的核无。

根據(jù)前面的分析,其實(shí)也沒有什么結(jié)論要說了藕坯。剩下的交給大家自己去思考团南,去探索,這個(gè)過程一定比看我的文章更有意思堕担。

原文地址:https://javadoop.com/post/sentinel

書籍推薦 Redis 深度歷險(xiǎn):核心原理和應(yīng)用實(shí)踐

獲取方式:關(guān)注然后簡信“資料”即可獲得文檔領(lǐng)取方式

image
image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末已慢,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子霹购,更是在濱河造成了極大的恐慌佑惠,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,888評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件齐疙,死亡現(xiàn)場離奇詭異膜楷,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)贞奋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評論 3 399
  • 文/潘曉璐 我一進(jìn)店門赌厅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人轿塔,你說我怎么就攤上這事特愿。” “怎么了勾缭?”我有些...
    開封第一講書人閱讀 168,386評論 0 360
  • 文/不壞的土叔 我叫張陵揍障,是天一觀的道長。 經(jīng)常有香客問我俩由,道長毒嫡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,726評論 1 297
  • 正文 為了忘掉前任幻梯,我火速辦了婚禮兜畸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘碘梢。我一直安慰自己咬摇,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,729評論 6 397
  • 文/花漫 我一把揭開白布煞躬。 她就那樣靜靜地躺著肛鹏,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上龄坪,一...
    開封第一講書人閱讀 52,337評論 1 310
  • 那天,我揣著相機(jī)與錄音复唤,去河邊找鬼健田。 笑死,一個(gè)胖子當(dāng)著我的面吹牛佛纫,可吹牛的內(nèi)容都是我干的妓局。 我是一名探鬼主播,決...
    沈念sama閱讀 40,902評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼呈宇,長吁一口氣:“原來是場噩夢啊……” “哼好爬!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起甥啄,我...
    開封第一講書人閱讀 39,807評論 0 276
  • 序言:老撾萬榮一對情侶失蹤存炮,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后蜈漓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體穆桂,經(jīng)...
    沈念sama閱讀 46,349評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,439評論 3 340
  • 正文 我和宋清朗相戀三年融虽,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了享完。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,567評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡有额,死狀恐怖般又,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情巍佑,我是刑警寧澤茴迁,帶...
    沈念sama閱讀 36,242評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站句狼,受9級特大地震影響笋熬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜腻菇,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,933評論 3 334
  • 文/蒙蒙 一胳螟、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧筹吐,春花似錦糖耸、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春舍扰,著一層夾襖步出監(jiān)牢的瞬間倦蚪,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評論 1 272
  • 我被黑心中介騙來泰國打工边苹, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留陵且,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,995評論 3 377
  • 正文 我出身青樓个束,卻偏偏與公主長得像慕购,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子茬底,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,585評論 2 359

推薦閱讀更多精彩內(nèi)容

  • 人的一生約有三分之一的時(shí)間是在睡眠中度過的沪悲,可曾留意過自己是什么樣的睡姿?你知道正確的睡姿嗎阱表? 睡姿是一種睡覺的姿...
    紫藤有鉆閱讀 1,565評論 0 0
  • 今天殿如,媽媽特別的忙碌,一早起來打卡捶枢、參加晨會(huì)分享握截、下樓給先生移車,8:00出門參加一次線下共同體的活動(dòng)烂叔,一直到午餐...
    不明所以的蝸牛閱讀 244評論 0 3
  • 深陷醋海的少陽谨胞,翻來覆去,總歸是睡不著了蒜鸡,索性起身胯努,去屋外走走。不湊巧的是鳳九的云逸哥哥逢防,也推開門走出了自己的房間...
    轉(zhuǎn)角花開閱讀 3,074評論 2 46