如果您正在构建公共使用的API,那么很可能需要实现某种形式的限流,以防止客户端过快地发出大量请求,从而给服务器带来很大的压力。
接下来的内容我们将创建中间件来实现这一点。
本质上,这个中间件检查在过去N秒钟内服务器接收到多少请求,如果请求太多就向客户端发送"429 Too Many Requests"响应。我们需要将这个中间件放在业务处理程序之前,对请求被处理之前进行拦截,避免对请求进行JSON序列化或数据库查询等操作。
你将学习到:
- 关于令牌桶限流算法背后的原理,以及我们如何在API或web应用程序的上下文中应用它们。
- 如何创建中间件来对API请求限速,首先通过创建单个全局限流器,然后进行扩展支持基于IP地址的客户端限流。
- 如何让限流器的参数在运行时可配置,包括关闭限流器等控制。
全局限流
我们先为应用程序创建一个全局限流器,然后慢慢深入。全局限流将考虑API接收到的所有请求(而不是为每个客户端设置单独的限速)。
可以利用x/time/rate包来帮助我们,而不是从头开始编写限流逻辑,因其非常复杂和耗时。x/time/rate包提供了经过测试的令牌桶限流器。先下载依赖包:
$ go get golang.org/x/time/rate@latest
go: downloading golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
go: added golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
在开始写代码之前,我们介绍下令牌桶限流是如何工作的。x/time/rate包的官方文档描述:
限流器能控制事件发生的频率。限流器实现了一个大小为b的“令牌桶”,最初桶装满token并以每秒r个令牌的速率重新填充。
将这些内容放在API上下文中可以理解为:
- 开始有一个包含b个tokens的桶。
- 每当我们接收到HTTP请求时,我们将从桶中删除一个令牌。
- 每隔1/r秒,将一个令牌添加回桶中——最多可达b个令牌总数。
- 如果我们收到一个HTTP请求,而桶是空的,那么我们应该返回一个429 Too Many Requests响应。
在实践中,这意味着我们的应用程序允许最大“突发”b个连续的HTTP请求,但随着时间的推移,它将允许平均每秒r个请求。
根据x/time/rate包,为了创建一个令牌桶限流器,我们需要使用NewLimiter()函数,其包含如下参数:
// Limit类型是float64的别名
func NewLimiter(r Limit, b int) *Limiter
因此,如果我们要创建一个每秒允许2个请求,支持4个突发请求的限流器,我们可以使用如下代码实现:
//每秒允许2个请求,一次最多4个请求
limiter := rate.NewLimiter(2, 4)
执行全局限流
前面我们从宏观角度解释限流,下面让我们进入代码,看看它在实践中是如何工作的。
使用中间件模式的一个优点是,它可以包含“初始化”代码,当我们用中间件包装一些东西时,它只运行一次,就可以处理所有的请求。
func (app *application) exampleMiddleware(next http.Handler) http.Handler {
//当使用中间件封装后,这里代码只运行一次
return http.HandlerFunc(func(w http.ResponseWriter, r *http.request) {
//这里代码在每个请求到来都会执行
next.ServerHTTP(w, r)
})
}
在我们的例子中,将创建一个rateLimit()中间件方法,它将创建一个新的限流器作为“初始化”代码的一部分,然后对随后处理的每个请求使用这个限流器。打开cmd/api/middleware.go文件并创建以下中间件:
package main
...
func (app *application)rateLimit(next http.Handler) http.Handler {
//初始化限流器
limit := rate.NewLimiter(2, 4)
//运行的是一个闭包
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//调用limit.Allow()函数检查请求是否允许,不允许就返回429
if !limit.Allow(){
app.rateLimitExceededResponse(w, r)
return
}
next.ServeHTTP(w, r)
})
}
在这段代码中,每当调用限流器上的Allow()方法时,只会从桶中消耗一个令牌。如果桶中没有令牌,那么Allow()将返回false,以此触发向客户端返回429 Too Many Requests响应。
要注意的是Allow()方法实现使用了互斥锁保护,并发使用是安全的。
下面到cmd/api/errors.go文件创建rateLimitExeededRespose()帮助函数:
package main
...
func (app *application) rateLimitExceededResponse(w http.ResponseWriter, r *http.Request) {
message := "rate limit exceeded"
app.errorResponse(w, r, http.StatusTooManyRequests, message)
}
最后,在cmd/api/routes.go文件中需要将rateLimit()中间件添加到中间件服务链中。它应该在我们的panicRecovery中间件之前运行(以便恢复rateLimit()中有任何panic发生),但是应该尽早使用它,以防止服务器进行不必要的工作。
File: cmd/api/routes.go
package main
...
func (app *application) routes() http.Handler {
router := httprouter.New()
router.NotFound = http.HandlerFunc(app.notFoundResponse)
router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse)
router.HandlerFunc(http.MethodGet, "/v1/healthcheck", app.healthcheckHandler)
router.HandlerFunc(http.MethodGet, "/v1/movies", app.listMoviesHandler)
router.HandlerFunc(http.MethodPost, "/v1/movies", app.createMovieHandler)
router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.showMovieHandler)
router.HandlerFunc(http.MethodPatch, "/v1/movies/:id", app.updateMovieHandler)
router.HandlerFunc(http.MethodDelete, "/v1/movies/:id", app.deleteMovieHandler)
//用ratelimit()中间件处理router
return app.recoverPanic(app.rateLimit(router))
}
现在我们应该准备好进行测试了!重启服务,然后在另一个终端窗口执行以下命令批量发送请求到GET /v1/healthcheck接口。你会看到如下响应:
$ for i in {1..6}; do curl http://localhost:4000/v1/healthcheck; done
{
"status": "available",
"system_info": {
"environment": "development",
"version": "1.0.0"
}
}
{
"status": "available",
"system_info": {
"environment": "development",
"version": "1.0.0"
}
}
{
"status": "available",
"system_info": {
"environment": "development",
"version": "1.0.0"
}
}
{
"status": "available",
"system_info": {
"environment": "development",
"version": "1.0.0"
}
}
{
"error": "rate limit exceeded"
}
{
"error": "rate limit exceeded"
}
从这里我们可以看到,前4个请求成功了,因为我们的限流器设置为允许接收“突发”4个请求。一旦这4个请求被用完,桶中的令牌已经用完,API将返回“rate limit exceeded”错误响应。
如果您等待一秒钟并重新运行此命令,您应该会发现第二批中的一些请求再次成功,这是因为令牌桶以每秒两个令牌的速度重新填充到桶中。
网友评论