限流系列
開源元件 rate-limit: 限流
高可用之限流-01-入門介紹
高可用之限流-02-如何設計限流框架
高可用之限流-03-Semaphore 訊號量做限流
高可用之限流-04-fixed window 固定視窗
高可用之限流-05-slide window 滑動視窗
高可用之限流-06-slide window 滑動視窗 sentinel 原始碼
高可用之限流-07-token bucket 令牌桶演算法
高可用之限流 08-leaky bucket漏桶演算法
高可用之限流 09-guava RateLimiter 入門使用簡介 & 原始碼分析
RateLimiter 入門使用
maven 引入
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
測試案例
@Test
public void limitTest() {
RateLimiter limiter = RateLimiter.create(1);
for(int i = 1; i < 5; i++) {
double waitTime = limiter.acquire(i);
System.out.println("cutTime=" + System.currentTimeMillis() + " acq:" + i + " waitTime:" + waitTime);
}
}
- 日誌
cutTime=1592880664419 acq:1 waitTime:0.0
cutTime=1592880665420 acq:2 waitTime:0.999098
cutTime=1592880667419 acq:3 waitTime:1.99867
cutTime=1592880670419 acq:4 waitTime:2.999099
說明
首先透過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()
透過這個介面設定令牌通每秒生成令牌的數量,內部時間透過呼叫SmoothRateLimiter的doSetRate來實現
public final void setRate(double permitsPerSecond) {
checkArgument(
permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive");
synchronized (mutex()) {
doSetRate(permitsPerSecond, stopwatch.readMicros());
}
}
doSetRate()
這裡先透過呼叫resync生成令牌以及更新下一期令牌生成時間,然後更新stableIntervalMicros,最後又呼叫了SmoothBursty的doSetRate
@Override
final void doSetRate(double permitsPerSecond, long nowMicros) {
resync(nowMicros);
double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;
this.stableIntervalMicros = stableIntervalMicros;
doSetRate(permitsPerSecond, stableIntervalMicros);
}
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
桶中可存放的最大令牌數由maxBurstSeconds計算而來,其含義為最大儲存maxBurstSeconds秒生成的令牌。
該引數的作用在於,可以更為靈活地控制流量。如,某些介面限制為300次/20秒,某些介面限制為50次/45秒等。也就是流量不侷限於qps
作者:人在碼途
連結:https://www.jianshu.com/p/5d4fe4b2a726
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。
@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;
}
}
RateLimiter 幾個常用介面分析
在瞭解以上概念後,就非常容易理解 RateLimiter 暴露出來的介面
@CanIgnoreReturnValue
public double acquire() {
return acquire(1);
}
/**
* 獲取令牌,返回阻塞的時間
**/
@CanIgnoreReturnValue
public 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());
}
}
acquire函式主要用於獲取permits個令牌,並計算需要等待多長時間,進而掛起等待,並將該值返回,主要透過reserve返回需要等待的時間,reserve中透過呼叫reserveAndGetWaitLength獲取等待時間
/**
* Reserves next ticket and returns the wait time that the caller must wait for.
*
* @return the required wait time, never negative
*/
final long reserveAndGetWaitLength(int permits, long nowMicros) {
long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
return max(momentAvailable - nowMicros, 0);
}
最後呼叫了 reserveEarliestAvailable
@Override
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;
}
首先透過resync生成令牌以及同步nextFreeTicketMicros時間戳,freshPermits從令牌桶中獲取令牌後還需要的令牌數量,透過storedPermitsToWaitTime計算出獲取freshPermits還需要等待的時間,在穩定模式中,這裡就是(long) (freshPermits * stableIntervalMicros) ,然後更新nextFreeTicketMicros以及storedPermits,這次獲取令牌需要的等待到的時間點,reserveAndGetWaitLength返回需要等待的時間間隔。
從reserveEarliestAvailable
可以看出RateLimiter的預消費原理,以及獲取令牌的等待時間時間原理(可以解釋示例結果),再獲取令牌不足時,並沒有等待到令牌全部生成,而是更新了下次獲取令牌時的nextFreeTicketMicros,從而影響的是下次獲取令牌的等待時間。
reserve
這裡返回等待時間後,acquire
透過呼叫stopwatch.sleepMicrosUninterruptibly(microsToWait);
進行sleep操作,這裡不同於Thread.sleep(), 這個函式的sleep是uninterruptibly的,內部實現:
public static void sleepUninterruptibly(long sleepFor, TimeUnit unit) {
//sleep 阻塞執行緒 內部透過Thread.sleep()
boolean interrupted = false;
try {
long remainingNanos = unit.toNanos(sleepFor);
long end = System.nanoTime() + remainingNanos;
while (true) {
try {
// TimeUnit.sleep() treats negative timeouts just like zero.
NANOSECONDS.sleep(remainingNanos);
return;
} catch (InterruptedException e) {
interrupted = true;
remainingNanos = end - System.nanoTime();
//如果被interrupt可以繼續,更新sleep時間,迴圈繼續sleep
}
}
} finally {
if (interrupted) {
Thread.currentThread().interrupt();
//如果被打斷過,sleep過後再真正中斷執行緒
}
}
}
sleep之後,acquire
返回sleep的時間,阻塞結束,獲取到令牌。
public boolean tryAcquire(int permits) {
return tryAcquire(permits, 0, MICROSECONDS);
}
public boolean tryAcquire() {
return tryAcquire(1, 0, MICROSECONDS);
}
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) {
long timeoutMicros = max(unit.toMicros(timeout), 0);
checkPermits(permits);
long microsToWait;
synchronized (mutex()) {
long nowMicros = stopwatch.readMicros();
if (!canAcquire(nowMicros, timeoutMicros)) {
return false;
} else {
microsToWait = reserveAndGetWaitLength(permits, nowMicros);
}
}
stopwatch.sleepMicrosUninterruptibly(microsToWait);
return true;
}
private boolean canAcquire(long nowMicros, long timeoutMicros) {
return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;
}
@Override
final long queryEarliestAvailable(long nowMicros) {
return nextFreeTicketMicros;
}
tryAcquire函式可以嘗試在timeout時間內獲取令牌,如果可以則掛起等待相應時間並返回true,否則立即返回false
canAcquire用於判斷timeout時間內是否可以獲取令牌,透過判斷當前時間+超時時間是否大於nextFreeTicketMicros 來決定是否能夠拿到足夠的令牌數,如果可以獲取到,則過程同acquire,執行緒sleep等待,如果透過canAcquire在此超時時間內不能回去到令牌,則可以快速返回,不需要等待timeout後才知道能否獲取到令牌。
到此,Guava RateLimiter穩定模式的實現原理基本已經清楚,如發現文中錯誤的地方,勞煩指正!
參考資料
限流限速 RateLimiter
Guava RateLimiter