0x00 流量限制的手段
流量限制的手段有很多,最常見的:漏桶、令牌桶兩種:
- 漏桶是指我們有一個一直裝滿了水的桶,每過固定的一段時間即向外漏一滴水。如果你接到了這滴水,那麼你就可以繼續服務請求,如果沒有接到,那麼就需要等待下一滴水。
- 令牌桶則是指勻速向桶中添加令牌,服務請求時需要從桶中獲取令牌,令牌的數目可以按照需要消耗的資源進行相應的調整。如果沒有令牌,可以選擇等待,或者放棄。
這兩種方法看起來很像,不過還是有區別的。漏桶流出的速率固定,而令牌桶只要在桶中有令牌,那就可以拿。也就是說令牌桶是允許一定程度的併發的,比如同一個時刻,有100個用戶請求,只要令牌桶中有100個令牌,那麼這100個請求全都會放過去。令牌桶在桶中沒有令牌的情況下也會退化爲漏桶模型。
0x01 簡單令牌桶的實現
簡單令牌桶的實現原理是使用定時器,每隔一定時間間隔向桶內放入令牌,實現如下:
// GetBucket return a token bucket
// capacityPs is maximum concurrency in one second
// capacityPs is maximum bucket capacity
func GetBucket(capacityPs, maxCapacity int) chan struct{} {
var bucketToken = make(chan struct{}, maxCapacity)
timeD := time.Second / time.Duration(capacityPs)
ticker := time.NewTicker(timeD)
go func() {
for {
select {
case <-ticker.C:
select {
case bucketToken <- struct{}{}:
default:
}
}
fmt.Println(len(bucketToken), time.Now())
}
}()
return bucketToken
}
有新的請求時需要獲取token,獲取token時可以選擇阻塞或者非阻塞兩種方式,獲取token的實現方式如下:
func GetToken(block bool, bucket *chan struct{}) bool {
if block {
select {
case <-*bucket:
return true
}
} else {
select {
case <-*bucket:
return true
default:
return false
}
}
}
整體方法的使用方式如下:
package main
import (
"fmt"
"time"
)
// GetBucket return a token bucket
// capacityPs is maximum concurrency in one second
// capacityPs is maximum bucket capacity
func GetBucket(capacityPs, maxCapacity int) chan struct{} {
var bucketToken = make(chan struct{}, maxCapacity)
timeD := time.Second / time.Duration(capacityPs)
ticker := time.NewTicker(timeD)
go func() {
for {
select {
case <-ticker.C:
select {
case bucketToken <- struct{}{}:
default:
}
}
fmt.Println(len(bucketToken), time.Now())
}
}()
return bucketToken
}
func GetToken(block bool, bucket *chan struct{}) bool {
if block {
select {
case <-*bucket:
return true
}
} else {
select {
case <-*bucket:
return true
default:
return false
}
}
}
func main() {
capacityPs := 50
maxCapacity := 100
bucket := GetBucket(capacityPs, maxCapacity)
for {
time.Sleep(100 * time.Millisecond)
go fmt.Println("====", time.Now(), GetToken(false, &bucket))
go fmt.Println("====", time.Now(), GetToken(false, &bucket))
go fmt.Println("====", time.Now(), GetToken(false, &bucket))
go fmt.Println("====", time.Now(), GetToken(false, &bucket))
go fmt.Println("====", time.Now(), GetToken(false, &bucket))
}
}
0x02 github.com/juju/ratelimit
取消定時填充token邏輯,改爲記錄上次填充時間,通過計算距今時間差值來計算該時間段內應填充的數量,一次性進行填充。
記:
- 桶中剩餘的token數量爲:
cnt
- token數量上限爲:
cap
- 每次填充的時間間隔爲:
d
- 每次填充的token數爲:
q
- 上次填充的時間爲:
t1
- 當前時間爲:
t2
則在 t2
時刻桶中的token數量應該爲:
min( cnt + ((t2 - t1) / d) * q , cap )
0x03 github.com/uber-go/ratelimit
- TODO
沒太看懂,有時間再看
0x04 參考文檔
本文參考了《Go語言高級編程》,作者:柴樹杉、曹春暉。