限流分析(Guava RateLimter)

通常系統爲了防止服務被調用的QPS超出其承載能力,防止大流量壓垮服務器形成雪崩後果,設計時每每會加入限流的邏輯。經過限流,當請求超出系統的服務能力時,系統能夠採起拒絕服務/排隊等待/降級等策略保護自身。git

常見的限流算法

計數器法

原理:單位時間內計數,對請求計數,當超過設定的限流值,系統對請求採起限流策略;當單位時間結束時,計數器清零,從新開始計數;github

優勢:實現簡單,容易理解;算法

缺點:不能實現均衡的限流,好比在單位時間快結束時發起大量請求,並在下一次單位時間剛開始發起大量請求,這樣就會對系統請求形成連續兩個峯值,有可能壓垮系統,不能有效的把請求均攤;緩存

若是單位時間越小,限流的精度就會越高。服務器

草圖

漏桶算法

原理:請求像一個水管裏的水流同樣注入到一個漏桶裏,漏桶以恆定的速度往外漏水(不管進入的桶裏的水流速度是多少);當桶滿了,水流則會溢出;這樣當流量再大也會以恆定的速度調用後臺服務,或者低於設定的速度(當請求流量不足時);工具

優勢:漏桶能夠起到削峯,平滑請求的做用,ui

缺點:不能有效的利用系統資源,即時在系統可乘受住的峯值進入時,也不能加快處理請求,總體上會放緩系統對請求的處理速度。this

輸入圖片說明

令牌桶

原理:令牌桶是一個桶能夠容納有限的令牌,令牌是以固定的速度往令牌桶裏不停的放,若是令牌桶滿了,令牌着放不進去溢出;當有請求時,根據請求量去取相應的令牌數;google

優勢:相比漏桶,令牌桶容許必定的突發流量,請求空閒時預熱一部分令牌,新請求進來時無需等待。.net

缺點:代碼實現相對複雜一些。

輸入圖片說明

RateLimiter 分析

RateLimiter簡介

RateLimiter是Guava工具包提供的限流工具,採用令牌桶算法實現。 RateLimiter經過配置固定的rate參數,來以恆定的速度分發許可。同時RateLimiter提供了預熱模式來分發許可,逐步達到配置的rate速度(好比在系統緩存尚未預熱好時,過多的請求會給系統帶來過大壓力,預熱模式能夠很好的解決這個問題)。若是RateLimiter在預熱階段內,不被調用了,它會逐漸的回到冷卻狀態,再次調用時,在像剛開始同樣從新預熱。RateLimiter提供了acquire(阻塞)和tryAcquire(非阻塞)兩種模式獲取許可。

相關接口

  • 建立實例接口

    • create(double permitsPerSecond)
    • create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) 預熱模式建立
  • 獲取permits

    • acquire(int permits) 阻塞模式獲取,直到等到permits數量知足要求。
    • tryAcquire(long timeout, TimeUnit unit) 指定超時時間獲取
    • tryAcquire(int permits) 非阻塞模式獲取

請求令牌數量不影響請求自己限流的效果(若是acquire(1)和acquire(1000)觸發了限流,對該請求限流結果是同樣的),它只會影響接下來請求的限流效果,好比若是上一次請求開銷很大,拿去了不少的permits,那接下來的高開銷請求可能會被限制。

使用示例

  • 有一些任務要執行,假設每秒最多不能執行2個以上的任務。

    final RateLimiter rateLimiter = RateLimiter.create(2.0); // rate is "2 permits per second"
    
      void submitTasks(List<Runnable> tasks, Executor executor) {
          for (Runnable task : tasks) {
              rateLimiter.acquire(); // may wait
              executor.execute(task);
          }
      }
* 另一個例子,假設咱們生產數據流,想以5kb/s 的速度進行發送,能夠採用下面的方式實現。
final RateLimiter rateLimiter = RateLimiter.create(5000.0); // rate = 5000 permits per second

void submitPacket(byte[] packet) {
      rateLimiter.acquire(packet.length);
      networkService.send(packet);
}
### RateLimiter實現原理

這裏經過對RateLimiter擴展實現類的SmoothRateLimiter註解進行翻譯加上我的理解,逐步講述RateLimiter設計實現。

假定對於實現固定QPS的訪問速度限定,能夠經過記錄上一次請求受權的timestamp,而後當1/QPS時間度事後分發一個permit。例如,固定速率 QPS=5,若是當上一個請求過去不到200ms,確保新請求不被受權,咱們就能夠達到想要的限定rate。

RateLimiter對過去的記錄不多,假設它只記錄了上一次請求。若是RateLimiter長時間沒有被使用,新的請求到達時,可否當即被受權?RateLimiter將忘掉過去的未使用的受權。依照現實世界,當使用沒有達到設定的rate時,perimits將會利用率不足,或者溢出。過去的利用不足(past underutilization)意味着過量的資源是可用的,那麼RateLimiter能夠經過加速一會來充分利用這些資源。例如當限定bandwidth時,過去資源利用不足意味着幾乎爲空的buffers能夠經過加速瞬速被填滿。過去利用不足意味着服務器可能對處理將來新的請求準備不充分,好比服務器的緩存失效,或者新請求可能觸發開銷比較大的操做。

爲了應對這些場景,RateLimiter添加了一個額外的維度,把過去的利用不足(past underutilization)設爲 storedPermits變量。當沒有利用不足時,storedPermits爲0,若是有充分的利用不足發生時,storedPermits逐漸增長到maxStoredPermits,當有新新請求調用acquire(permits)時,若是storedPermits>permits,則直接返還回對應的permits(這裏在預熱模式實現會不一樣,預熱模式一樣會讓請求等待),若是不足,則不足部分等到新的permits。


經過下邊的例子解釋這種工做機制:

例如RateLimiter每秒產生一個token,若是接下來RateLimiter沒有被利用,那麼咱們把storedPermits加1,假設咱們10s沒有用RateLimiter,那麼storedPermits將爲10(設定maxStoredPermits >= 10.0)。這時一個acquire(3)的請求到達,咱們直接從storedPermits中取出3個來服務,瞬間這時有個acquire(10)的請求到達,咱們能夠經過storedPermits中取出剩餘的7個,而後剩下的3個經過RateLimiter新的生產出3個permits來服務。

咱們已經知道分發3個新permits將會花費3秒,那麼如何分發7個stored permits?這個沒有統一的答案。若是咱們但願處理利用不足問題,那麼咱們可能會把更快的或者不讓請求等待直接從storedPermits中取出,若是咱們擔憂資源使用過分或者預熱不足,咱們能夠放慢分發storedPermits,所以咱們須要一個function把storedPermits轉換爲限流時間。

這個function 在RateLimiter的擴展類SmoothRateLimiter中是storedPermitsToWaitTime(double storedPermits, double permitsToTake),這個function把storedPermits轉換爲等待時間。這個在SmoothBursty和SmoothWarmingUp是不一樣的實現,SmoothBursty直接返回無需等待,SmoothWarmingUp則要計數出等待時間。

最後一點是,若是RateLimiter 採用QPS=1的限定速度,那麼開銷較大的acquire(100)請求到達時,它是不必等到100s 纔開始實際任務。咱們能夠先開始任務執行,並把將來的請求推後100s,這樣咱們就能夠同時工做,同時生產須要的permits,而不是空等待permits生產夠纔開始工做。這樣的話那麼RateLimiter不須要記住time of the _last_ request,它只須要記住
   the (expected) time of the _next_ request。咱們經過 (now -  the expected time of the _next_ request )計算出爲利用的時間段t,經過t*QPS計算出storedPermits。這個方法就是 SmoothRateLimiter 中的reserveEarliestAvailable方法。


### 關鍵方法分析
*  SmoothRateLimiter中的 reserveEarliestAvailable

//該方法計算知足當前requiredPermits數量的「時刻」 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; }

 
void resync(long nowMicros) {
// if nextFreeTicket is in the past, resync to now
//nextFreeTicketMicros 是上文中咱們講的the expected time of the _next_ request
if (nowMicros > nextFreeTicketMicros) {
  double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
  storedPermits = min(maxPermits, storedPermits + newPermits);
  nextFreeTicketMicros = nowMicros;
}

}

 

//非預熱模式 SmoothBursty double coolDownIntervalMicros() { // 等價於 1/QPS return stableIntervalMicros; }

* SmoothWarmingUp中的storedPermitsToWaitTime

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

![輸入圖片說明](https://static.oschina.net/uploads/img/201611/19221636_NrQQ.png "在這裏輸入圖片標題")

方法中設計到的參數計算方式這裏不細講了。

[https://my.oschina.net/robinyao/blog/790890](https://my.oschina.net/robinyao/blog/790890)
相關文章
相關標籤/搜索