超詳細的Guava RateLimiter限流原理解析

 點擊上方「方誌朋」,選擇「置頂或者星標」java

你的關注意義重大!nginx

 

限流是保護高併發系統的三把利器之一,另外兩個是緩存和降級。限流在不少場景中用來限制併發和請求量,好比說秒殺搶購,保護自身系統和下游系統不被巨型流量沖垮等。面試

 限流的目的是經過對併發訪問/請求進行限速或者一個時間窗口內的的請求進行限速來保護系統,一旦達到限制速率則能夠拒絕服務或進行流量整形。redis

 經常使用的限流方式和場景有:限制總併發數(好比數據庫鏈接池、線程池)、限制瞬時併發數(如nginx的limitconn模塊,用來限制瞬時併發鏈接數,Java的Semaphore也能夠實現)、限制時間窗口內的平均速率(如Guava的RateLimiter、nginx的limitreq模塊,限制每秒的平均速率);其餘還有如限制遠程接口調用速率、限制MQ的消費速率。另外還能夠根據網絡鏈接數、網絡流量、CPU或內存負載等來限流。算法

 好比說,咱們須要限制方法被調用的併發數不能超過100(同一時間併發數),則咱們能夠用信號量 Semaphore實現。可若是咱們要限制方法在一段時間內平均被調用次數不超過100,則須要使用 RateLimiter數據庫

限流的基礎算法

 咱們先來說解一下兩個限流相關的基本算法:漏桶算法和令牌桶算法。編程

 

 

 從上圖中,咱們能夠看到,就像一個漏斗同樣,進來的水量就好像訪問流量同樣,而出去的水量就像是咱們的系統處理請求同樣。當訪問流量過大時,這個漏斗中就會積水,若是水太多了就會溢出。segmentfault

 漏桶算法的實現每每依賴於隊列,請求到達若是隊列未滿則直接放入隊列,而後有一個處理器按照固定頻率從隊列頭取出請求進行處理。若是請求量大,則會致使隊列滿,那麼新來的請求就會被拋棄。緩存

 

 

      令牌桶算法則是一個存放固定容量令牌的桶,按照固定速率往桶裏添加令牌。桶中存放的令牌數有最大上限,超出以後就被丟棄或者拒絕。當流量或者網絡請求到達時,每一個請求都要獲取一個令牌,若是可以獲取到,則直接處理,而且令牌桶刪除一個令牌。若是獲取不一樣,該請求就要被限流,要麼直接丟棄,要麼在緩衝區等待。性能優化

 

 

令牌桶和漏桶對比:

  • 令牌桶是按照固定速率往桶中添加令牌,請求是否被處理須要看桶中令牌是否足夠,當令牌數減爲零時則拒絕新的請求;漏桶則是按照常量固定速率流出請求,流入請求速率任意,當流入的請求數累積到漏桶容量時,則新流入的請求被拒絕;

  • 令牌桶限制的是平均流入速率,容許突發請求,只要有令牌就能夠處理,支持一次拿3個令牌,4個令牌;漏桶限制的是常量流出速率,即流出速率是一個固定常量值,好比都是1的速率流出,而不能一次是1,下次又是2,從而平滑突發流入速率;

  • 令牌桶容許必定程度的突發,而漏桶主要目的是平滑流出速率;

Guava RateLimiter

  Guava是Java領域優秀的開源項目,它包含了Google在Java項目中使用一些核心庫,包含集合(Collections),緩存(Caching),併發編程庫(Concurrency),經常使用註解(Common annotations),String操做,I/O操做方面的衆多很是實用的函數。  Guava的 RateLimiter提供了令牌桶算法實現:平滑突發限流(SmoothBursty)和平滑預熱限流(SmoothWarmingUp)實現。

 

 

  RateLimiter的類圖如上所示,其中 RateLimiter是入口類,它提供了兩套工廠方法來建立出兩個子類。這很符合《Effective Java》中的用靜態工廠方法代替構造函數的建議,畢竟該書的做者也正是Guava庫的主要維護者,兩者配合"食用"更佳。

  1. // RateLimiter提供了兩個工廠方法,最終會調用下面兩個函數,生成RateLimiter的兩個子類。

  2. static RateLimiter create(SleepingStopwatch stopwatch, double permitsPerSecond) {

  3. RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);

  4. rateLimiter.setRate(permitsPerSecond);

  5. return rateLimiter;

  6. }

  7. static RateLimiter create(

  8. SleepingStopwatch stopwatch, double permitsPerSecond, long warmupPeriod, TimeUnit unit,

  9. double coldFactor) {

  10. RateLimiter rateLimiter = new SmoothWarmingUp(stopwatch, warmupPeriod, unit, coldFactor);

  11. rateLimiter.setRate(permitsPerSecond);

  12. return rateLimiter;

  13. }

平滑突發限流

 使用 RateLimiter的靜態方法建立一個限流器,設置每秒放置的令牌數爲5個。返回的RateLimiter對象能夠保證1秒內不會給超過5個令牌,而且以固定速率進行放置,達到平滑輸出的效果。

  1. public void testSmoothBursty() {

  2. RateLimiter r = RateLimiter.create(5);

  3. while (true) {

  4. System.out.println("get 1 tokens: " + r.acquire() + "s");

  5. }

  6. /**

  7. * output: 基本上都是0.2s執行一次,符合一秒發放5個令牌的設定。

  8. * get 1 tokens: 0.0s

  9. * get 1 tokens: 0.182014s

  10. * get 1 tokens: 0.188464s

  11. * get 1 tokens: 0.198072s

  12. * get 1 tokens: 0.196048s

  13. * get 1 tokens: 0.197538s

  14. * get 1 tokens: 0.196049s

  15. */

  16. }

  RateLimiter使用令牌桶算法,會進行令牌的累積,若是獲取令牌的頻率比較低,則不會致使等待,直接獲取令牌。

  1. public void testSmoothBursty2() {

  2. RateLimiter r = RateLimiter.create(2);

  3. while (true)

  4. {

  5. System.out.println("get 1 tokens: " + r.acquire(1) + "s");

  6. try {

  7. Thread.sleep(2000);

  8. } catch (Exception e) {}

  9. System.out.println("get 1 tokens: " + r.acquire(1) + "s");

  10. System.out.println("get 1 tokens: " + r.acquire(1) + "s");

  11. System.out.println("get 1 tokens: " + r.acquire(1) + "s");

  12. System.out.println("end");

  13. /**

  14. * output:

  15. * get 1 tokens: 0.0s

  16. * get 1 tokens: 0.0s

  17. * get 1 tokens: 0.0s

  18. * get 1 tokens: 0.0s

  19. * end

  20. * get 1 tokens: 0.499796s

  21. * get 1 tokens: 0.0s

  22. * get 1 tokens: 0.0s

  23. * get 1 tokens: 0.0s

  24. */

  25. }

  26. }

  RateLimiter因爲會累積令牌,因此能夠應對突發流量。在下面代碼中,有一個請求會直接請求5個令牌,可是因爲此時令牌桶中有累積的令牌,足以快速響應。   RateLimiter在沒有足夠令牌發放時,採用滯後處理的方式,也就是前一個請求獲取令牌所需等待的時間由下一次請求來承受,也就是代替前一個請求進行等待。

  1. public void testSmoothBursty3() {

  2. RateLimiter r = RateLimiter.create(5);

  3. while (true)

  4. {

  5. System.out.println("get 5 tokens: " + r.acquire(5) + "s");

  6. System.out.println("get 1 tokens: " + r.acquire(1) + "s");

  7. System.out.println("get 1 tokens: " + r.acquire(1) + "s");

  8. System.out.println("get 1 tokens: " + r.acquire(1) + "s");

  9. System.out.println("end");

  10. /**

  11. * output:

  12. * get 5 tokens: 0.0s

  13. * get 1 tokens: 0.996766s 滯後效應,須要替前一個請求進行等待

  14. * get 1 tokens: 0.194007s

  15. * get 1 tokens: 0.196267s

  16. * end

  17. * get 5 tokens: 0.195756s

  18. * get 1 tokens: 0.995625s 滯後效應,須要替前一個請求進行等待

  19. * get 1 tokens: 0.194603s

  20. * get 1 tokens: 0.196866s

  21. */

  22. }

  23. }

平滑預熱限流

  RateLimiter的 SmoothWarmingUp是帶有預熱期的平滑限流,它啓動後會有一段預熱期,逐步將分發頻率提高到配置的速率。  好比下面代碼中的例子,建立一個平均分發令牌速率爲2,預熱期爲3分鐘。因爲設置了預熱時間是3秒,令牌桶一開始並不會0.5秒發一個令牌,而是造成一個平滑線性降低的坡度,頻率愈來愈高,在3秒鐘以內達到本來設置的頻率,之後就以固定的頻率輸出。這種功能適合系統剛啓動須要一點時間來「熱身」的場景。

  1. public void testSmoothwarmingUp() {

  2. RateLimiter r = RateLimiter.create(2, 3, TimeUnit.SECONDS);

  3. while (true)

  4. {

  5. System.out.println("get 1 tokens: " + r.acquire(1) + "s");

  6. System.out.println("get 1 tokens: " + r.acquire(1) + "s");

  7. System.out.println("get 1 tokens: " + r.acquire(1) + "s");

  8. System.out.println("get 1 tokens: " + r.acquire(1) + "s");

  9. System.out.println("end");

  10. /**

  11. * output:

  12. * get 1 tokens: 0.0s

  13. * get 1 tokens: 1.329289s

  14. * get 1 tokens: 0.994375s

  15. * get 1 tokens: 0.662888s 上邊三次獲取的時間相加正好爲3秒

  16. * end

  17. * get 1 tokens: 0.49764s 正常速率0.5秒一個令牌

  18. * get 1 tokens: 0.497828s

  19. * get 1 tokens: 0.49449s

  20. * get 1 tokens: 0.497522s

  21. */

  22. }

  23. }

源碼分析

 看完了 RateLimiter的基本使用示例後,咱們來學習一下它的實現原理。先了解一下幾個比較重要的成員變量的含義。

  1. //SmoothRateLimiter.java

  2. //當前存儲令牌數

  3. double storedPermits;

  4. //最大存儲令牌數

  5. double maxPermits;

  6. //添加令牌時間間隔

  7. double stableIntervalMicros;

  8. /**

  9. * 下一次請求能夠獲取令牌的起始時間

  10. * 因爲RateLimiter容許預消費,上次請求預消費令牌後

  11. * 下次請求須要等待相應的時間到nextFreeTicketMicros時刻才能夠獲取令牌

  12. */

  13. private long nextFreeTicketMicros = 0L;

平滑突發限流

  RateLimiter的原理就是每次調用 acquire時用當前時間和 nextFreeTicketMicros進行比較,根據兩者的間隔和添加單位令牌的時間間隔 stableIntervalMicros來刷新存儲令牌數 storedPermits。而後acquire會進行休眠,直到 nextFreeTicketMicros

  acquire函數以下所示,它會調用 reserve函數計算獲取目標令牌數所需等待的時間,而後使用 SleepStopwatch進行休眠,最後返回等待時間。

  1. public double acquire(int permits) {

  2. // 計算獲取令牌所需等待的時間

  3. long microsToWait = reserve(permits);

  4. // 進行線程sleep

  5. stopwatch.sleepMicrosUninterruptibly(microsToWait);

  6. return 1.0 * microsToWait / SECONDS.toMicros(1L);

  7. }

  8. final long reserve(int permits) {

  9. checkPermits(permits);

  10. // 因爲涉及併發操做,因此使用synchronized進行併發操做

  11. synchronized (mutex()) {

  12. return reserveAndGetWaitLength(permits, stopwatch.readMicros());

  13. }

  14. }

  15. final long reserveAndGetWaitLength(int permits, long nowMicros) {

  16. // 計算從當前時間開始,可以獲取到目標數量令牌時的時間

  17. long momentAvailable = reserveEarliestAvailable(permits, nowMicros);

  18. // 兩個時間相減,得到須要等待的時間

  19. return max(momentAvailable - nowMicros, 0);

  20. }

  reserveEarliestAvailable是刷新令牌數和下次獲取令牌時間 nextFreeTicketMicros的關鍵函數。它有三個步驟,一是調用 resync函數增長令牌數,二是計算預支付令牌所需額外等待的時間,三是更新下次獲取令牌時間 nextFreeTicketMicros和存儲令牌數 storedPermits

 這裏涉及 RateLimiter的一個特性,也就是能夠預先支付令牌,而且所需等待的時間在下次獲取令牌時再實際執行。詳細的代碼邏輯的解釋請看註釋。

  1. final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {

  2. // 刷新令牌數,至關於每次acquire時在根據時間進行令牌的刷新

  3. resync(nowMicros);

  4. long returnValue = nextFreeTicketMicros;

  5. // 獲取當前已有的令牌數和須要獲取的目標令牌數進行比較,計算出能夠目前便可獲得的令牌數。

  6. double storedPermitsToSpend = min(requiredPermits, this.storedPermits);

  7. // freshPermits是須要預先支付的令牌,也就是目標令牌數減去目前便可獲得的令牌數

  8. double freshPermits = requiredPermits - storedPermitsToSpend;

  9. // 由於會忽然涌入大量請求,而現有令牌數又不夠用,所以會預先支付必定的令牌數

  10. // waitMicros便是產生預先支付令牌的數量時間,則將下次要添加令牌的時間應該計算時間加上watiMicros

  11. long waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)

  12. + (long) (freshPermits * stableIntervalMicros);

  13. // storedPermitsToWaitTime在SmoothWarmingUp和SmoothBuresty的實現不一樣,用於實現預熱緩衝期

  14. // SmoothBuresty的storedPermitsToWaitTime直接返回0,因此watiMicros就是預先支付的令牌所需等待的時間

  15. try {

  16. // 更新nextFreeTicketMicros,本次預先支付的令牌所需等待的時間讓下一次請求來實際等待。

  17. this.nextFreeTicketMicros = LongMath.checkedAdd(nextFreeTicketMicros, waitMicros);

  18. } catch (ArithmeticException e) {

  19. this.nextFreeTicketMicros = Long.MAX_VALUE;

  20. }

  21. // 更新令牌數,最低數量爲0

  22. this.storedPermits -= storedPermitsToSpend;

  23. // 返回舊的nextFreeTicketMicros數值,無需爲預支付的令牌多加等待時間。

  24. return returnValue;

  25. }

  26. // SmoothBurest

  27. long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {

  28. return 0L;

  29. }

  resync函數用於增長存儲令牌,核心邏輯就是 (nowMicros-nextFreeTicketMicros)/stableIntervalMicros。當前時間大於 nextFreeTicketMicros時進行刷新,不然直接返回。

  1. void resync(long nowMicros) {

  2. // 當前時間晚於nextFreeTicketMicros,因此刷新令牌和nextFreeTicketMicros

  3. if (nowMicros > nextFreeTicketMicros) {

  4. // coolDownIntervalMicros函數獲取每機秒生成一個令牌,SmoothWarmingUp和SmoothBuresty的實現不一樣

  5. // SmoothBuresty的coolDownIntervalMicros直接返回stableIntervalMicros

  6. // 當前時間減去要更新令牌的時間獲取時間間隔,再除以添加令牌時間間隔獲取這段時間內要添加的令牌數

  7. storedPermits = min(maxPermits,

  8. storedPermits

  9. + (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros());

  10. nextFreeTicketMicros = nowMicros;

  11. }

  12. // 若是當前時間早於nextFreeTicketMicros,則獲取令牌的線程要一直等待到nextFreeTicketMicros,該線程獲取令牌所需

  13. // 額外等待的時間由下一次獲取的線程來代替等待。

  14. }

  15. double coolDownIntervalMicros() {

  16. return stableIntervalMicros;

  17. }

 下面咱們舉個例子,讓你們更好的理解 resync和 reserveEarliestAvailable函數的邏輯。

 好比 RateLimiter的 stableIntervalMicros爲500,也就是1秒發兩個令牌,storedPermits爲0,nextFreeTicketMicros爲155391849 5748。線程一acquire(2),當前時間爲155391849 6248,首先 resync函數計算,(1553918496248 - 1553918495748)/500 = 1,因此當前可獲取令牌數爲1,可是因爲能夠預支付,因此nextFreeTicketMicros= nextFreeTicketMicro + 1 * 500 = 155391849 6748。線程一無需等待。

 緊接着,線程二也來acquire(2),首先 resync函數發現當前時間早於 nextFreeTicketMicros,因此沒法增長令牌數,因此須要預支付2個令牌,nextFreeTicketMicros= nextFreeTicketMicro + 2 * 500 = 155391849 7748。線程二須要等待155391849 6748時刻,也就是線程一獲取時計算的nextFreeTicketMicros時刻。一樣的,線程三獲取令牌時也須要等待到線程二計算的nextFreeTicketMicros時刻。

平滑預熱限流

 上述就是平滑突發限流RateLimiter的實現,下面咱們來看一下加上預熱緩衝期的實現原理。   SmoothWarmingUp實現預熱緩衝的關鍵在於其分發令牌的速率會隨時間和令牌數而改變,速率會先慢後快。表現形式以下圖所示,令牌刷新的時間間隔由長逐漸變短。等存儲令牌數從maxPermits到達thresholdPermits時,發放令牌的時間價格也由coldInterval下降到了正常的stableInterval。

 

 

  SmoothWarmingUp的相關代碼以下所示,相關的邏輯都寫在註釋中。

  1. // SmoothWarmingUp,等待時間就是計算上圖中梯形或者正方形的面積。

  2. long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {

  3. /**

  4. * 當前permits超出閾值的部分

  5. */

  6. double availablePermitsAboveThreshold = storedPermits - thresholdPermits;

  7. long micros = 0;

  8. /**

  9. * 若是當前存儲的令牌數超出thresholdPermits

  10. */

  11. if (availablePermitsAboveThreshold > 0.0) {

  12. /**

  13. * 在閾值右側而且須要被消耗的令牌數量

  14. */

  15. double permitsAboveThresholdToTake = min(availablePermitsAboveThreshold, permitsToTake);

  16. /**

  17. * 梯形的面積

  18. *

  19. * 高 * (頂 * 底) / 2

  20. *

  21. * 高是 permitsAboveThresholdToTake 也就是右側須要消費的令牌數

  22. * 底 較長 permitsToTime(availablePermitsAboveThreshold)

  23. * 頂 較短 permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake)

  24. */

  25. micros = (long) (permitsAboveThresholdToTake

  26. * (permitsToTime(availablePermitsAboveThreshold)

  27. + permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake)) / 2.0);

  28. /**

  29. * 減去已經獲取的在閾值右側的令牌數

  30. */

  31. permitsToTake -= permitsAboveThresholdToTake;

  32. }

  33. /**

  34. * 平穩時期的面積,正好是長乘寬

  35. */

  36. micros += (stableIntervalMicros * permitsToTake);

  37. return micros;

  38. }

  39. double coolDownIntervalMicros() {

  40. /**

  41. * 每秒增長的令牌數爲 warmup時間/maxPermits. 這樣的話,在warmuptime時間內,就就增張的令牌數量

  42. * 爲 maxPermits

  43. */

  44. return warmupPeriodMicros / maxPermits;

  45. }

後記

  RateLimiter只能用於單機的限流,若是想要集羣限流,則須要引入 redis或者阿里開源的 sentinel中間件,請你們繼續關注。

參考

  • https://jinnianshilongnian.iteye.com/blog/2305117

  • https://segmentfault.com/a/1190000012875897

-更多文章-

微服務架構·基礎篇

數據庫中間件詳解 | 珍藏版

Java 性能優化的 45 個細節

哥們,你真覺得你會作這道JVM面試題?

如何閱讀Java源碼?

-關注我-

 

 

看完了,幫我點個「好看」鴨

點鴨點鴨

↓↓↓↓

相關文章
相關標籤/搜索