Sentienl 流控效果之勻速排隊與預熱實現原理與實戰建議

溫馨提示,如果大家對源碼不感興趣,可以直接跳到本文的總結部分,瞭解一下預熱實現原理的一些實戰建議。

首先先回顧一下 Sentinel 流控效果相關的類圖:
在這裏插入圖片描述
DefaultController 快速失敗已經在上文詳細介紹過,本文將詳細介紹其他兩種策略的實現原理。

首先我們應該知道,一條流控規則(FlowRule)對應一個 TrafficShapingController 對象。

1、RateLimiterController

勻速排隊策略實現類,首先我們先來介紹一下該類的幾個成員變量的含義:

  • int maxQueueingTimeMs
    排隊等待的最大超時時間,如果等待超過該時間,將會拋出 FlowException。
  • double count
    流控規則中的闊值,即令牌的總個數,以QPS爲例,如果該值設置爲1000,則表示1s可併發的請求數量。
  • AtomicLong latestPassedTime
    上一次成功通過的時間戳。

接下來我們詳細來看一下其算法的實現:
RateLimiterController#canPass

public boolean canPass(Node node, int acquireCount, boolean prioritized) {
    if (acquireCount <= 0) {
        return true;
    }
    if (count <= 0) {
        return false;
    }
    long currentTime = TimeUtil.currentTimeMillis();
    long costTime = Math.round(1.0 * (acquireCount) / count * 1000);    // @1
    long expectedTime = costTime + latestPassedTime.get();                // @2
    if (expectedTime <= currentTime) {                                                    // @3
        latestPassedTime.set(currentTime);
        return true;
    } else {
        long waitTime = costTime + latestPassedTime.get() - TimeUtil.currentTimeMillis();   // @4
        if (waitTime > maxQueueingTimeMs) {                                                                        // @5
            return false;
        } else {
            long oldTime = latestPassedTime.addAndGet(costTime);                                     // @6
            try {
                waitTime = oldTime - TimeUtil.currentTimeMillis();                                            
                if (waitTime > maxQueueingTimeMs) {
                    latestPassedTime.addAndGet(-costTime);
                    return false;
                }
		if (waitTime > 0) {                                                     // @7
                    Thread.sleep(waitTime);
                }
                return true;
            } catch (InterruptedException e) {
            }
        }
    }
    return false;
}

代碼@1:首先算出每一個請求之間最小的間隔,時間單位爲毫秒。例如 cout 設置爲 1000,表示一秒可以通過 1000個請求,勻速排隊,那每個請求的間隔爲 1 / 1000(s),乘以1000將時間單位轉換爲毫秒,如果一次需要2個令牌,則其間隔時間爲2ms,用 costTime 表示。

代碼@2:計算下一個請求的期望達到時間,等於上一次通過的時間戳 + costTime ,用 expectedTime 表示。

代碼@3:如果 expectedTime 小於等於當前時間,說明在期望的時間沒有請求到達,說明沒有按照期望消耗令牌,故本次請求直接通過,並更新上次通過的時間爲當前時間。

代碼@4:如果 expectedTime 大於當前時間,說明還沒到令牌發放時間,當前請求需要等待。首先先計算需要等待是時間,用 waitTime 表示。

代碼@5:如果計算的需要等待的時間大於允許排隊的時間,則返回 false,即本次請求將被限流,返回 FlowException。

代碼@6:進入排隊,默認是本次請求通過,故先將上一次通過流量的時間戳增加 costTime,然後直接調用 Thread 的 sleep 方法,將當前請求先阻塞一會,然後返回 true 表示請求通過。

勻速排隊模式的實現的關鍵:主要是記錄上一次請求通過的時間戳,然後根據流控規則,判斷兩次請求之間最小的間隔,並加入一個排隊時間。

2、WarmUpController

預熱策略的實現,首先我們先來介紹一下該類的幾個成員變量的含義:

  • double count
    流控規則設定的闊值。
  • int coldFactor
    冷卻因子。
  • int warningToken
    告警token,對應 Guava 中的 RateLimiter 中的
  • int maxToken
    double slope
    AtomicLong storedTokens
    AtomicLong lastFilledTime

2.1 WarmUpController 構造函數

內部的構造函數,最終將調用 construct 方法。
WarmUpController#construct

private void construct(double count, int warmUpPeriodInSec, int coldFactor) { // @1
	if (coldFactor <= 1) {
		throw new IllegalArgumentException("Cold factor should be larger than 1");
         }
	this.count = count;  
	this.coldFactor = coldFactor;   
	warningToken = (int)(warmUpPeriodInSec * count) / (coldFactor - 1);   // @2
	maxToken = warningToken + (int)(2 * warmUpPeriodInSec * count / (1.0 + coldFactor));  // @3
	slope = (coldFactor - 1.0) / count / (maxToken - warningToken);  
}

要理解該方法,就需要理解 Guava 框架的 SmoothWarmingUp 相關的預熱算法,其算法原理如圖所示:
在這裏插入圖片描述
關於該圖的詳細介紹,請參考筆者的另外一篇博文:源碼分析RateLimiter SmoothWarmingUp 實現原理,對該圖進行了詳細解讀。

代碼@1:首先介紹該方法的參數列表:

  • double count
    限流規則配置的闊值,例如是按 TPS 類型來限流,如果限制爲100tps,則該值爲100。
  • int warmUpPeriodInSec
    預熱時間,單位爲秒,通用在限流規則頁面可配置。
  • int coldFactor
    冷卻因子,這裏默認爲3,與 RateLimiter 中的冷卻因子保持一致,表示的含義爲 coldIntervalMicros 與 stableIntervalMicros 的比值。

代碼@2:計算 warningToken 的值,與 Guava 中的 RateLimiter 中的 thresholdPermits 的計算算法公式相同,thresholdPermits = 0.5 * warmupPeriod / stableInterval,在Sentienl 中,而 stableInteral = 1 / count,thresholdPermits 表達式中的 0.5 就是因爲 codeFactor 爲3,因爲 warm up period與 stable 面積之比等於 (coldIntervalMicros - stableIntervalMicros ) 與 stableIntervalMicros 的比值,這個比值又等於 coldIntervalMicros / stableIntervalMicros - stableIntervalMicros / stableIntervalMicros 等於 coldFactor - 1。

代碼@3:同樣根據 Guava 中的 RateLimiter 關於 maxToken 也能理解。

2.2 canPass 方法詳解

WarmUpController#canPass

public boolean canPass(Node node, int acquireCount, boolean prioritized) {
    long passQps = (long) node.passQps(); // @1
    long previousQps = (long) node.previousPassQps();  // @2
    syncToken(previousQps);  // @3
	// 開始計算它的斜率
    // 如果進入了警戒線,開始調整他的qps
    long restToken = storedTokens.get();
    if (restToken >= warningToken) {    // @4
        long aboveToken = restToken - warningToken;
        // 消耗的速度要比warning快,但是要比慢
        // current interval = restToken*slope+1/count
        double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
        if (passQps + acquireCount <= warningQps) {
            return true;
        }
    } else {   // @5
        if (passQps + acquireCount <= count) {
            return true;
        }
    }
    return false;
}

代碼@1:先獲取當前節點已通過的QPS。

代碼@2:獲取當前滑動窗口的前一個窗口收集的已通過QPS。

代碼@3:調用 syncToken 更新 storedTokens 與 lastFilledTime 的值,即按照令牌發放速率發送指定令牌,將在下文詳細介紹 syncToken 方法內部的實現細節。

代碼@4:如果當前存儲的許可大於warningToken的處理邏輯,主要是在預熱階段允許通過的速率會比限流規則設定的速率要低,判斷是否通過的依據就是當前通過的TPS與申請的許可數是否小於當前的速率(這個值加入斜率,即在預熱期間,速率是慢慢達到設定速率的。

代碼@5:當前存儲的許可小於warningToken,則按照規則設定的速率進行判定。

不知大家有沒有一個疑問,爲什麼 storedTokens 剩餘許可數越大,限制其通過的速率竟然會越慢,這又怎麼理解呢?大家可以思考一下這個問題,將在本文的總結部分進行解答。

我們先來看一下 syncToken 的實現細節,即更新 storedTokens 的邏輯。
WarmUpController#syncToken

protected void syncToken(long passQps) {
    long currentTime = TimeUtil.currentTimeMillis();
    currentTime = currentTime - currentTime % 1000;    // @1
    long oldLastFillTime = lastFilledTime.get();
    if (currentTime <= oldLastFillTime) {                          // @2
        return;
    }

    long oldValue = storedTokens.get();
    long newValue = coolDownTokens(currentTime, passQps);   // @3

    if (storedTokens.compareAndSet(oldValue, newValue)) {  
        long currentValue = storedTokens.addAndGet(0 - passQps);    // @4
        if (currentValue < 0) {
            storedTokens.set(0L);
        }
        lastFilledTime.set(currentTime);
    }
}

代碼@1:這個是計算出當前時間秒的最開始時間。例如當前是 2020-04-06 08:29:01:056,該方法返回的時間爲 2020-04-06 08:29:01:000。

代碼@2:如果當前時間小於等於上次發放許可的時間,則跳過,無法發放令牌,即每秒發放一次令牌。

代碼@3:具體方法令牌的邏輯,稍後詳細介紹。

代碼@4:更新剩餘令牌,即生成的許可後要減去上一秒通過的令牌。

我們詳細來看一下 coolDownTokens 方法。
WarmUpController#coolDownTokens

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

    // 添加令牌的判斷前提條件:
    // 當令牌的消耗程度遠遠低於警戒線的時候
    if (oldValue < warningToken) {    // @1
        newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
    } else if (oldValue > warningToken) {   // @2
        if (passQps < (int)count / coldFactor) {
            newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
        }
    }
    return Math.min(newValue, maxToken);// @3
}

代碼@1:如果當前剩餘的 token 小於警戒線,可以按照正常速率發放許可。

代碼@2:如果當前剩餘的 token 大於警戒線但前一秒的QPS小於 (count 與 冷卻因子的比),也發放許可(這裏我不是太明白其用意)。

代碼@3:這裏是關鍵點,第一次運行,由於 lastFilledTime 等於0,這裏將返回的是 maxToken,故這裏一開始的許可就會超過 warningToken,啓動預熱機制,進行速率限制。

3、總結

WarmUpController 這個預熱算法還是挺複雜的,接下來我們來總結一下它的特徵。

不知大家有沒有一個疑問,爲什麼 storedTokens 剩餘許可數越大,限制其通過的速率竟然會越慢,這又怎麼理解呢?

這裏感覺有點逆向思維的味道,因爲一開始就會將 storedTokens 的值設置爲 maxToken,即開始就會超過 warningToken,從而一開始進入到預熱階段,此時的速率有一個爬坡的過程,類似於數學中的斜率,達到其他啓動預熱的效果。

實戰指南:注意 warmUpPeriodInSec 與 coldFactor 的設置,將會影響最終的限流效果。

爲了更加直觀的理解,我們舉例如下,warningToken 與 maxToken 的生成公式如下:

warningToken = (int)(warmUpPeriodInSec * count) / (coldFactor - 1);  
maxToken = warningToken + (int)(2 * warmUpPeriodInSec * count / (1.0 + coldFactor));  

coldFactor 設定爲 3,例如限流規則中配置每秒允許通過的許可數量爲 10,即 count 值等於 10,我們改變 warmUpPeriodInSec 的值來看一下 warningToken 與 maxToken 的值,以此來探究 Sentinel WarmUpController 的工作機制或工作效果。

warmUpPeriodInSec warningToken maxToken
1 5 10
2 10 20
3 15 30
4 20 40

根據上面的算法,如果 warningToken 的值小於 count,則限流會變的更嚴厲,即最終的限流TPS會小於設置的TPS。即 warmUpPeriodInSec 設置過大過小都不合適,其標準是要使得 warningToken 的值大於 count。

如果文章對你有所幫助的話,還請點個贊,謝謝。


作者信息:丁威,《RocketMQ技術內幕》作者、CSDN博客專家,原創公衆號『中間件興趣圈』維護者。擅長JAVA編程,對主流中間件 RocketMQ、Dubbo、ElasticJob、Netty、Sentinel、Mybatis、Mycat 等中間件有深入研究。歡迎加入筆者的知識星球,一起探討高併發、分佈式服務架構,分享閱讀源碼心得。

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