uber在Github上開源了一套用于服務限流的go語言庫ratelimit, 該組件基于Leaky Bucket(漏桶)實現。
我在之前寫過《Golang限流器time/rate實現剖析》,講了Golang標準庫中提供的基于Token Bucket實現限流組件的time/rate原理,同時也講了限流的一些背景。
相比于TokenBucket,只要桶內還有剩余令牌,調用方就可以一直消費。而Leaky Bucket相對來說比較嚴格,調用方只能嚴格按照這個間隔順序進行消費調用。(實際上,uber-go對這個限制也做了一些優(yōu)化,具體可以看下文詳解)
還是老規(guī)矩,在正式講其實現之前,我們先看下ratelimit的使用方法。
ratelimit的使用
我們直接看下uber-go官方庫給的例子:
rl := ratelimit.New(100) // per second
prev := time.Now()
for i := 0; i < 10; i++ {
now := rl.Take()
fmt.Println(i, now.Sub(prev))
prev = now
}
在這個例子中,我們給定限流器每秒可以通過100個請求,也就是平均每個請求間隔10ms。
因此,最終會每10ms打印一行數據。輸出結果如下:
// Output:
// 0 0
// 1 10ms
// 2 10ms
// 3 10ms
// 4 10ms
// 5 10ms
// 6 10ms
// 7 10ms
// 8 10ms
// 9 10ms
基本實現
要實現以上每秒固定速率的目的,其實還是比較簡單的。
在ratelimit的New函數中,傳入的參數是每秒允許請求量(RPS)。
我們可以很輕易的換算出每個請求之間的間隔:
limiter.perRequest = time.Second / time.Duration(rate)
以上limiter.perRequest指的就是每個請求之間的間隔時間。
如下圖,當請求1處理結束后, 我們記錄下請求1的處理完成的時刻, 記為limiter.last。
稍后請求2到來, 如果此刻的時間與limiter.last相比并沒有達到perRequest的間隔大小,那么sleep一段時間即可。
[圖片上傳失敗...(image-4b37d-1574514204743)]
對應ratelimit的實現代碼如下:
sleepFor = t.perRequest - now.Sub(t.last)
if sleepFor > 0 {
t.clock.Sleep(sleepFor)
t.last = now.Add(sleepFor)
} else {
t.last = now
}
最大松弛量
我們講到,傳統(tǒng)的Leaky Bucket,每個請求的間隔是固定的,然而,在實際上的互聯(lián)網應用中,流量經常是突發(fā)性的。對于這種情況,uber-go對Leaky Bucket做了一些改良,引入了最大松弛量(maxSlack)的概念。
我們先理解下整體背景: 假如我們要求每秒限定100個請求,平均每個請求間隔10ms。但是實際情況下,有些請求間隔比較長,有些請求間隔比較短。如下圖所示:
[圖片上傳失敗...(image-81ea99-1574514204743)]
請求1完成后,15ms后,請求2才到來,可以對請求2立即處理。請求2完成后,5ms后,請求3到來,這個時候距離上次請求還不足10ms,因此還需要等待5ms。
但是,對于這種情況,實際上三個請求一共消耗了25ms才完成,并不是預期的20ms。在uber-go實現的ratelimit中,可以把之前間隔比較長的請求的時間,勻給后面的使用,保證每秒請求數(RPS)即可。
對于以上case,因為請求2相當于多等了5ms,我們可以把這5ms移給請求3使用。加上請求3本身就是5ms之后過來的,一共剛好10ms,所以請求3無需等待,直接可以處理。此時三個請求也恰好一共是20ms。
如下圖所示:
[圖片上傳失敗...(image-994a4e-1574514204743)]
在ratelimit的對應實現中很簡單,是把每個請求多余出來的等待時間累加起來,以給后面的抵消使用。
t.sleepFor += t.perRequest - now.Sub(t.last)
if t.sleepFor > 0 {
t.clock.Sleep(t.sleepFor)
t.last = now.Add(t.sleepFor)
t.sleepFor = 0
} else {
t.last = now
}
注意:這里跟上述代碼不同的是,這里是+=。而同時t.perRequest - now.Sub(t.last)是可能為負值的,負值代表請求間隔時間比預期的長。
當t.sleepFor > 0,代表此前的請求多余出來的時間,無法完全抵消此次的所需量,因此需要sleep相應時間, 同時將t.sleepFor置為0。
當t.sleepFor < 0,說明此次請求間隔大于預期間隔,將多出來的時間累加到t.sleepFor即可。
但是,對于某種情況,請求1完成后,請求2過了很久到達(好幾個小時都有可能),那么此時對于請求2的請求間隔now.Sub(t.last),會非常大。以至于即使后面大量請求瞬時到達,也無法抵消完這個時間。那這樣就失去了限流的意義。
為了防止這種情況,ratelimit就引入了最大松弛量(maxSlack)的概念, 該值為負值,表示允許抵消的最長時間,防止以上情況的出現。
if t.sleepFor < t.maxSlack {
t.sleepFor = t.maxSlack
}
ratelimit中maxSlack的值為-10 * time.Second / time.Duration(rate), 是十個請求的間隔大小。我們也可以理解為ratelimit允許的最大瞬時請求為10。
高級用法
ratelimit的New函數,除了可以配置每秒請求數(QPS), 其實還提供了一套可選配置項Option。
func New(rate int, opts ...Option) Limiter
Option的類型為type Option func(l *limiter), 也就是說我們可以提供一些這樣類型的函數,作為Option,傳給ratelimit, 定制相關需求。
但實際上,自定義Option的用處比較小,因為limiter結構體本身就是個私有類型,我們并不能拿它做任何事情。
我們只需要了解ratelimit目前提供的兩個配置項即可:
WithoutSlack
我們上文講到ratelimit中引入了最大松弛量的概念,而且默認的最大松弛量為10個請求的間隔時間。
但是確實會有這樣需求場景,需要嚴格的限制請求的固定間隔。那么我們就可以利用WithoutSlack來取消松弛量的影響。
limiter := ratelimit.New(100, ratelimit.WithoutSlack)
WithClock(clock Clock)
我們上文講到,ratelimit的實現時,會計算當前時間與上次請求時間的差值,并sleep相應時間。
在ratelimit基于go標準庫的time實現時間相關計算。如果有精度更高或者特殊需求的計時場景,可以用WithClock來替換默認時鐘。
通過該方法,只要實現了Clock的interface,就可以自定義時鐘了。
type Clock interface {
Now() time.Time
Sleep(time.Duration)
}
clock &= MyClock{}
limiter := ratelimit.New(100, ratelimit.WithClock(clock))