美文网首页go
【Go Web开发】API限流

【Go Web开发】API限流

作者: Go语言由浅入深 | 来源:发表于2022-02-28 21:25 被阅读0次

    如果您正在构建公共使用的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”错误响应。

    如果您等待一秒钟并重新运行此命令,您应该会发现第二批中的一些请求再次成功,这是因为令牌桶以每秒两个令牌的速度重新填充到桶中。

    相关文章

      网友评论

        本文标题:【Go Web开发】API限流

        本文链接:https://www.haomeiwen.com/subject/cbkrrrtx.html