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

上篇詳細介紹了Sentinel FlowSlot 限流實現原理(文末附流程圖與總結)的限流實現機制,但主要介紹的策略限流的快速失敗機制,在Sentinel 中除了快速失敗,還提供了勻速排隊,預熱等限流策略,但我發現 Sentinel 的勻速排隊、預熱機制是基於 guava 的 RateLimiter,爲了更加徹底的理解 Sentienl 限流相關的內容,從本文開始先來學習一下 RateLimiter 的相關實現原理。

溫馨提示:文章的末尾會總結 SmoothBursty 的核心流程圖與實現原理,本文將展示筆者是如何一步一步揭曉其實現原理的方法。

1、RateLimiter 類設計圖

在這裏插入圖片描述

  • RateLimiter
    限流抽象類,定義限流器的基本接口。
  • SmoothRateLimiter
    平滑限流實現器,也是一個抽象類。
  • SmoothWarmingUp
    自帶預熱機制的限流器實現類型。
  • SmoothBursty
    適應於突發流量的限流器。

上述類這些屬性,在講解 SmoothBursty、SmoothWarmingUp 時再詳細介紹。

溫馨提示:可以看看這些類上的註釋,先初步瞭解其設計思想。

2、尋找入口

我們首先從 guava 的測試用例中嘗試尋找一下 RateLimiterTest。

public void testSimple() {
    RateLimiter limiter = RateLimiter.create(stopwatch, 5.0);
    limiter.acquire(); // R0.00, since it's the first request
    limiter.acquire(); // R0.20
    limiter.acquire(); // R0.20
    assertEvents("R0.00", "R0.20", "R0.20");
}

從這裏基本可以看出,首先通過 RateLimiter.create 的靜態方法創建一個限流器,然後應用程序在執行業務邏輯之前先調研限流器的 acquire 方法申請許可,接下來我們將循着這個流程來探討其實現思路。

3、探究 SmoothBursty 實現原理

3.1 SmoothBursty 創建流程

從上面的示例來看,應用程序首先通過 RateLimiter 的靜態方法創建一個限流器,其代碼如下:
RateLimiter#create

static RateLimiter create(SleepingStopwatch stopwatch, double permitsPerSecond) { // @1
    RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0);                                  // @2
    rateLimiter.setRate(permitsPerSecond);                                                                    // @3
    return rateLimiter;
}

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

  • SleepingStopwatch stopwatch
    秒錶,主要是實現當前從啓動開始已消耗的時間,有點類似計算一個操作耗時,實現精度納秒。
  • double permitsPerSecond
    每秒的許可數,即通常我們說的限流TPS。

代碼@2:創建 SmoothBursty 對象。

代碼@3:調用 setRate API 設置其速率器。
接下來我們對其進行展開。

3.1.1 SmoothBursty 構造函數

SmoothBursty 構造函數

SmoothBursty(SleepingStopwatch stopwatch, double maxBurstSeconds) {
    super(stopwatch);
    this.maxBurstSeconds = maxBurstSeconds;
}

這裏主要是爲 stopWatch 與 maxBurstSeconds 賦值,其中 maxBurstSeconds 爲允許的突發流量的時間,這裏默認爲 1.0,表示一秒,會影響最大可存儲的許可數。

3.1.2 RateLimiter setRate 方法詳解

RateLimiter#setRate

public final void setRate(double permitsPerSecond) {
    checkArgument(
        permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive");
    synchronized (mutex()) { // @1
      doSetRate(permitsPerSecond, stopwatch.readMicros()); // @2
    }
}

代碼@1:該方法需要獲取該類的監視器,在同步代碼塊中執行,實現線程安全性。

代碼@2:調用 doSetRate 設置速率,將調用其具體實現類 SmoothRateLimiter 的 doSetRate 方法。

SmoothRateLimiter#doSetRate

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

代碼@1:先來介紹一下該方法的參數的含義:

  • double permitsPerSecond
    每秒的許可數,即TPS。
  • long nowMicros
    系統已運行時間。

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

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

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

在介紹 SmoothBursty 的 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:根據當前時間可增加的許可數量,在 SmoothBursty 的 coolDownIntervalMicros 方法返回的就是上文提到的 stableIntervalMicros (發放一個許可所需要的時間),故本次可以增加的許可數的算法也好理解,即用當前時間戳減去 nextFreeTicketMicros 的差值,再除以發送一個許可所需要的時間即可。

代碼@3:計算當前可用的許可。

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

接下來再繼續看 SmoothBursty 的 doSetRate 方法。

SmoothBursty#doSetRate

void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
    double oldMaxPermits = this.maxPermits;
    maxPermits = maxBurstSeconds * permitsPerSecond;
    if (oldMaxPermits == Double.POSITIVE_INFINITY) {
        storedPermits = maxPermits;
    } else {
        storedPermits =
            (oldMaxPermits == 0.0)
                ? 0.0 // initial state
                : storedPermits * maxPermits / oldMaxPermits;
    }
}

這裏主要是初始化 storedPermits 的值,該限速器支持在運行過程中動態改變 permitsPerSecond 的值。

3.2 SmoothBursty 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

final 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 方法在 SmoothBursty 的實現中默認返回 0,即 SmoothBursty 的等待時間主要來自按照速率生成 freshPermits 個許可的時間,生成一個許可的時間爲 stableIntervalMicros,故需要等待的時長爲 freshPermits * stableIntervalMicros。

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

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

代碼@7:請注意這裏返回的 returnValue 的值,並沒有包含由於剩餘許可需要等待創建新許可的時間,即允許一定的突發流量,故本次計算需要的等待時間將對下一次請求生效,這也是框架作者將該限速器取名爲 SmoothBursty 的緣由。

SmoothBursty 的 acquire 方法就介紹到這裏了。

4、總結

由於源碼分析會顯得枯燥與不直觀,我們先給出如下流程圖:
在這裏插入圖片描述
SmoothBursty 的核心設計思想基本與令牌桶類似,但還是有些不同。
基本思想:

  1. SmoothBursty 以指定的速率生成許可,在 SmoothBursty 中用 storedPermits 表示。
  2. 當一個請求需要申請許可時,如果需要申請的許可數小於 storedPermits ,則消耗指定許可,直接返回,無需等待。
  3. 當一個請求需要申請的許可大於 storedPermits 時,則計算需要等待的時間,更新下一次許可可發放時間,直接返回,即當請求消耗掉所有許可後,當前請求並不會阻塞,而是影響下一個請求,即支持突發流量。

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


歡迎加筆者微信號(dingwpmz),加羣探討,筆者優質專欄目錄:
1、源碼分析RocketMQ專欄(40篇+)
2、源碼分析Sentinel專欄(12篇+)
3、源碼分析Dubbo專欄(28篇+)
4、源碼分析Mybatis專欄
5、源碼分析Netty專欄(18篇+)
6、源碼分析JUC專欄
7、源碼分析Elasticjob專欄
8、Elasticsearch專欄(20篇+)
9、源碼分析MyCat專欄

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