源碼分析RateLimiter SmoothWarmingUp 實現原理(文末附流程圖)

上一篇詳細介紹了 SmoothBursty 的實現原理,本文將介紹帶有預熱機制的限速器實現原理。

1、類圖

在這裏插入圖片描述
從上文也詳細介紹了 RateLimiter 相關的類圖,本文就不詳細介紹。

2、SmoothWarmingUp 創建流程

創建 SmoothWarmingUp 限速器的入口爲 RateLimiter 的 create 方法,其代碼如下:
RateLimiter#create

public static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) {  // @1
    checkArgument(warmupPeriod >= 0, "warmupPeriod must not be negative: %s", warmupPeriod);
    return create(
        SleepingStopwatch.createFromSystemTimer(), permitsPerSecond, warmupPeriod, unit, 3.0);
}

代碼@1:首先先來看一下參數列表:

  • double permitsPerSecond
    每秒發放許可數量,即所謂的QPS。
  • long warmupPeriod
    設置預熱時間。
  • TimeUnit unit
    warmupPeriod 的時間單位。

代碼@2:調用內部的重載方法創建 SmoothWarmingUp 。

RateLimiter#create

static RateLimiter create( SleepingStopwatch stopwatch, double permitsPerSecond, long warmupPeriod, TimeUnit unit, double coldFactor) {
    RateLimiter rateLimiter = new SmoothWarmingUp(stopwatch, warmupPeriod, unit, coldFactor);  // @1
    rateLimiter.setRate(permitsPerSecond); // @2
    return rateLimiter;
}

創建 SmoothWarmingUp 兩個主要步驟分別是調用其構造方法首先創建 SmoothWarmingUp 實例,然後調用其 setRate 方法進行初始化速率。這裏先突出 coldFactor,默認爲 3.0,該屬性的作用將在下文詳細介紹。

我們先來重點探討一下 setRate 方法的實現。最終會調用其父類 SmoothRateLimiter 的doSetRate 方法。

SmoothRateLimiter#doSetRate

final void doSetRate(double permitsPerSecond, long nowMicros) {
    resync(nowMicros);   // @1 
    double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;   
    this.stableIntervalMicros = stableIntervalMicros;   // @2
    doSetRate(permitsPerSecond, stableIntervalMicros);  // @3
}

代碼@1:基於當前時間重置 SmoothRateLimiter 內部的 storedPermits(已存儲的許可數量) 與 nextFreeTicketMicros(下一次可以免費獲取許可的時間) 值,所謂的免費指的是無需等待就可以獲取設定速率的許可,該方法對理解限流許可的產生非常關鍵,稍後詳細介紹。

代碼@2:根據QPS算出一個穩定的獲取1個許可的時間。以一秒發放5個許可,即限速爲5QPS,那發放一個許可的世界間隔爲 200ms,stableIntervalMicros 變量是以微妙爲單位。

代碼@4:調用 SmoothRateLimiter 的抽象方法 doSetRate 設置速率,這裏會調用 SmoothWarmingUp 的 doSetRate 方法。

在介紹 SmoothWarmingUp 的 doSetRate 方法之前,我們先來看一下 resync 方法的實現。

SmoothRateLimiter#resync

void resync(long nowMicros) {
    if (nowMicros > nextFreeTicketMicros) {  // @1 
      double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();  // @2
      storedPermits = min(maxPermits, storedPermits + newPermits);    // @3
      nextFreeTicketMicros = nowMicros;   // @4
    }
}

代碼@1:如果當前已啓動時間大於nextFreeTicketMicros(下一次可以免費獲取許可的時間),則需要重新計算許可,即又可以向許可池中添加許可。

代碼@2:根據當前時間可增加的許可數量,由於 SmoothWarmingUp 實現了預熱機制,平均生成一個許可的時間並不是固定不變的。具體由 coolDownIntervalMicros 方法實現,稍候詳細介紹。

代碼@3:計算當前可用的許可,將新增的這些許可添加到許可池,但不會超過其最大值。

代碼@4:更新下一次可增加計算許可的時間。

SmoothWarmingUp#coolDownIntervalMicros

double coolDownIntervalMicros() {
    return warmupPeriodMicros / maxPermits;
}

這個方法的實現其實簡單,用生成這些許可的總時間除以現在已經生成的許可數,即可得到當前時間點平均一個許可的生成時間。

接下來重點探討 SmoothWarmingUp 的 doSetRate 方法。
爲了方便理解 SmoothWarmingUp doSetRate 方法,我根據 SmoothWarmingUp 類的註釋,結合代碼,給出如下示例圖:
在這裏插入圖片描述
首先我們先來根據 SmoothWarmingUp 的相關注釋來理解一下上述這張圖的幾個要點。

  • 圖中有兩個陰影面積,一個用 stable,另外一個warm up period。在預熱算法中,這兩個陰影面積的關係與冷卻因子相關。
  • 冷卻因子 coldFactor 表示的含義爲 coldIntervalMicros 與 stableIntervalMicros 的比值。
  • warm up period 陰影面積 與 stable 陰影面積的比值等於 (coldIntervalMicros - stableIntervalMicros ) / stableIntervalMicros ,例如 SmoothWarmingUp 固定的冷卻因子爲3,那麼 coldIntervalMicros 與 stableIntervalMicros 的比值爲 3,那 (coldIntervalMicros - stableIntervalMicros ) / stableIntervalMicros 則爲 2。
  • 在預熱算法中與數學中的積分相關(筆者對這方面的數學知識一竅不通),故這裏只展示結論,而不做推導,陰影 WARM UP PERIOD 的面積等於 warmupPeriod,那陰影stable的面積等於 warmupPeriod/2。
  • 存在如下等式 warmupPeriod/2 = thresholdPermits * stableIntervalMicros (長方形的面積)
  • 同樣存在如下等式 warmupPeriod = 0.5 * (stableInterval + coldInterval) * (maxPermits - thresholdPermits) (梯形面積,(上底 + 下底 * 高 / 2) )

有了上述基本知識,我們再來看一下代碼。

SmoothWarmingUp#doSetRate

void doSetRate(double permitsPerSecond, double stableIntervalMicros) { 
    double oldMaxPermits = maxPermits;
    double coldIntervalMicros = stableIntervalMicros * coldFactor;                // @1
    thresholdPermits = 0.5 * warmupPeriodMicros / stableIntervalMicros;    // @2
    maxPermits =
          thresholdPermits + 2.0 * warmupPeriodMicros / (stableIntervalMicros + coldIntervalMicros);   // @3
    slope = (coldIntervalMicros - stableIntervalMicros) / (maxPermits - thresholdPermits);  // @4
    if (oldMaxPermits == Double.POSITIVE_INFINITY) {
        storedPermits = 0.0;
    } else {
        storedPermits =
            (oldMaxPermits == 0.0)
                ? maxPermits // initial state is cold
                : storedPermits * maxPermits / oldMaxPermits;    // @5
    }
}

代碼@1:根據冷卻因子(coldFactor)來計算冷卻間隔(單位爲微秒),等於冷卻因子與 stableIntervalMicros 的乘積。從這裏我們可以得出如下幾個基本的概念。冷卻因子 coldFactor 爲 冷卻間隔與穩定間隔的比例。

代碼@2:通過 warmupPeriod/2 = thresholdPermits * stableIntervalMicros 等式,求出 thresholdPermits 的值。

代碼@3:根據 warmupPeriod = 0.5 * (stableInterval + coldInterval) * (maxPermits - thresholdPermits) 表示可求出 maxPermits 的數量。

代碼@4:斜率,表示的是從 stableIntervalMicros 到 coldIntervalMicros 這段時間,許可數量從 thresholdPermits 變爲 maxPermits 的增長速率。

代碼@5:根據 maxPermits 更新當前存儲的許可,即當前剩餘可消耗的許可數量。

3、SmoothWarmingUp acquire 流程

首先 acquire 的定義在其父類,這裏是典型的模板模式,由其父類定義基本流程,由具體的子類實現其特定功能。RateLimiter 中的 acquire 方法如下:

public double acquire(int permits) {
    long microsToWait = reserve(permits);    // @1
    stopwatch.sleepMicrosUninterruptibly(microsToWait);   // @2
    return 1.0 * microsToWait / SECONDS.toMicros(1L);   // @3
}

代碼@1:根據當前剩餘的許可與本次申請的許可來判斷本次申請需要等待的時長,如果返回0則表示無需等待。

代碼@2:如果需要等待的時間不爲0,表示觸發限速,睡眠指定時間後喚醒。

代碼@3:返回本次申請等待的時長。

接下來重點介紹 reserve 方法的實現原理。

RateLimiter#reserve

inal long reserve(int permits) {
    checkPermits(permits);
    synchronized (mutex()) {  // @1
      return reserveAndGetWaitLength(permits, stopwatch.readMicros()); // @2
    }
}

代碼@1:限速器主要維護的重要數據字段(storedPermits),對其進行維護時都需要先獲取鎖。

代碼@2:調用內部方法 reserveAndGetWaitLength 來計算需要等待時間。

繼續跟蹤 reserveAndGetWaitLength 方法。

final long reserveAndGetWaitLength(int permits, long nowMicros) {
    long momentAvailable = reserveEarliestAvailable(permits, nowMicros);   // @1
    return max(momentAvailable - nowMicros, 0);  // @2
}

代碼@1:根據當前擁有的許可數量、當前時間判斷待申請許可最早能得到滿足的最早時間,用momentAvailable 表示。

代碼@2:然後計算 momentAvailable 與 nowMicros 的差值與0做比較,得出需要等待的時間。

繼續跟蹤 reserveEarliestAvailable方法,該方法在 RateLimiter 中一個抽象方法,具體實現在其子類 SmoothRateLimiter 中。

SmoothRateLimiter#reserveEarliestAvailable

final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
    resync(nowMicros);   // @1
    long returnValue = nextFreeTicketMicros;
    double storedPermitsToSpend = min(requiredPermits, this.storedPermits); // @2
    double freshPermits = requiredPermits - storedPermitsToSpend; // @3
    long waitMicros =
        storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
            + (long) (freshPermits * stableIntervalMicros);  // @4

    this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);  // @5
    this.storedPermits -= storedPermitsToSpend;    // @6
    return returnValue;
}

代碼@1:在嘗試申請許可之前,先根據當前時間即發放許可速率更新 storedPermits 與 nextFreeTicketMicros(下一次可以免費獲取許可的時間)。

代碼@2:計算本次能從 storedPermits 中消耗的許可數量,取需要申請的許可數量與當前可用的許可數量的最小值,用 storedPermitsToSpend 表示。

代碼@3:如果需要申請的許可數量(requiredPermits)大於當前剩餘許可數量(storedPermits),則還需要等待新的許可生成,用freshPermits 表示,即如果該值大於0,則表示本次申請需要阻塞一定時間。

代碼@4:計算本次申請需要等待的時間,等待的時間由兩部分組成,一部分是由 storedPermitsToWaitTime 方法返回的,另外一部分以穩定速率生成需要的許可,其需要時間爲 freshPermits * stableIntervalMicros,稍後我們詳細分析一下 storedPermitsToWaitTime 方法的實現。

代碼@5:更新 nextFreeTicketMicros 爲當前時間加上需要等待的時間。

代碼@6:更新 storedPermits 的值,即減少本次已消耗的許可數量。

代碼@7:請注意這裏返回的 returnValue 的值,並沒有包含由於剩餘許可需要等待創建新許可的時間,即允許一定的突發流量,故本次計算需要的等待時間將對下一次請求生效。

接下來重點探討一下 SmoothWarmingUp 的 storedPermitsToWaitTime 方法。

SmoothWarmingUp#SmoothWarmingUp

long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {  // @1
	double availablePermitsAboveThreshold = storedPermits - thresholdPermits;   // @2
	long micros = 0;
	if (availablePermitsAboveThreshold > 0.0) {  // @3
		double permitsAboveThresholdToTake = min(availablePermitsAboveThreshold, permitsToTake);  // @31 
                // TODO(cpovirk): Figure out a good name for this variable.
                double length = permitsToTime(availablePermitsAboveThreshold)
                     + permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake);             // @32
                micros = (long) (permitsAboveThresholdToTake * length / 2.0);                                                      // @33
                permitsToTake -= permitsAboveThresholdToTake;                                                                          // @34
         }
        // measuring the integral on the left part of the function (the horizontal line)
        micros += (stableIntervalMicros * permitsToTake);   // @4
        return micros;
}

代碼@1:首先介紹其兩個參數的含義:

  • double storedPermits
    當前存儲的許可數量。
  • double permitsToTake
    本次申請需要的許可數量。

代碼@2:availablePermitsAboveThreshold ,當前超出 thresholdPermits 的許可個數,如果超過 thresholdPermits ,申請許可將來源於超過的部分,只有其不足後,纔會從 thresholdPermits 中申請,這部分的詳細邏輯見代碼@3。

代碼@3:如果當前存儲的許可數量超過了穩定許可 thresholdPermits,即存在預熱的許可數量的申請邏輯,其實現關鍵點如下:

  • 獲取本次從預熱區間申請的許可數量。
  • 從預熱區間獲取一個許可的時間其算法有點晦澀難懂,具體實現爲@32~@34。

代碼@4:從穩定區間獲取一個許可的時間,就容易理解,爲固定的 stableIntervalMicros 。

溫馨提示:從預熱區間計算獲取多個許可的算法,與 slope 有關,筆者並未完成感悟,但至少我們需要明白的是,從 剩餘許可(storedPermits)中申請許可時,優先消耗(大於thresholdPermits 的許可,即消耗 (thresholdPermits ~ maxPermit ) 之間的許可)。

SmoothWarmingUp 的 acquire 流程就介紹到這裏了。

4、總結

SmoothWarmingUp 的 acquire 的流程與 SmoothBursty 類似,故其流程圖與下圖通用,主要的區別生成一個許可的時間有變化,主要是提供了預熱機制。
在這裏插入圖片描述
如果文章對你有所幫助的話,還請點個贊,謝謝。


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

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