最近收到个给接口限制QPS的需求,起初不知道从哪入手,折腾了几天,终于找到了些方案,在此记录一下吧。

限速常见算法

漏桶算法(leaky bucket)

算法原理大概如图
在这里插入图片描述
大致可理解为

  • 水(请求)从上方倒入水桶,从水桶下方流出(被处理);
  • 来不及流出的水存在水桶中(缓冲),以固定速率流出;
  • 水桶满后水溢出(丢弃)。

这里我个人理解为:
强调的是匀速处理请求,毕竟水是匀速流出的。

令牌桶算法(token bucket)

算法原理大概如图
在这里插入图片描述
基本思想:

  • 令牌以固定速率产生,并缓存到令牌桶中;
  • 令牌桶放满时,多余的令牌被丢弃;
  • 请求要消耗等比例的令牌才能被处理;
  • 令牌不够时,请求被缓存。

然而令牌桶算法中,最大不同是它多了这个桶,它可以把令牌(可以理解为通行证)缓存起来,也就是说在某个时间段内,它可以不均匀的消耗令牌。

举个栗子:某个接口允许每秒100的QPS。
漏桶算法:平均到毫秒的话,大概就是每10ms允许通过一个请求,它强调匀速。
令牌桶算法:它则可以在前10ms通过全部的100个请求,剩余的时间不让通过(实际有些不同,毕竟受限于令牌桶容量以及它令牌是匀速产生的)。

nginx限速

下面使用的是 lua-resty-limit-traffic,该模块主要用于在OpenResty/ngx_lua中控制流量。
其中QPS限制主要有两种模式,即resty.limit.reqresty.limit.count

相关环境为Windwos上使用openresty。

resty.limit.req

这个是类似前面漏桶算法的实现。使用方法如下:
nginx.conf文件部分内容

        location /other {
            access_by_lua_block {
                -- https://github.com/openresty/lua-resty-limit-traffic

                -- well, we could put the require() and new() calls in our own Lua
                -- modules to save overhead. here we put them below just for
                -- convenience.

                local limit_req = require "resty.limit.req"

                -- limit the requests under 200 req/sec with a burst of 100 req/sec,
                -- that is, we delay requests under 300 req/sec and above 200
                -- req/sec, and reject any requests exceeding 300 req/sec.
                -- 关键是这两个参数,正常即200QPS,瞬间的话允许300QPS
                local lim, err = limit_req.new("my_limit_store", 200, 100)
                if not lim then
                    ngx.log(ngx.ERR,
                            "failed to instantiate a resty.limit.req object: ", err)
                    return ngx.exit(500)
                end

                -- the following call must be per-request.
                -- here we use the remote (IP) address as the limiting key
                local key = ngx.var.binary_remote_addr
                local delay, err = lim:incoming(key, true)
                if not delay then
                    if err == "rejected" then
                        return ngx.exit(503)
                    end
                    ngx.log(ngx.ERR, "failed to limit req: ", err)
                    return ngx.exit(500)
                end

                if delay >= 0.001 then
                    -- the 2nd return value holds the number of excess requests
                    -- per second for the specified key. for example, number 31
                    -- means the current request rate is at 231 req/sec for the
                    -- specified key.
                    local excess = err

                    -- the request exceeding the 200 req/sec but below 300 req/sec,
                    -- so we intentionally delay it here a bit to conform to the
                    -- 200 req/sec rate.
                    ngx.sleep(delay)
                end

                ngx.say("other!")
            }

        }

而且还要在http标签下,加上这一行lua_shared_dict my_limit_store 100m;,用于声明一个共享字典,用于存储访问次数等信息。

然后nginx -s reload重启nginx,再压测观察效果。
压测可以使用Apache JmeterApache Benchmark等工具,这里就不详细介绍了。

resty.limit.count

这个模块就是令牌桶思路的实现了,感觉是更符合我们预期的。

同样的nginx.conf里面部分内容:

       location /acc {
            access_by_lua_file lua/access.lua;
            
            default_type 'text/plain';
            
            content_by_lua_block{
                ngx.say('ok')
            }
        }

以及http标签下添加如下内容:

    init_by_lua_block {
        require "resty.core"
    }

    lua_shared_dict my_limit_store 100m;

然后就是lua目录里的access.lua文件了


local limit_count = require "resty.limit.count"
-- rate: 100 requests per 1s
-- 即限制每秒100的QPS
local lim, err = limit_count.new("my_limit_store", 100, 1)
if not lim then
    ngx.log(ngx.ERR, "failed to instantiate a resty.limit.count object: ", err)
    return ngx.exit(500)
end
-- use the Authorization header as the limiting key
local key = ngx.req.get_headers()["Authorization"] or "public"
local delay, err = lim:incoming(key, true)

if not delay then
    if err == "rejected" then
        ngx.header["X-RateLimit-Limit"] = "100"
        ngx.header["X-RateLimit-Remaining"] = 0
        return ngx.exit(403)
    end
    ngx.log(ngx.ERR, "failed to limit count: ", err)

    return ngx.exit(500)
end
local remaining = err

ngx.header["X-RateLimit-Limit"] = "100"
ngx.header["X-RateLimit-Remaining"] = remaining

参考链接



Be Yourself !