uber-go漏桶限流器使用與原理分析

轉載自:uber-go漏桶限流器使用與原理分析

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))
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容