本文来自于对nginx和openresty文档和网上文章的学习记录,非纯粹原创
一、nginx本身支持的限流功能
主要是依靠ngx_http_limit_req_module
,ngx_http_limit_conn_module
两个模块中的,limit_req与limit_conn两组配置来实现rps与连接数两个维度的限流。
1、limit_req_zone与limit_req
示例:
limit_req_zone $binary_remote_addr zone=perip_rps:10m rate=5r/s; #单ip每秒限制5个请求
limit_req_zone $server_name zone=perserver_rps:10m rate=3000r/s; #每个server每秒限制处理3000个请求
上面分别是按照ip和server来限流rps,zone=perip_rps:10m是设定这个limit_req_zone的名字为perid_rps,且在nginx内存里分配10m的空间来存储访问频次信息,rate=15r/s表示每秒15个请求,30r/m每分钟30次请求。
一般在http里配置好了limit_req_zone之后,就可以在server或者location里边配置limit_req了,比如:
limit_req zone=perserver_rps burst=2000 nodelay; #server每秒请求限流
limit_req zone=perip_rps burst=10 nodelay; #每个ip每秒请求如果超过limit_req_zone的配置,最多可以缓冲10个
limit_req模块使用的是漏桶算法。参考:http://nginx.org/en/docs/http/ngx_http_limit_req_module.html#limit_req
2、limit_conn_zone与limit_conn
limit_conn_zone $binary_remote_addr zone=perip_conn:10m;
limit_conn perip_conn 10; #每个ip最多允许10个连接
这俩一般用来控制单客户端ip可以连多少连接到nginx上。总的连接一般就直接在nginx.conf里配置worker_connections 1024;
来限制住了。
附nginx.conf配置文件片段:
http {
include mime.types;
default_type application/octet-stream;
#nginx限流配置
#每秒请求数
limit_req_log_level error;
limit_req_status 503;
limit_req_zone $binary_remote_addr zone=perip_rps:10m rate=15r/s; #单ip每秒限制15个请求
limit_req_zone $server_name zone=perserver_rps:10m rate=1500r/s; #每个server每秒限制处理1500个请求
#连接数限制
limit_conn_log_level error;
limit_conn_status 503;
limit_conn_zone $binary_remote_addr zone=perip_conn:10m;
limit_conn_zone $server_name zone=perserver_conn:10m;
upstream seckillcore {
server 127.0.0.1:8080;
}
server {
listen 80;
server_name localhost;
#开发调试模式、关闭lua代码缓存,生产环境请勿关闭
lua_code_cache off;
#charset koi8-r;
#access_log logs/host.access.log main;
limit_req zone=perserver_rps burst=10 nodelay; #server每秒请求限流
location / {
root html;
index index.html index.htm;
}
#预约接口
location /seckill/rest/appointment {
limit_req zone=perip_rps burst=10 nodelay; #每个ip每秒请求如果超过limit_req_zone的配置,最多可以缓冲10个
limit_conn perip_conn 10; #每个ip最多允许10个连接
default_type text/html;
access_by_lua_file lua/wangan/seckill/appointment_check.lua;
proxy_pass http://seckillcore;
proxy_redirect default;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
参考:https://blog.csdn.net/myle69/article/details/83512617
二、openresty限流功能
lua-resty-limit-traffic
库中的resty.limit.count
模块、resty.limit.conn
模块、resty.limit.req
模块。
我们先从源码入手:
1、count.lua 限制单位时间内的请求数(请求速率)
每隔window时间在dict里放一个key、到时间自动失效、失效时间也是window。限制啥要看key按照什么去设置,比如每个ip设置一个key就是限制ip的单位时间请求数,key对应的value记的是ip1在这个时间window所允许的请求数limit,每来一个请求则-1,如果减没了就503拒绝。有点像弱化版的令牌桶算法,只不过没有按照一定速率添加令牌这个操作罢了。
关键代码:
--incoming
remaining, err = dict:incr(key, -1, limit)
ok, err = dict:expire(key, window)
2、conn.lua 限制连接数或者也可以说是并发数,标准ngx_limit_conn模块的lua增强版
key设置ip1的话,那么就是ip1来一个请求,在access阶段去调用income(),则value从0开始 +1,加到max则拒绝,同时在log阶段去调leave(),value -1。所以在某一时刻看过去,key=ip1的value里边记着的就是此时的并发连接数。
这里要结合两个执行阶段access和log分别调用income和leave去理解,笔者一开始没绕过这个弯儿,明明conn说是限制连接数的,为啥统计的是请求,连接复用的情况下不是统计请求要比连接多?就是因为没有注意到leave,实际上每个请求结束都会减1,这样动态的来看某一时刻value里的值就是这个ip到openresty的连接数、因为这+1和-1使得每个连接在某一个时刻只会有一个请求计数。
关键代码:
--incoming
conn, err = dict:incr(key, 1, 0)
if conn > max + self.burst then
conn, err = dict:incr(key, -1)
if not conn then
return nil, err
end
return nil, "rejected"
end
--leaving
local conn, err = dict:incr(key, -1)
上面为了说明方便,假设了burst=0,也没讨论根据delay(也就是>max但是<max+burst这部分连接)如何sleep进行限制连接处理,以及如何在log阶段的leaving里边自动修正delay的逻辑。代码如下:
--incoming
if conn > max then --conn介于max和max + burst之间
-- make the excessive connections wait
-- unit_delay相当于是个预估的请求处理时长的基准值
return self.unit_delay * floor((conn - 1) / max), conn
end
--leaving
-- req_latency = tonumber(ngx.var.request_time) - ctx.limit_conn_delay
-- 即实际请求处理时长 - limit:incoming(key, true)
function _M.leaving(self, key, req_latency)
assert(key)
local dict = self.dict
local conn, err = dict:incr(key, -1)
if not conn then
return nil, err
end
if req_latency then
local unit_delay = self.unit_delay
self.unit_delay = (req_latency + unit_delay) / 2
end
return conn
end
unit_delay = (req_latency + unit_delay) / 2 修正基准时间
req_latency = request_time - limit_conn_delay 实际处理时间与sleep时间的差值
limit_conn_delay = unit_delay * floor((conn - 1) / max) 需要sleep的时间
参考:https://moonbingbing.gitbooks.io/openresty-best-practices/content/ngx_lua/lua-limit.html
3、req.lua 标准ngx_limit_req模块的lua接口
使用这个模块可以实现使用漏桶和令牌桶算法来平滑的限制请求rps
参考:https://segmentfault.com/a/1190000022585978 《接入层限流之OpenResty提供的Lua限流模块lua-resty-limit-traffic》
req.lua
里边比上面的conn和count要复杂一些,核心代码如下:
ffi.cdef[[
struct lua_resty_limit_req_rec {
unsigned long excess;
uint64_t last; /* time in milliseconds */
/* integer value, 1 corresponds to 0.001 r/s */
};
]]
local const_rec_ptr_type = ffi.typeof("const struct lua_resty_limit_req_rec*")
local rec_size = ffi.sizeof("struct lua_resty_limit_req_rec")
-- we can share the cdata here since we only need it temporarily for
-- serialization inside the shared dict:
local rec_cdata = ffi.new("struct lua_resty_limit_req_rec")
function _M.new(dict_name, rate, burst)
local dict = ngx_shared[dict_name]
if not dict then
return nil, "shared dict not found"
end
assert(rate > 0 and burst >= 0)
local self = {
dict = dict,
rate = rate * 1000,
burst = burst * 1000,
}
return setmetatable(self, mt)
end
function _M.incoming(self, key, commit)
local dict = self.dict
local rate = self.rate
local now = ngx_now() * 1000 --时间戳,ms
local excess
-- it's important to anchor the string value for the read-only pointer
-- cdata:
local v = dict:get(key)
if v then
if type(v) ~= "string" or #v ~= rec_size then
return nil, "shdict abused by other users"
end
local rec = ffi_cast(const_rec_ptr_type, v)
local elapsed = now - tonumber(rec.last) --过了多少ms了
-- print("elapsed: ", elapsed, "ms")
-- we do not handle changing rate values specifically. the excess value
-- can get automatically adjusted by the following formula with new rate
-- values rather quickly anyway.
--[[
我们不专门处理变化的速率值rate。因为剩余值excess可以通过以下公式自动调整,并使用新的速率值,调整速度相当快。
上一次剩余值 - 这段时间可以处理的数量 + 1000
]]
excess = max( tonumber(rec.excess) - rate * abs(elapsed) / 1000 + 1000,
0 )
-- print("excess: ", excess)
if excess > self.burst then
return nil, "rejected"
end
else
excess = 0
end
if commit then
rec_cdata.excess = excess
rec_cdata.last = now
dict:set(key, ffi_str(rec_cdata, rec_size))
end
-- return the delay in seconds, as well as excess
-- 剩余除以速率就是延迟时间
return excess / rate, excess / 1000
end
大致思路就是逐个请求去判断,根据至上一个请求到此刻经过的时间和rate,可以计算出这个时间段允许通过的请求。如果小于burst则返回延迟,否则拒绝。
设置burst = 0,漏桶容量0,漏不过去的直接拒绝
local limit_req = require "resty.limit.req"
local lim, err = limit_req.new("my_limit_req_store", 2, 0) -- rate = 2r/s, burst = 0
local delay, err = lim:incoming(key, true)
漏桶算法:设置burst = 100,漏桶容量100, 超过容量的拒绝,没超过的就计算一下延迟,然后ngx.sleep(delay)控制一下请求的流入速度。
local lim, err = limit_req.new("my_limit_req_store", 2, 60)
local delay, err = lim:incoming(key, true)
if delay >= 0.001 then
ngx.sleep(delay)
end
令牌桶算法:设置burst=100,桶容量100,超过容量拒绝,没超过的话可以一次放过去,nodelay,也就是允许一定的突发流量。这个时候就是令牌桶算法的思路:桶里100个令牌,来的请求拿1个令牌通过,没令牌拿了则拒绝。
local lim, err = limit_req.new("my_limit_req_store", 2, 100)
local delay, err = lim:incoming(key, true)
if delay >= 0.001 then
-- 令牌桶就这里直接放到后端服务器,不做sleep延迟处理了
-- ngx.sleep(delay)
end
其实nginx的ngx_http_limit_req_module
这个模块中的delay和nodelay也就是类似此处对桶中请求是否做延迟处理的两种方案,也就是分别对应的漏桶和令牌桶两种算法。
三、总结:
我们需要:
- 单ip需要限制连接数、以及rps ,防止恶意请求脚本来刷服务器。
#http
limit_req_zone $binary_remote_addr zone=perip_rps:10m rate=5r/s; #单ip每秒限制5个请求
limit_conn_zone $binary_remote_addr zone=perip_conn:10m;
#location
limit_req zone=perip_rps burst=10 nodelay; #每个ip每秒请求如果超过limit_req_zone的配置,最多可以缓冲10个
limit_conn perip_conn 5; #每个ip最多允许同时5个连接
- 重点location接口使用漏桶或令牌桶平滑限制rps,保护后端的核心服务。
#http
limit_req_zone $server_name zone=perserver_rps:10m rate=1500r/s; #每个server每秒限制处理1500个请求
#server
limit_req zone=perserver_rps burst=100 nodelay; #server每秒请求限流
或者使用openresty:
location /seckill/rest/appointment {
limit_req zone=perip_rps burst=10 nodelay; #每个ip每秒请求如果超过limit_req_zone的配置,最多可以缓冲10个
limit_conn perip_conn 5; #每个ip最多允许5个连接
default_type text/html;
# 在access阶段使用resty.limit.req做令牌桶或者漏桶限流
access_by_lua_file lua/wangan/seckill/appointment_check.lua;
proxy_pass http://seckillcore;
proxy_redirect default;
# log_by_lua_file src/log.lua; # 如果是漏桶那么在log阶段ngx.sleep(delay),如果是令牌桶则不需要
}
nginx自带的限流功能不需要代码开发,只在nginx.conf配置就可以了。可以做到按ip或按server来限流rps和连接数。
openresty使用resty.limit库进行lua开发,也可以按ip和server来限流rps、连接数。
ps:学习了openresty限流之后对nginx的限流原理也理解更深入了,但是笔者没认识到什么场景非用openresty限流替换nginx限流不可。。。