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语言高级编程》,作者:柴树杉、曹春晖。