1. 介绍

通过对并发访问 / 请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理。说白了就是限制请求数量,或者是在某一段时间内限制总的请求数量。

1.1 原则

在实际使用时,一般不会做全局的限流,而是针对某些特征去做精细化的限流。例如:通过header、x-forward-for 等限制爬虫的访问,通过对 ip,session 等用户信息限制单个用户的访问等。

1.2 算法

  • 漏桶

    漏桶,听起来有点像漏斗的样子,也是一滴一滴的滴下去的。漏桶是有缓存的,有请求就会放到缓存中。

    漏桶以固定的速率往外漏水,若桶空了则停止漏水。比如说,1s 漏 1000 滴水,正如 1s 处理 1000 个请求。如果漏桶慢了,则多余的水滴也会被直接舍弃。

  • 令牌桶

    通过动态控制令牌的数量,来更好的服务客户端的请求事情,令牌的生成数量和生产速率都是可以灵活控制的。

    令牌桶实现的限流器算法,相较于漏斗算法可以在一定程度上允许突发的流量进入我们的应用中,所以在web应用中最为广泛。

1.3 实现

https://github.com/uber-go/ratelimitgithub/go/timehttps://github.com/didip/tollboothhttps://github.com/golang/net/blob/master/netutil/listen.go
2. 令牌桶限流
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

package main

import (
"fmt"
"net/http"
"time"

"github.com/go-resty/resty/v2"
"golang.org/x/time/rate"
)

func main() {
limiter := rate.NewLimiter(rate.Every(100*time.Millisecond), 3) // 3个桶,每100ms放入桶一个令牌
http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
if limiter.Allow() { // do something
fmt.Println("say hello")
} else {
fmt.Println("limit")
}
})

go func() {
time.Sleep(time.Second)
Req()
}()

_ = http.ListenAndServe(":13100", nil)
}

func Req() {
for i := 0; i < 20; i++ {
_, _ = resty.New().R().Get("http://localhost:13100/ping")
// 当不睡的时候,只能收到3个sayhello

// 当睡的时候,每个请求都能满足,say hello都可以出来
time.Sleep(100 * time.Millisecond)
}
}

2.1 http 中间件加入流控

如何在 http 中间件中加入流控呢,目的是限流,每一个请求,都需要经过这个中间件,才有机会向后走,才有机会被处理。

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
package main

import (
"time"

"github.com/didip/tollbooth"
"github.com/didip/tollbooth/limiter"
"github.com/gin-gonic/gin"
)

func main() {
r := gin.New()

lmt := tollbooth.NewLimiter(1, &limiter.ExpirableOptions{DefaultExpirationTTL: time.Second * 5})
lmt.SetIPLookups([]string{"RemoteAddr", "X-Forwarded-For", "X-Real-IP"})
lmt.SetMethods([]string{"POST", "GET"}) //放开更精准限制,但是也放松了流量。

r.Use(LimitHandler(lmt))
r.GET("/", func(c *gin.Context) {
c.String(200, "Get Hello, world!")
})
r.POST("/", func(c *gin.Context) {
c.String(200, "Post Hello, world!")
})
r.Run(":12345")
}

func LimitHandler(lmt *limiter.Limiter) gin.HandlerFunc {
return func(c *gin.Context) {
httpError := tollbooth.LimitByRequest(lmt, c.Writer, c.Request)
if httpError != nil {
c.Data(httpError.StatusCode, lmt.GetMessageContentType(), []byte(httpError.Message))
c.Abort()
} else {
c.Next()
}
}
}

2.2 vegeta测试

有一个非常棒的工具称作 vegeta,我喜欢在 HTTP 负载测试中使用(它也是用 Go 编写的)。

1
brew install vegeta

我们需要创建一个简单的配置文件,声明我们想要发送的请求。

1
GET http://localhost:12345/

然后,以每个时间单元 100 个请求的速率攻击 10 秒。

1
2
3
4
5
6
7
vegeta attack -duration=10s -rate=100 -targets=vegeta.conf | vegeta report

echo "http://localhost:12345" | vegeta attack -rate=500 -connections=100 -duration=10s | tee results.bin | vegeta report


// vegeta attack -duration=10s -rate=100 -targets=vegeta.conf | vegeta report 1s只成功了一个
// Status Codes [code:count] 200:10 429:990

结果,你将看到一些请求返回 200,但大多数返回 429。

2.3 wrk测试

先在本地安装wrk。

1
2
3
4
git clone https://github.com/wg/wrk
cd wrk
make
ln -s $PWD/wrk /usr/local/bin/

我的mac是6核,线程数不要太多,是核数的 2 到 4 倍即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
wrk -t6 -c10 -d10s  --latency http://127.0.0.1:9061/health


Running 10s test @ http://127.0.0.1:9061/health
6 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.28ms 14.43ms 399.37ms 99.59%
Req/Sec 2.06k 262.21 2.72k 84.25%
Latency Distribution
50% 448.00us
75% 620.00us
90% 797.00us
99% 1.38ms
123958 requests in 10.10s, 20.21MB read
Non-2xx or 3xx responses: 123847
Requests/sec: 12270.53
Transfer/sec: 2.00MB

# 限流后,可以看到,一共123958个, 失败了 123847 个
3. 参考资料