Guava之RateLimiter的設計

Guava源碼中很詳盡的解釋了RateLimiter的概念。

從概念上看,限流器以配置速率釋放允許的請求(permit)。如有必要,調用acquire()將會阻塞知道一個允許可用。一旦被獲?。╝cquired),允許(permits)將不必釋放。

限流器在并發環境中是安全的:它限制所有線程總的調用速率。但是,值得注意的是,它難以保證公平。
限流器經常被用來限制一些物理或邏輯資源被訪問的速率。經常和它對比的是j.u.c.Semaphore,它限制了訪問資源總的并發數。(并發數和速率緊密相關,參見Little's Law)

限流器原始定義為許可被發布的速率。沒有多余的配置,許可將被以固定速率(——字面意義被定義為 許可/sec) 分發。通過調節獨立的許可之間的延遲,保證許可按照配置的速率平滑分發。

在限流器正式進入穩定速率前,通常允許限流器有一個短暫的預熱階段。在該階段,許可分發速率穩步提升,直至預定到達速率為止。

舉例,想象我們有一組任務要執行,但我們不想要每秒提交超過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/sec速率恒定接收它。這個想法可以以限流器方式實現。即,每個許可對應一個字節,指定(限流器)恒定的速率為每秒5000次許可。

final RateLimiter rateLimiter = RateLimiter.create(5000.0); // rate = 5000 permits per second
   void submitPacket(byte[] packet) {
     rateLimiter.acquire(packet.length);
     networkService.send(packet);
   }

注意,請求的許可數量不會影響到對請求本身的壓制。(acquire(1)會和acquire(1000)產生相同的效果),但是它會影響到對下一個請求的壓制作用。如果成本較大的任務在空閑時到達限流器,將會被立即允許。但這之后的請求將會遭受額外的限制,為上次高昂代價的任務買單。

下面是一個SmoothRateLimiter的設計原理:

限流器的基本提點是一個“穩定的速率”,即在正常條件下的最大速率。未達到這個目的,限流器會根據需要壓制到達的請求。通過計算,限流器會確保到達請求等待合理的時間,以此達到壓制的目的。

維持一個速率(通常被指定為QPS)最簡單的方法是記住上個被允許請求的最后時間戳,并保證在1/QPS時間內不執行請求。例如,QPS=5(每秒5個token),如果我們能夠確保自從上個請求后,在200ms內沒有請求被允許執行,那么我們將獲得一個想要的速率。如果一個請求在上個請求被放行100ms后到達,那么我們需要等待額外的100ms。在這個速率下,15個新的許可耗時3秒鐘(例如對于請求 acquire(15))。

很重要的一點,是能夠意識到限流器對過去只有很淺的記憶。它只會記住上一個請求。那如果限流器很久沒有被使用,然后一個請求突然到達并被立即允許怎么辦?這可能會有兩種情形,一種是資源利用不充分,另一種則是導致溢出,具體取決于沒有遵循預定速率的真實原因。

之前的利用不足意味多余的資源可被獲取。限流器應該加速一段時間,以利用這些資源。速率適應網絡(帶寬)很重要,過去的利用不足被解釋為“幾乎為空的緩沖”,可以被快速填補。

另一方面,過去的利用不足也可能意味著“服務器沒有準備好處理將來的請求”,例如,緩存失效,請求更有可能會觸發耗時的操作(一個更極端的例子是,當一個服務器剛剛被引導,它更可能忙于自身的喚醒)。

為應對以上場景,我們增添一種維度。即“過去的利用不足”被建模為變量“storedPermits”。這個變量在沒有使用時為0,當有大量使用時,它可以增長到maxStoredPermits。所以,請求會被函數acquire(permits)觸發許可,提供以下兩種類型的許可:
-stored permits(可獲取的已存許可)
-fresh permits(新的的許可)

工作原理如下:

對于一個限流器,每秒產生一個令牌。不使用限流器時,我們都會給storedPermits加1。如果說我們有10sec不使用限流器(例如預計請求在時刻X到來,但在請求到來之前,我們在X+10。這也是上段所描述的點。)。因此storedPermits變為10(假設maxStoredPermits>=10)。在這時,一個人acquire(3)的請求到了。我們從已有的storedPermits拿出許可服務這個請求,并將許可數降至7.(這如何被解釋為壓制時間,將會在之后被詳細討論。)這之后,假設馬上有一個acquire(10)的請求到達,我們用剩下所有的7個許可數來應對這個請求,還有3個許可數,我們需要通過刷新限流器新提供。

我們也已經知道花費在3個新的許可上的時間:如果速率是1令牌/sec,那么我們將花費3秒。但是使用7個已存許可又是什么意思呢?正如上面所說,這里沒有固定答案。如果我們主要興趣在應對資源利用不足上,我們想要存儲許可釋放比刷新許可快。因為利用不足=尚有未被占用的資源。如果我們主要興趣點在應對溢出,那么存儲的許可數應該釋放的比刷新的慢。因此,我們想要一個(在每種情形都不同的)方法來解釋storePermits,以此壓制時間。storedPermitsToWaitTime(double storedPermits, double permitsToTake) 在其中扮演重要角色。底層的模型是一個持續變化的函數映射storedPermits(從0到maxStoredPermits)到1/rate(時間間隔)。storedPermits在衡量未使用時間上是必不可少的。我們使用未利用時間換取許可數(permits)。速率是permits/time,因此1/rate=time/permits.因此"1/rate"(time/permits)乘以permits等于給定時間。對于指定數量的請求許可來說,這個積分函數(storedPermitsToWaitTime()計算)與持續請求的最小時間間隔相關。

這里有個storePermitsToWaitTime的例子。如果storedPermits=10,我們想要3個permits,我們從storedPermits中去獲取,減少他們到7個,并且計算壓制時間作為一個調用storedPermitsToWaitTime(storedPermits=10,permitsToTake=3),這將會評估這個函數積分從7到10.

使用積分保證acquire(3)效果等同于3次acquire(1),或一次acquire(2)+一次acquire(1)。因為積分在[7.0,10.0]等同于在[7.0,8.0],[8.0,9.0],[9.0,10.0]等等。無論這個函數是什么。這使得我們可以正確處理不同權重(permits)的請求時,不論真正的函數是什么。所以我們可以自由調整。(唯一的條件顯然是我們能夠計算出他的間隔時間)。

注意,對于這個函數,我們選擇水平線,高度為1/QPS,因此這個函數的影響是不存在的。對于storedPermits將會完全等同于刷新一個新的(1/QPS是他的代價)。我們將會在之后使用這個小訣竅。

如果我們采用一個低于這條水平線的函數,這意味著我們減少了這個函數的區域,也就是時間。因此限流器就會在一段時間的利用不足后變快。另一方面,如果我們使用一個高于此水平線的函數,這就意味著代表時間的區域增大,因此storedPermits將會比刷新一個新許可更耗時,相應地,限流器就會在一段時間的利用不足后變慢。

最后,考慮一個限流器以1permit/sec速率,當前未被使用,有一個acquire(100)的請求到來。等待100sec才開始執行任務將會是很愚蠢的行為。為什么不作任何事情只等待呢?一個更好的方法是立刻允許請求(正如它是acquire(1)的請求一樣),并且按需要延緩此后的需求。在這個版本,我們允許立刻開始執行任務,并且延緩100秒之后的請求,因此我們允許工作執行而不是讓它空閑等待。

這里有很重要的因果關系。這意味著限流器不會記住最后請求的時刻,但它會記住下一個請求(預計)時間。這也使我們能夠立即知道(見tryAcquire(timeout))指定時間timeout是否足夠將我們帶到下一個調度的時間點,因為我們總維持那個。并且我們所指的“未被使用的限流器”也被這所定義:但我們觀察“下一個請求的期待到達時間”在過去,那么(now-past)的時間差將被看作RateLimiter未被正式使用時間。這也是被我們解釋為storedPermits的時間。(我們用空閑的時間產生的許可數來增加storedPermits)。所以,如果速率=1許可/sec,并且請求在之前那個請求后一秒后準時到達,那么storedPermits將永遠不會增加。我們只會在當晚于預期一秒時間的到達,才會增加它。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容