最近收到个给接口限制QPS的需求,起初不知道从哪入手,折腾了几天,终于找到了些方案,在此记录一下吧。
限速常见算法
漏桶算法(leaky bucket)
算法原理大概如图
大致可理解为
- 水(请求)从上方倒入水桶,从水桶下方流出(被处理);
- 来不及流出的水存在水桶中(缓冲),以固定速率流出;
- 水桶满后水溢出(丢弃)。
这里我个人理解为:
强调的是匀速处理请求,毕竟水是匀速流出的。
令牌桶算法(token bucket)
算法原理大概如图
基本思想:
- 令牌以固定速率产生,并缓存到令牌桶中;
- 令牌桶放满时,多余的令牌被丢弃;
- 请求要消耗等比例的令牌才能被处理;
- 令牌不够时,请求被缓存。
然而令牌桶算法中,最大不同是它多了这个桶,它可以把令牌(可以理解为通行证)缓存起来,也就是说在某个时间段内,它可以不均匀的消耗令牌。
举个栗子:某个接口允许每秒100的QPS。
漏桶算法:平均到毫秒的话,大概就是每10ms允许通过一个请求,它强调匀速。
令牌桶算法:它则可以在前10ms通过全部的100个请求,剩余的时间不让通过(实际有些不同,毕竟受限于令牌桶容量以及它令牌是匀速产生的)。
nginx限速
下面使用的是 lua-resty-limit-traffic,该模块主要用于在OpenResty/ngx_lua中控制流量。
其中QPS限制主要有两种模式,即resty.limit.req
和resty.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 Jmeter、Apache 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
参考链接
Comments | 1 条评论