經(jīng)典限流方法——漏桶、令牌桶與Guava RateLimiter的實現(xiàn)
點擊上方藍色字體,選擇“設為星標”
回復”資源“獲取更多資源

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

這樣就能使請求處理的速率有一個穩(wěn)定的上限。很顯然,如果進水的速率比開孔出水的速率大的話,水(請求)就會逐漸在桶中積累。若一直積累直到溢出了,那些溢出去的水(請求)就會被直接丟棄,也就是拒絕服務(denial of service, DoS)。
漏桶的思路非常簡單直接,易于實現(xiàn)。但是如果當前的網(wǎng)絡環(huán)境中充斥著短時流量尖峰,始終保持勻速的漏桶就無法很好地處理了,故后來又產(chǎn)生了令牌桶算法。
令牌以恒定的速率產(chǎn)生并放入桶中。桶的大小是有限的,也就是說桶滿了之后,多余的令牌就扔掉了。每當請求到來時,需要從桶中獲取一個令牌才能真正地處理它。如果桶里沒有令牌,該請求就會被丟棄。可見,由于桶里預先留有一定量的令牌,故可以保證在突發(fā)流量到來且令牌沒耗盡前平滑地處理,比漏桶更靈活一些。
Guava RateLimiter
RateLimiter抽象類提供限流的所有功能,它的實現(xiàn)類只有SmoothRateLimiter。而SmoothRateLimiter的具體策略又由它的兩個內部子類來實現(xiàn)。SmoothBursty:兼容突發(fā)流量的令牌桶實現(xiàn),也就是上一節(jié)描述的經(jīng)典令牌桶算法。
SmoothWarmingUp:帶預熱過程的令牌桶實現(xiàn),即在桶較滿時產(chǎn)生令牌的速度較慢,隨著令牌的消耗慢慢增長到恒定速率。如下圖所示,x軸為令牌數(shù),y軸為延遲,從右向左看的梯形區(qū)域就是令牌消耗的預熱過程。
本文以經(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)生新的令牌)。當然,如果有一段時間沒有請求進入的話,它就會保持在上次請求的過去時間戳。
在當前時間戳大于nextFreeTicketMicros(即后者是一個過去的時間戳)的情況下,就用它們的時間差除以產(chǎn)生令牌的頻率,結果就是這段時間內應該產(chǎn)生的令牌數(shù),進而更新桶內的現(xiàn)有令牌數(shù)storedPermits,以及將nextFreeTicketMicros更新到現(xiàn)在。void resync(long nowMicros) {if (nowMicros > nextFreeTicketMicros) {double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();storedPermits = min(maxPermits, storedPermits + newPermits);nextFreeTicketMicros = nowMicros;}}// SmoothBursty.coolDownIntervalMicros()@Overridedouble coolDownIntervalMicros() {return stableIntervalMicros;}
可見,RateLimiter的令牌是延遲(lazy)生成的,也就是說每次受理當前請求時,如果系統(tǒng)已經(jīng)空閑了一定時間,才會計算上次請求到當前時間應該產(chǎn)生多少個令牌,而不是使用單獨的任務來定期產(chǎn)生令牌——因為定時器無法保證較高的精度,并且性能不佳。當然,如果令牌已經(jīng)“超支”,當前就不需要再更新令牌了。
接下來則是獲取令牌,由SmoothRateLimiter.reserveEarliestAvailable()方法來實現(xiàn),這個方法名非常self-explanatory了。
這個方法的執(zhí)行流程如下: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()long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {return 0L;}
調用上述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的值。
Guava提供了多個靜態(tài)create()方法創(chuàng)建RateLimiter,其中創(chuàng)建SmoothBursty的源碼如下。
可見,我們創(chuàng)建RateLimiter時只需要指定期望的QPS。那么SmoothBursty構造方法中的maxBurstSeconds參數(shù)有什么用?顧名思義,它表示在令牌桶滿時能夠承受的突發(fā)流量時長,這樣就能夠確定出桶的最大容量maxPermits了:public static RateLimiter create(double permitsPerSecond) {return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer());}static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) {RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);rateLimiter.setRate(permitsPerSecond);return rateLimiter;}
maxPermits?=?maxBurstSeconds?*?permitsPerSecond;不過maxBurstSeconds當前不支持自定義,Guava規(guī)定為1秒,故桶的最大容量總是與我們指定的QPS相同。RateLimiter向用戶提供了acquire()方法用于獲取令牌,以及帶超時的tryAcquire()方法。下面的代碼示出從acquire()到reserveEarliestAvailable()的調用鏈,方法都很短。
可見:@CanIgnoreReturnValuepublic double acquire() {return acquire(1);}@CanIgnoreReturnValuepublic 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));}}}
可見,每次獲取的令牌數(shù)和獲取時需要等待的時長是符合上面所述邏輯的預期的。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
文章不錯?點個【在看】吧!??


