使用Guava RateLimiter限流入門到深入
前言
在開發高並發系統時有三把利器用來保護系統:緩存、降級和限流
-
緩存
: 緩存的目的是提升系統訪問速度和增大系統處理容量 -
降級
: 降級是當服務出現問題或者影響到核心流程時,需要暫時屏蔽掉,待高峰或者問題解決後再打開 -
限流
: 限流的目的是通過對並發訪問/請求進行限速,或者對一個時間窗口內的請求進行限速來保護系統,一旦達到限制速率則可以拒絕服務、排隊或等待、降級等處理
常見限流算法
- 漏桶算法
漏桶算法思路很簡單,水(請求)先進入到漏桶里,漏桶以一定的速度出水,當水流入速度過大會直接溢出,可以看出漏桶算法能強行限制數據的傳輸速率。
- 令牌桶算法
對於很多應用場景來說,除了要求能夠限制數據的平均傳輸速率外,還要求允許某種程度的突發傳輸。這時候漏桶算法可能就不合適了,令牌桶算法更為適合。如圖所示,令牌桶算法的原理是系統會以一個恆定的速度往桶里放入令牌,而如果請求需要被處理,則需要先從桶里獲取一個令牌,當桶里沒有令牌可取時,則拒絕服務。
RateLimiter使用以及源碼解析
Google開源工具包Guava提供了限流工具類RateLimiter,該類基於令牌桶算法實現流量限制,使用十分方便,而且十分高效。
RateLimiter使用
首先簡單介紹下RateLimiter的使用
public void testAcquire() {
RateLimiter limiter = RateLimiter.create(1);
for(int i = 1; i < 10; i = i + 2 ) {
double waitTime = limiter.acquire(i);
System.out.println("cutTime=" + System.currentTimeMillis() + " acq:" + i + " waitTime:" + waitTime);
}
}
輸出結果:
cutTime=1535439657427 acq:1 waitTime:0.0
cutTime=1535439658431 acq:3 waitTime:0.997045
cutTime=1535439661429 acq:5 waitTime:2.993028
cutTime=1535439666426 acq:7 waitTime:4.995625
cutTime=1535439673426 acq:9 waitTime:6.999223
首先通過RateLimiter.create(1)
創建一個限流器,參數代表每秒生成的令牌數,通過limiter.acquire(i)
來以阻塞的方式獲取令牌,當然也可以通過tryAcquire(int permits, long timeout, TimeUnit unit)
來設置等待超時時間的方式獲取令牌,如果超timeout為0,則代表非阻塞,獲取不到立即返回。
從輸出來看,RateLimiter支持預消費,比如在acquire(5)時,等待時間是3秒,是上一個獲取令牌時預消費了3個兩排,固需要等待3*1秒,然後又預消費了5個令牌,以此類推
RateLimiter通過限制後面請求的等待時間,來支持一定程度的突發請求(預消費),在使用過程中需要注意這一點,具體實現原理後面再分析。
RateLimiter實現原理
Guava有兩種限流模式,一種為穩定模式(SmoothBursty:令牌生成速度恆定),一種為漸進模式(SmoothWarmingUp:令牌生成速度緩慢提升直到維持在一個穩定值) 兩種模式實現思路類似,主要區別在等待時間的計算上,本篇重點介紹SmoothBursty
RateLimiter的創建
通過調用RateLimiter的create接口來創建實例,實際是調用的SmoothBuisty穩定模式創建的實例。
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;
}
SmoothBursty
中的兩個構造參數含義:
- SleepingStopwatch:guava中的一個時鐘類實例,會通過這個來計算時間及令牌
- maxBurstSeconds:官方解釋,在ReteLimiter未使用時,最多保存幾秒的令牌,默認是1
在解析SmoothBursty原理前,重點解釋下SmoothBursty中幾個屬性的含義
/**
* The work (permits) of how many seconds can be saved up if this RateLimiter is unused?
* 在RateLimiter未使用時,最多存儲幾秒的令牌
* */
final double maxBurstSeconds;
/**
* The currently stored permits.
* 當前存儲令牌數
*/
double storedPermits;
/**
* The maximum number of stored permits.
* 最大存儲令牌數 = maxBurstSeconds * stableIntervalMicros(見下文)
*/
double maxPermits;
/**
* The interval between two unit requests, at our stable rate. E.g., a stable rate of 5 permits
* per second has a stable interval of 200ms.
* 添加令牌時間間隔 = SECONDS.toMicros(1L) / permitsPerSecond;(1秒/每秒的令牌數)
*/
double stableIntervalMicros;
/**
* The time when the next request (no matter its size) will be granted. After granting a request,
* this is pushed further in the future. Large requests push this further than small requests.
* 下一次請求可以獲取令牌的起始時間
* 由於RateLimiter允許預消費,上次請求預消費令牌後
* 下次請求需要等待相應的時間到nextFreeTicketMicros時刻才可以獲取令牌
*/
private long nextFreeTicketMicros = 0L; // could be either in the past or future
接下來介紹幾個關鍵函數
- setRate
public final void setRate(double permitsPerSecond) {
checkArgument(
permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive");
synchronized (mutex()) {
doSetRate(permitsPerSecond, stopwatch.readMicros());
}
}
通過這個接口設置令牌通每秒生成令牌的數量,內部時間通過調用SmoothRateLimiter的doSetRate來實現
- doSetRate
@Override
final void doSetRate(double permitsPerSecond, long nowMicros) {
resync(nowMicros);
double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;
this.stableIntervalMicros = stableIntervalMicros;
doSetRate(permitsPerSecond, stableIntervalMicros);
}
這裡先通過調用resync生成令牌以及更新下一期令牌生成時間,然後更新stableIntervalMicros,最後又調用了SmoothBursty的doSetRate
- resync
/**
* Updates {@code storedPermits} and {@code nextFreeTicketMicros} based on the current time.
* 基於當前時間,更新下一次請求令牌的時間,以及當前存儲的令牌(可以理解為生成令牌)
*/
void resync(long nowMicros) {
// if nextFreeTicket is in the past, resync to now
if (nowMicros > nextFreeTicketMicros) {
double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
storedPermits = min(maxPermits, storedPermits + newPermits);
nextFreeTicketMicros = nowMicros;
}
}
根據令牌桶算法,桶中的令牌是持續生成存放的,有請求時需要先從桶中拿到令牌才能開始執行,誰來持續生成令牌存放呢?
一種解法是,開啟一個定時任務,由定時任務持續生成令牌。這樣的問題在於會極大的消耗系統資源,如,某接口需要分別對每個用戶做訪問頻率限制,假設系統中存在6W用戶,則至多需要開啟6W個定時任務來維持每個桶中的令牌數,這樣的開銷是巨大的。
另一種解法則是延遲計算,如上resync函數。該函數會在每次獲取令牌之前調用,其實現思路為,若當前時間晚於nextFreeTicketMicros,則計算該段時間內可以生成多少令牌,將生成的令牌加入令牌桶中並更新數據。這樣一來,只需要在獲取令牌時計算一次即可。
- SmoothBursty的doSetRate
@Override
void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
double oldMaxPermits = this.maxPermits;
maxPermits = maxBurstSeconds * permitsPerSecond;
if (oldMaxPermits == Double.POSITIVE_INFINITY) {
// if we don't special-case this, we would get storedPermits == NaN, below
// Double.POSITIVE_INFINITY 代表無窮啊
storedPermits = maxPermits;
} else {
storedPermits =
(oldMaxPermits == 0.0)
? 0.0 // initial state
: storedPermits * maxPermits / oldMaxPermits;
}
}
桶中可存放的最大令牌數由maxBurstSeconds計算而來,其含義為最大存儲maxBurstSeconds秒生成的令牌。
該參數的作用在於,可以更為靈活地控制流量。如,某些接口限制為300次/20秒,某些接口限制為50次/45秒等。也就是流量不局限於qps
參考
- 使用Guava RateLimiter限流以及源碼解析
- 使用Guava的RateLimiter做限流
- SpringBoot使用RateLimiter通過AOP方式進行限流
- Guava RateLimiter源碼解析
結語
歡迎關注微信公眾號『碼仔zonE』,專註於分享Java、雲計算相關內容,包括SpringBoot、SpringCloud、微服務、Docker、Kubernetes、Python等領域相關技術乾貨,期待與您相遇!