天A君突然發現自己的接口請求量突然漲到之前的10倍,沒多久該接口幾乎不可使用,並引發連鎖反應導致整個系統崩潰。如何應對這種情況呢?生活給了我們答案:比如老式電閘都安裝了保險絲,一旦有人使用超大功率的設備,保險絲就會燒斷以保護各個電器不被強電流給燒壞。同理我們的接口也需要安裝上“保險絲”,以防止非預期的請求對系統壓力過大而引起的系統癱瘓,當流量過大時,可以采取拒絕或者引流等機制。
二、常用的限流算法
常用的限流算法有兩種:漏桶算法和令牌桶算法。
1、漏桶算法
漏桶算法思路很簡單,水(請求)先進入到漏桶里,漏桶以一定的速度出水,當水流入速度過大會直接溢出,可以看出漏桶算法能強行限制數據的傳輸速率。
圖1 漏桶算法示意圖
在某些情況下,漏桶算法不能夠有效地使用網絡資源。因為漏桶的漏出速率是固定的參數,所以即使網絡中不存在資源沖突(沒有發生擁塞),漏桶算法也不能使某一個單獨的流突發到端口速率。因此,漏桶算法對於存在突發特性的流量來說缺乏效率。而令牌桶算法則能夠滿足這些具有突發特性的流量。通常,漏桶算法與令牌桶算法可以結合起來為網絡流量提供更大的控制。
2、令牌桶算法:
對於很多應用場景來說,除了要求能夠限制數據的平均傳輸速率外,還要求允許某種程度的突發傳輸。這時候漏桶算法可能就不合適了,令牌桶算法更為適合。如圖2所示,令牌桶算法的原理是系統會以一個恆定的速度往桶里放入令牌,而如果請求需要被處理,則需要先從桶里獲取一個令牌,當桶里沒有令牌可取時,則拒絕服務。
圖2 令牌桶算法示意圖
令牌桶算法的基本過程如下:
假如用戶配置的平均發送速率為r,則每隔1/r秒一個令牌被加入到桶中;
假設桶最多可以存發b個令牌。如果令牌到達時令牌桶已經滿了,那么這個令牌會被丟棄;
當一個n個字節的數據包到達時,就從令牌桶中刪除n個令牌,並且數據包被發送到網絡;
如果令牌桶中少於n個令牌,那么不會刪除令牌,並且認為這個數據包在流量限制之外;
算法允許最長b個字節的突發,但從長期運行結果看,數據包的速率被限制成常量r。對於在流量限制外的數據包可以以不同的方式處理:
它們可以被丟棄;
它們可以排放在隊列中以便當令牌桶中累積了足夠多的令牌時再傳輸;
它們可以繼續發送,但需要做特殊標記,網絡過載的時候將這些特殊標記的包丟棄。
二、兩種算法的區別
兩者主要區別在於“漏桶算法”能夠強行限制數據的傳輸速率,而“令牌桶算法”在能夠限制數據的平均傳輸速率外,還允許某種程度的突發傳輸。在“令牌桶算法”中,只要令牌桶中存在令牌,那么就允許突發地傳輸數據直到達到用戶配置的門限,所以它適合於具有突發特性的流量。
三、限流工具類RateLimiter
Google開源工具包Guava提供了限流工具類RateLimiter,該類基於令牌桶算法來完成限流,非常易於使用。RateLimiter類的接口描述請參考:RateLimiter接口描述,具體使用請參考:RateLimiter使用實踐。
下面是主要源碼:
public double acquire() { return acquire(1); } public double acquire(int permits) { checkPermits(permits); //檢查參數是否合法(是否大於0) long microsToWait; synchronized (mutex) { //應對並發情況需要同步 microsToWait = reserveNextTicket(permits, readSafeMicros()); //獲得需要等待的時間 } ticker.sleepMicrosUninterruptibly(microsToWait); //等待,當未達到限制時,microsToWait為0 return 1.0 * microsToWait / TimeUnit.SECONDS.toMicros(1L); } private long reserveNextTicket(double requiredPermits, long nowMicros) { resync(nowMicros); //補充令牌 long microsToNextFreeTicket = nextFreeTicketMicros - nowMicros; double storedPermitsToSpend = Math.min(requiredPermits, this.storedPermits); //獲取這次請求消耗的令牌數目 double freshPermits = requiredPermits - storedPermitsToSpend; long waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend) + (long) (freshPermits * stableIntervalMicros); this.nextFreeTicketMicros = nextFreeTicketMicros + waitMicros; this.storedPermits -= storedPermitsToSpend; // 減去消耗的令牌 return microsToNextFreeTicket; } private void resync(long nowMicros) { // if nextFreeTicket is in the past, resync to now if (nowMicros > nextFreeTicketMicros) { storedPermits = Math.min(maxPermits, storedPermits + (nowMicros - nextFreeTicketMicros) / stableIntervalMicros); nextFreeTicketMicros = nowMicros; } }
參考:漏桶算法和令牌桶算法