<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          經(jīng)典限流方法——漏桶、令牌桶與Guava RateLimiter的實現(xiàn)

          共 5603字,需瀏覽 12分鐘

           ·

          2020-07-10 00:28

          點擊上方藍色字體,選擇“設為星標

          回復”資源“獲取更多資源

          dd9b2f06fe33c1c331c3e459826be966.webp

          c5bb5f672cca27a9e51e04a8bc1a4a40.webp

          大數(shù)據(jù)技術與架構點擊右側關注,大數(shù)據(jù)開發(fā)領域最強公眾號!

          eea37b6aa6eb5545effe9e7430b05596.webp

          暴走大數(shù)據(jù)點擊右側關注,暴走大數(shù)據(jù)!8b71e48e512ef448ebf0f6abffafa8df.webp


          高并發(fā)的業(yè)務系統(tǒng)經(jīng)常要接受大流量的考驗,為了保證系統(tǒng)的響應度和穩(wěn)定性,往往都需要對有風險的接口實施限流(rate limiting),更高大上的說法則是“流量整形”(traffic shaping)。限流的思想最初來源于計算機網(wǎng)絡,有兩種經(jīng)典的方法:漏桶和令牌桶。本文先來稍微研究一下它們。

          漏桶(Leaky Bucket)

          將請求想象成水龍頭里流出的水,接口中有一個底部開孔的桶,如下圖所示。

          8e64b9ada013ad12c97e9d171c009f1c.webp

          這樣就能使請求處理的速率有一個穩(wěn)定的上限。很顯然,如果進水的速率比開孔出水的速率大的話,水(請求)就會逐漸在桶中積累。若一直積累直到溢出了,那些溢出去的水(請求)就會被直接丟棄,也就是拒絕服務(denial of service, DoS)。


          漏桶的思路非常簡單直接,易于實現(xiàn)。但是如果當前的網(wǎng)絡環(huán)境中充斥著短時流量尖峰,始終保持勻速的漏桶就無法很好地處理了,故后來又產(chǎn)生了令牌桶算法。


          令牌桶(Token Bucket)顧名思義,令牌桶里放的不再是請求,而是令牌,可以理解為處理請求的通行證。如下圖所示。724a05a516080fe296b7fd7e054de89b.webp令牌以恒定的速率產(chǎn)生并放入桶中。桶的大小是有限的,也就是說桶滿了之后,多余的令牌就扔掉了。每當請求到來時,需要從桶中獲取一個令牌才能真正地處理它。如果桶里沒有令牌,該請求就會被丟棄。
          可見,由于桶里預先留有一定量的令牌,故可以保證在突發(fā)流量到來且令牌沒耗盡前平滑地處理,比漏桶更靈活一些。
          Guava RateLimiter
          30f987b24eb9027cf24775ef793c259c.webpRateLimiter抽象類提供限流的所有功能,它的實現(xiàn)類只有SmoothRateLimiter。而SmoothRateLimiter的具體策略又由它的兩個內部子類來實現(xiàn)。
          • SmoothBursty:兼容突發(fā)流量的令牌桶實現(xiàn),也就是上一節(jié)描述的經(jīng)典令牌桶算法。

          • SmoothWarmingUp:帶預熱過程的令牌桶實現(xiàn),即在桶較滿時產(chǎn)生令牌的速度較慢,隨著令牌的消耗慢慢增長到恒定速率。如下圖所示,x軸為令牌數(shù),y軸為延遲,從右向左看的梯形區(qū)域就是令牌消耗的預熱過程。

          e7561ceb5f6d4da3205c4cce0dac931d.webp本文以經(jīng)典的SmoothBursty為范本來講。先說明一下SmoothRateLimiter的4個屬性的含義,它們都非常重要。
          • storedPermits:當前桶里有的令牌數(shù)量。

          • maxPermits:桶裝滿時的令牌數(shù)量,storedPermits不會比它大。

          • stableIntervalMicros:產(chǎn)生令牌的頻率(時間間隔),單位為微秒。舉個栗子,如果我們想限制系統(tǒng)的QPS為10,亦即每秒有10個令牌放入桶中,那么stableIntervalMicros的值就是100000。

          • nextFreeTicketMicros:下一個令牌可用的時間戳,也就是下一個請求能夠被處理的時間戳,單位為微秒。該值會隨著當前請求獲得令牌而增大(因為時間是自然流動的)。若當前請求的令牌數(shù)超出可用令牌數(shù),這個時間就被推后(需要時間產(chǎn)生新的令牌)。當然,如果有一段時間沒有請求進入的話,它就會保持在上次請求的過去時間戳。


          RateLimiter要發(fā)揮作用,首先得產(chǎn)生令牌,由SmoothRateLimiter.resync()方法來實現(xiàn)。
            void resync(long nowMicros) {    if (nowMicros > nextFreeTicketMicros) {      double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();      storedPermits = min(maxPermits, storedPermits + newPermits);      nextFreeTicketMicros = nowMicros;    }  }
          // SmoothBursty.coolDownIntervalMicros() @Override double coolDownIntervalMicros() { return stableIntervalMicros; }
          在當前時間戳大于nextFreeTicketMicros(即后者是一個過去的時間戳)的情況下,就用它們的時間差除以產(chǎn)生令牌的頻率,結果就是這段時間內應該產(chǎn)生的令牌數(shù),進而更新桶內的現(xiàn)有令牌數(shù)storedPermits,以及將nextFreeTicketMicros更新到現(xiàn)在。
          可見,RateLimiter的令牌是延遲(lazy)生成的,也就是說每次受理當前請求時,如果系統(tǒng)已經(jīng)空閑了一定時間,才會計算上次請求到當前時間應該產(chǎn)生多少個令牌,而不是使用單獨的任務來定期產(chǎn)生令牌——因為定時器無法保證較高的精度,并且性能不佳。當然,如果令牌已經(jīng)“超支”,當前就不需要再更新令牌了。
          接下來則是獲取令牌,由SmoothRateLimiter.reserveEarliestAvailable()方法來實現(xiàn),這個方法名非常self-explanatory了。
            @Override  final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {    resync(nowMicros);    long returnValue = nextFreeTicketMicros;    double storedPermitsToSpend = min(requiredPermits, this.storedPermits);    double freshPermits = requiredPermits - storedPermitsToSpend;    long waitMicros =        storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)            + (long) (freshPermits * stableIntervalMicros);
          this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros); this.storedPermits -= storedPermitsToSpend; return returnValue; }
          // SmoothBursty.storedPermitsToWaitTime() @Override long storedPermitsToWaitTime(double storedPermits, double permitsToTake) { return 0L; }
          這個方法的執(zhí)行流程如下:
          • 調用上述resync()方法產(chǎn)生令牌。

          • 計算實際能提供的令牌數(shù)storedPermitsToSpend,它其實就是本次請求需要的令牌數(shù)requiredPermits和桶中有的令牌數(shù)storedPermits的較小值。

          • 計算需要新產(chǎn)生的令牌數(shù)freshPermits。當上一步桶中有的令牌不夠用時,該值就大于0。

          • 根據(jù)freshPermits計算新產(chǎn)生這批令牌需要多長時間,記為waitMicros。由于SmoothBursty始終以恒定速率產(chǎn)生令牌,只需要將它與令牌產(chǎn)生的速率簡單相乘就行。SmoothWarmingUp需要考慮預熱的延時,所以storedPermitsToWaitTime()方法實現(xiàn)要復雜得多。

          • 更新nextFreeTicketMicros和storedPermits的值。

          弄明白了產(chǎn)生令牌和獲取令牌的細節(jié),我們回到RateLimiter的頂層,看看它到底是如何發(fā)揮作用的。
          Guava提供了多個靜態(tài)create()方法創(chuàng)建RateLimiter,其中創(chuàng)建SmoothBursty的源碼如下。
            public static RateLimiter create(double permitsPerSecond) {    return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer());  }
          @VisibleForTesting static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) { RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */); rateLimiter.setRate(permitsPerSecond); return rateLimiter; }
          可見,我們創(chuàng)建RateLimiter時只需要指定期望的QPS。那么SmoothBursty構造方法中的maxBurstSeconds參數(shù)有什么用?顧名思義,它表示在令牌桶滿時能夠承受的突發(fā)流量時長,這樣就能夠確定出桶的最大容量maxPermits了:
          maxPermits?=?maxBurstSeconds?*?permitsPerSecond;
          不過maxBurstSeconds當前不支持自定義,Guava規(guī)定為1秒,故桶的最大容量總是與我們指定的QPS相同。
          RateLimiter向用戶提供了acquire()方法用于獲取令牌,以及帶超時的tryAcquire()方法。下面的代碼示出從acquire()到reserveEarliestAvailable()的調用鏈,方法都很短。
            @CanIgnoreReturnValue  public double acquire() {    return acquire(1);  }
          @CanIgnoreReturnValue public double acquire(int permits) { long microsToWait = reserve(permits); stopwatch.sleepMicrosUninterruptibly(microsToWait); return 1.0 * microsToWait / SECONDS.toMicros(1L); }
          final long reserve(int permits) { checkPermits(permits); synchronized (mutex()) { return reserveAndGetWaitLength(permits, stopwatch.readMicros()); }
          final long reserveAndGetWaitLength(int permits, long nowMicros) { long momentAvailable = reserveEarliestAvailable(permits, nowMicros); return max(momentAvailable - nowMicros, 0); }
          可見:
          • 獲取令牌的過程是加鎖的。

          • reserveAndGetWaitLength()返回的是獲取令牌需要等待的時間,在acquire()方法中會借助Stopwatch阻塞(睡眠)直到獲取成功。Stopwatch是Guava自行實現(xiàn)的一個高精度計時器。

          • acquire()方法會將上述等待時間返回,但不需要用戶再處理了,所以該返回值可以忽略(即@CanIgnoreReturnValue注解的含義)。

          最后舉個簡單的例子吧。
          public class RateLimiterExample {public static void main(String[] args) throws Exception {    RateLimiter rateLimiter = RateLimiter.create(10);    Random random = new Random();for (int i = 0; i < 20; i++) {int numPermits = random.nextInt(20);      System.out.println(numPermits + "\t" + rateLimiter.acquire(numPermits));    }  }}
          運行結果如下。
          2   0.013  0.1987924   1.2940886   0.3962218  0.5951612  1.79779814  1.19866414  1.39732113  1.3962616  1.2995343   1.5954539   0.2993254   0.89885718  0.3949732   1.79583513  0.19592611  1.2948512   1.095513   0.1948576   0.297884
          可見,每次獲取的令牌數(shù)和獲取時需要等待的時長是符合上面所述邏輯的預期的。


          歡迎點贊+收藏+轉發(fā)朋友圈素質三連


          文章不錯?點個【在看】吧!??

          瀏覽 52
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美成人在线性爱 | 亚洲色图欧美日韩 | 高圆圆一区二区三区 | 久久久人妻无码精品蜜桃 | 色天使av|