Ratelimit

2019-06-06

常见的限流算法

常用的限流算法有:漏桶算法(Leaky Bucket)和令牌桶算法(Token Bucket)。

  • 漏桶算法

    漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率。

  • 令牌桶

    随着时间流逝,系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里加入Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了。新请求来临时,会各自拿走一个Token,如果没有Token可拿了就阻塞或者拒绝服务。

漏桶漏出的速率是固定参数的,流出速率是多少,流入的速率也是多少,对突发特性的流量处理缺乏效率。而令牌桶只要桶中有令牌就可以拿,允许一定程度的并发,比如同一个时刻,有100个用户请求,只要令牌桶中有100个令牌,那么这100个请求全都会放过去。令牌桶在桶中没有令牌的情况下也会退化为漏桶模型。另外,令牌桶还可以根据需求,提高放入令牌存放速率。

Golang实现

Leaky Bucket

实际应用中令牌桶应用较为广泛,漏桶使用较少。uber开源实现过一个基于请求之间的时间,而不是单纯的间隔时间来填充的漏桶:https://github.com/uber-go/ratelimit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import (
"fmt"
"time"

"go.uber.org/ratelimit"
)

func main() {
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
}

// Output:
// 0 0
// 1 10ms
// 2 10ms
// 3 10ms
// 4 10ms
// 5 10ms
// 6 10ms
// 7 10ms
// 8 10ms
// 9 10ms
}

每次请求的时间间隔为10ms。其中Sub()方法是时间减,即now-prev,所以主要实现在rl.Take(),如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
type limiter struct {
sync.Mutex
last time.Time
sleepFor time.Duration
perRequest time.Duration
maxSlack time.Duration
clock Clock
}

// New returns a Limiter that will limit to the given RPS.
func New(rate int, opts ...Option) Limiter {
l := &limiter{
perRequest: time.Second / time.Duration(rate),
maxSlack: -10 * time.Second / time.Duration(rate),
}
for _, opt := range opts {
opt(l)
}
if l.clock == nil {
l.clock = clock.New()
}
return l
}

func (t *limiter) Take() time.Time {
t.Lock()
defer t.Unlock()

now := t.clock.Now()

// If this is our first request, then we allow it.
if t.last.IsZero() {
t.last = now
return t.last
}

// sleepFor calculates how much time we should sleep based on
// the perRequest budget and how long the last request took.
// Since the request may take longer than the budget, this number
// can get negative, and is summed across requests.
t.sleepFor += t.perRequest - now.Sub(t.last)

// We shouldn't allow sleepFor to get too negative, since it would mean that
// a service that slowed down a lot for a short period of time would get
// a much higher RPS following that.
if t.sleepFor < t.maxSlack {
t.sleepFor = t.maxSlack
}

// If sleepFor is positive, then we should sleep now.
if t.sleepFor > 0 {
t.clock.Sleep(t.sleepFor)
t.last = now.Add(t.sleepFor)
t.sleepFor = 0
} else {
t.last = now
}

return t.last
}

记录每次请求的时间,若当前时间减上一次请求时间小于设置的间隔时间t.sleepFor += t.perRequest - now.Sub(t.last),则休眠t.sleepFor的时间,若大于则记录当前时间。

Token Bucket

令牌桶模型实际上就是对全局计数的加减法操作过程,但使用计数需要我们自己加读写锁,很容易想到可以用buffered channel来完成简单的加令牌取令牌操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"fmt"
"time"
)

func main() {
var fillInterval = time.Millisecond * 10
var capacity = 100
var tokenBucket = make(chan struct{}, capacity)

fillToken := func() {
ticker := time.NewTicker(fillInterval)
for {
select {
case <-ticker.C:
select {
case tokenBucket <- struct{}{}:
default:
}
fmt.Println("current token cnt:", len(tokenBucket), time.Now())
}
}
}

go fillToken()
time.Sleep(time.Hour)
}

每10ms放入1个token,若token已满,则直接放弃。

请求进来时,获取token:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func TakeAvailable(block bool) bool{
var takenResult bool
if block {
select {
case <-tokenBucket:
takenResult = true
}
} else {
select {
case <-tokenBucket:
takenResult = true
default:
takenResult = false
}
}

return takenResult
}

若桶未满,则取出一个token,返回true,若桶已满,取到令牌则返回true,没取到令牌则返回false。

除了以上实现方法外,github.com/juju/ratelimit还提供了一种高效的实现方式。

想象以下,每隔一段固定时间向令牌桶中放令牌,如果我们记下上一次放令牌的时间t1,和当时的令牌数k1,放令牌的时间间隔未ti,每次向令牌桶中放x个令牌,桶的容量为cap。现有人来取n个令牌,记此时的时间为t2,在t2时刻,令牌桶中的令牌数为多少呢:

1
2
cur = k1 + ((t2-t1)/ti) * x
cur = cur > cap ? cap : cur

使用两个时间的时间差,就可以计算出桶中有多少令牌了。不需要填充token的操作,只需要每个Take的时候进行简单的计算就能得到正确的令牌数。

参考:

https://github.com/uber-go/ratelimit

https://en.wikipedia.org/wiki/Token_bucket

https://chai2010.cn/advanced-go-programming-book/ch5-web/ch5-06-ratelimit.html