OpenResty+Lua限流实战
OpenResty+Lua限流实战目录
一. 限制并发
二. 反向代理
三. 内部重定向
四. lua初始化
五. 限制接口时间窗请求数(非平滑)
六. 限制接口时间窗请求数(平滑)
a. 桶(无容量)
b. 漏桶(有桶容量)
七. 令牌桶
八. 组合各种limter
一. 限制并发
场景1:按照 ip 限制其并发连接数
原理:lua_share_dict是nginx所有woker和lua runtime共享的,当一个请求进来,往lua_share_dict记录键值对ip地址:1,当请求完成时再-1,再来一个在+1,设置一个上限5,当超过5时则拒绝请求,一定要注意内部重定向的问题!
1. 新建utils/limit_conn.lua模块
mkdir -p D:\dev\openresty-1.19.9.1\lua\utils
limit_conn.lua文件
-- utils/limit_conn.lua
local limit_conn = require "resty.limit.conn"
-- new 的第四个参数用于估算每个请求会维持多长时间,以便于应用漏桶算法(0.05是预估的并发处理时间)
-- 允许的最大并发为常规的8个,突发的2个,一共8+2=10个并发
local limit, limit_err = limit_conn.new("limit_conn_store", 8, 2, 0.05)
if not limit then
ngx.log(ngx.ERR, "failed to instantiate a resty.limit.conn object: ", limit_err)
return ngx.exit(500)
end
local _M = {}
function _M.incoming()
-- 获取IP
local key = ngx.var.binary_remote_addr
local delay, err = limit:incoming(key, true)
if not delay then
if err == "rejected" then
return ngx.exit(503) -- 超过的请求直接返回503(被拒绝的请求直接返回503)
end
ngx.log(ngx.ERR, "failed to limit req: ", err)
return ngx.exit(500)
end
-- 请求连接等信息会被加到shared dict中,在ctx中记录下,因为后面要告知连接断开,以处理其他连接
if limit:is_committed() then
local ctx = ngx.ctx
ctx.limit_conn_key = key
ctx.limit_conn_delay = delay
end
-- 其实这里的 delay 是上面说的并发处理时间的整数倍,
-- 举个例子,每秒处理100并发,桶容量200个,当同时来500个并发,则200个拒绝请求
-- 100个正在被处理,然后200个进入桶中暂存,被暂存的这200个连接中,0-100个连接其实应该延后0.05秒处理,
-- 101-200个则应该延后0.05*2=0.1秒处理(0.05是上面预估的并发处理时间)
if delay >= 0.001 then
ngx.log(ngx.WARN, "delaying conn, excess ", delay, "s per binary_remote_addr by limit_conn_store")
ngx.sleep(delay)
end
return ngx.var.uri
end
function _M.leaving()
local ctx = ngx.ctx
local key = ctx.limit_conn_key
if key then
local latency = tonumber(ngx.var.request_time) - ctx.limit_conn_delay
local conn, err = limit:leaving(key, latency)
if not conn then
ngx.log(ngx.ERR,
"failed to record the connection leaving ",
"request: ", err)
end
end
return ngx.var.uri
end
return _M
重点在于这句话local limit, limit_err = limit_conn.new("limit_conn_store", 8, 2, 0.05),允许的最大并发为常规的8个,突发的2个,一共8+2=10个并发
2. 修改nginx配置文件
# 注意 limit_conn_store 的大小需要足够放置限流所需的键值。
# 每个 $binary_remote_addr 大小不会超过 16 字节(IPv6 情况下),算上 lua_shared_dict 的节点大小,总共不到 64 字节。
# 100M 可以放 1.6M 个键值对
lua_shared_dict limit_conn_store 100m;
server {
listen 80;
server_name 192.168.8.188;
location /limit/conn {
default_type text/html;
access_by_lua_block {
-- limit_conn.lua放在D:\dev\openresty-1.19.9.1\lua\目录文件夹下
local limit_conn = require "utils.limit_conn"
-- 对于内部重定向或子请求,不进行限制。因为这些并不是真正对外的请求。
if ngx.req.is_internal() then
ngx.log(ngx.INFO,">> 内部重定向")
return
end
local uri = limit_conn.incoming()
ngx.log(ngx.INFO, string.format(">>> [%s]请求进来了!", uri))
}
content_by_lua_block {
-- 模拟请求处理时间,很重要,不加可能测试不出效果
-- 生产中没有请求是只返回一个静态的index.html的!
-- 模拟每个请求0.5秒处理完成
ngx.sleep(0.5)
ngx.say('/limit/conn')
}
log_by_lua_block {
local limit_conn = require "utils.limit_conn"
local uri = limit_conn.leaving()
ngx.log(ngx.INFO, string.format(">>> [%s]请求离开了!", uri))
}
}
}
重点在于这句话,模拟每个请求0.5秒处理完成
content_by_lua_block {
-- 模拟请求处理时间,很重要,不加可能测试不出效果
-- 生产中没有请求是只返回一个静态的index.html的!
-- 模拟每个请求0.5秒处理完成
ngx.sleep(0.5)
ngx.say('/limit/conn')
}
注意在限制连接的代码里面,我们用 ngx.ctx 来存储 limit_conn_key。这里有一个坑。内部重定向(比如调用了 ngx.exec)会销毁 ngx.ctx,导致 limit_conn:leaving() 无法正确调用。 如果需要限制连接业务里有用到 ngx.exec,可以考虑改用 ngx.var 而不是 ngx.ctx,或者另外设计一套存储方式。只要能保证请求结束时能及时调用 limit:leaving() 即可。
3.重新加载配置文件
nginx.exe -s reload
4.测试
上面的配置是每个请求处理0.5秒,并发是10
a. 10个请求,并发为1
ab -n 10 -c 1 http://localhost/limit/conn
# 请求全部成功,用时5s左右
Concurrency Level: 1
Time taken for tests: 5.012 seconds
Complete requests: 10
Failed requests: 0
b. 10个请求,并发为10
ab -n 10 -c 10 http://localhost/limit/conn
# 请求全部成功,用时1.5s左右
Concurrency Level: 10
Time taken for tests: 1.505 seconds
Complete requests: 10
Failed requests: 0
c. 20个请求,并发为10,并发为10并不会触发限制条件,所以能成功!注意和下面并发11的区别!
ab -n 20 -c 10 http://localhost/limit/conn
# 请求全部成功,用时2s左右
Concurrency Level: 10
Time taken for tests: 2.005 seconds
Complete requests: 20
Failed requests: 0
d. 22个请求,并发为11
ab -n 22 -c 11 http://localhost/limit/conn
# 11个成功,11个失败
Concurrency Level: 11
Time taken for tests: 1.506 seconds
Complete requests: 22
Failed requests: 11
Non-2xx responses: 11 # HTTP状态非2xx的有11个,说明限并发成功(只有有非2xx的返回才会显示这句话)
22个请求,并发为11 重点解释一下:
1.并发不是qps,并发11不是说第一秒发11个请求,然后第二秒再发送11个请求,而是发完第一波紧接着发第二波,每一波的间隔时间不一定是1秒,下面的1.506 seconds就能看出来,按理应该是2s但是并不是
2.第一波11个请求发送过去了,但是只能处理10个,所以成功了10个,紧接着第二波11个请求发过去了,但是第一波大部分未处理完成所以第二波的都失败了,也有处理完成了的可以接着处理,所以至少会成功10个,下面显示的是11个
3.此处的大量失败应该是并发超过了10,触发了限制条件让nginx worker线程睡眠了,所以导致后面的请求大量失败
4.-- 触发限制条件
if delay >= 0.001 then
ngx.sleep(delay) -- ngx worker睡眠
end
二. 反向代理
上面测试的是content_by_lua,也就是内容直接在lua中生成,但是实际中内容有可能是后端服务器生成的,所以可以设置反向代理或者负载均衡,如下为反向代理配置
location /limit/conn {
default_type text/html;
access_by_lua_block {
-- limit_conn.lua放在D:\dev\openresty-1.19.9.1\lua\目录文件夹下
local limit_conn = require "utils.limit_conn"
-- 对于内部重定向或子请求,不进行限制。因为这些并不是真正对外的请求。
if ngx.req.is_internal() then
ngx.log(ngx.INFO,">> 内部重定向")
return
end
local uri = limit_conn.incoming()
ngx.log(ngx.INFO, string.format(">>> [%s]请求进来了!", uri))
}
log_by_lua_block {
local limit_conn = require "utils.limit_conn"
local uri = limit_conn.leaving()
ngx.log(ngx.INFO, string.format(">>> [%s]请求离开了!", uri))
}
# 反向代理
proxy_pass http://172.17.0.3:8080;
proxy_set_header Host $host;
proxy_redirect off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 60;
proxy_read_timeout 600;
proxy_send_timeout 600;
}
三. 内部重定向
location / {
access_by_lua_block {...}
content_by_lua_block {...}
log_by_lua_block {...}
}
nginx是按照阶段来执行指令的,和配置文件顺序没有关系,nginx是先执行access_by_lua_block,再执行content_by_lua_block,最后执行log_by_lua_block的,当在访问curl 127.0.0.1/时,如果没有content_by_lua_block,这里有一个内部重定向,会将127.0.0.1/的请求重定向到127.0.0.1/index.html,所以会按顺序再次执行access_by_lua_block,所以access_by_lua_block执行了两次,log_by_lua_block却执行了一次,当时的我十分懵逼,而加上content_by_lua或者proxy_pass则不会导致重定向,总之有内容来源时不会重定向,没有则会去找index.html导致重定向!
测试
vim /usr/local/openresty/nginx/conf/nginx.conf
location / {
default_type text/html;
access_by_lua_block {
ngx.log(ngx.ERR,">>> access")
}
log_by_lua_block {
ngx.log(ngx.ERR,">>> log")
}
}
# 查看日志
tail -f /usr/local/openresty/nginx/logs/error.log
测试curl 127.0.0.1日志输出如下 access_by_lua_block执行了两次,并且页面上的内容是index.html的内容,说明发生了重定向 如果加上index.html,即curl 127.0.0.1/index.html,则不会发生重定向
...[lua] access_by_lua(default.conf:8):2: >>> access, client: 127.0.0.1, server: 192.168.8.188
...[lua] access_by_lua(default.conf:8):2: >>> access, client: 127.0.0.1, server: 192.168.8.188
...[lua] log_by_lua(default.conf:11):2: >>> log while logging request, client: 127.0.0.1, server: 192.168.8.188
加上content_by_lua则访问http://127.0.0.1不会发生重定向
四. lua初始化
local limit_conn = require "utils.limit_conn"
这句话local limit_conn = require "utils.limit_conn",limit_conn中的local limit, limit_err = limit_conn.new("limit_conn_store", 8, 2, 0.05)只会初始化一次,之后都是用的这一个实例,不会每个请求进来都要new一个limit_conn有点浪费性能而且还把参数都重置了,是不可取的,所以封装到了utils.limit_conn中!
五. 限制接口时间窗请求数(非平滑)
场景:限制 ip 每1s只能调用 10 次(允许在时间段开始的时候一次性放过10个请求)也就是说,速率不是固定的
也可以设置成别的,比如120/min,只需要修改个数和时间窗口(resty.limit.count和resty.limit.req区别在于:前者传入的是个数,后者传入的是速率)
resty.limit.count, 用于限制时间窗口内的请求数量限制,时间窗口可自定义
新建utils/limit_count.lua模块
-- utils/limit_count.lua
local limit_count=require "resty.limit.count"
-- rate: 10/s,每秒最多只能调用10次
local lim,err=limit_count.new("limit_count_store", 10, 1) -- 第二个参数次数,第三个参数时间窗口,单位s
if not lim then
ngx.log(ngx.ERR, "failed to instantiate a resty.limit.count object: ", err)
return ngx.exit(500)
end
local _M = {}
function _M.incoming()
local key = ngx.var.binary_remote_addr
local delay, err = lim:incoming(key, true)
if not delay then
if err == "rejected" then
ngx.header["X-RateLimit-Limit"] = "10"
ngx.header["X-RateLimit-Remaining"] = 0
return ngx.exit(503) -- 超过的请求直接返回503
end
ngx.log(ngx.ERR, "failed to limit req: ", err)
return ngx.exit(500)
end
-- 第二个参数是指定key的剩余调用量
local remaining = err
--X-RateLimit-Limit为最大调用量,X-RateLimit-Remaining为剩余调用量
ngx.header["X-RateLimit-Limit"] = "10"
ngx.header["X-RateLimit-Remaining"] = remaining
end
return _M
修改nginx配置文件
lua_shared_dict limit_count_store 100m;
# resty.limit.count 需要resty.core
init_by_lua_block {
require "resty.core"
}
server {
listen 80;
server_name 192.168.8.188;
location /limit/count {
default_type text/html;
access_by_lua_block {
-- limit_count.lua放在D:\dev\openresty-1.19.9.1\lua\目录文件夹下
local limit_count = require "utils.limit_count"
-- 对于内部重定向或子请求,不进行限制。因为这些并不是真正对外的请求。
if ngx.req.is_internal() then
return
end
limit_count.incoming()
}
content_by_lua_block {
-- 模拟请求处理时间,很重要,不加可能测试不出效果
-- 生产中没有请求是只返回一个静态的index.html的!
-- 模拟每个请求0.1秒处理完成
ngx.sleep(0.1)
ngx.say('/limit/count')
}
}
}
重新加载配置文件
nginx.exe -s reload
测试
上面的配置是10/s,不叠加
a. 10个请求,并发为10,1s内完成
ab -n 10 -c 10 http://localhost/limit/count
# 请求全部成功
Concurrency Level: 10
Time taken for tests: 0.202 seconds
Complete requests: 10
Failed requests: 0
b. 20个请求,并发为20,1s内完成
ab -n 20 -c 20 http://localhost/limit/count
# 请求成功10个,其余全部失败
Concurrency Level: 20
Time taken for tests: 0.202 seconds
Complete requests: 20
Failed requests: 10
(Connect: 0, Receive: 0, Length: 10, Exceptions: 0)
Non-2xx responses: 10
c.查看请求头curl -I http://localhost/limit/count,可以看到接口限流信息
HTTP/1.1 200 OK
Server: openresty/1.17.8.2
Date: Sat, 12 Sep 2020 09:46:06 GMT
Content-Type: application/octet-stream
Connection: keep-alive
X-RateLimit-Limit: 10 # 当前限制10个
X-RateLimit-Remaining: 9 # 剩余9个
六. 限制接口时间窗请求数(平滑)
a. 桶(无容量)
场景:限制 ip 每1min只能调用 120次(平滑处理请求,即每秒放过2个请求),速率是固定的,并且桶没有容量(容量为0)
新建utils/limit_req.lua模块
-- utils/limit_req.lua
local limit_req = require "resty.limit.req"
-- rate: 2/s即为120/min,burst设置为0,也就是没有桶容量,超过的都拒绝(rejected)
local lim, err = limit_req.new("limit_req_store", 2, 0)
if not lim then
ngx.log(ngx.ERR, "failed to instantiate a resty.limit.req object: ", err)
return ngx.exit(500)
end
local _M = {}
function _M.incoming()
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) -- 超过的请求直接返回503
end
ngx.log(ngx.ERR, "failed to limit req: ", err)
return ngx.exit(500)
end
end
return _M
修改nginx配置文件
lua_shared_dict limit_req_store 100m;
server {
listen 80;
server_name 192.168.8.188;
location /limit/req {
default_type text/html;
access_by_lua_block {
local limit_req = require "utils.limit_req"
-- 对于内部重定向或子请求,不进行限制。因为这些并不是真正对外的请求。
if ngx.req.is_internal() then
return
end
limit_req.incoming()
}
content_by_lua_block {
-- 模拟请求处理时间,很重要,不加可能测试不出效果
-- 生产中没有请求是只返回一个静态的index.html的!
-- 模拟每个请求0.1秒处理完成
ngx.sleep(0.1)
ngx.say('/limit/req')
}
}
}
重新加载配置文件
nginx.exe -s reload
测试
上面的配置是2/s即为120/min
1.请求时间限制为1s
ab -t 1 http://localhost/limit/req
# 实际请求1.1s,成功3个请求,符合预期
Time taken for tests: 1.100 seconds
Complete requests: 8656
Failed requests: 8653
(Connect: 0, Receive: 0, Length: 8653, Exceptions: 0)
Non-2xx responses: 8653
2. 请求时间限制为5s
ab -t 5 http://localhost/limit/req
# 实际请求5.1s,成功11个请求,符合预期
Concurrency Level: 1
Time taken for tests: 5.100 seconds
Complete requests: 40054
Failed requests: 40043
(Connect: 0, Receive: 0, Length: 40043, Exceptions: 0)
Non-2xx responses: 40043
b. 漏桶(有桶容量)
场景:限制 ip 每1min只能调用 120次(平滑处理请求,即每秒放过2个请求),速率是固定的,并且桶的容量有容量限制(设置burst)
新建utils/limit_req_bucket.lua模块
只需要在桶(无容量)的基础之上增加burst的值即可,并且增加delay的处理
-- utils/limit_req_bucket.lua
local limit_req = require "resty.limit.req"
-- rate: 2/s即为120/min,增加桶容量为1/s,超过2/s不超过(2+1)/s的delay,排队等候,这就是标准的漏桶
-- 进行平滑处理请求,每秒放过2个请求(2/s),超过部分进入桶中等待,(桶容量为1),如果桶也满了,则进行限流
local lim, err = limit_req.new("limit_req_store", 2, 1)
if not lim then
ngx.log(ngx.ERR, "failed to instantiate a resty.limit.req object: ", err)
return ngx.exit(500)
end
local _M = {}
function _M.incoming()
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) -- 超过的请求直接返回503
end
ngx.log(ngx.ERR, "failed to limit req: ", err)
return ngx.exit(500)
end
-- 此方法返回,当前请求需要delay秒后才会被处理
-- 所以此处对桶中请求进行延时处理,让其排队等待,就是应用了漏桶算法
-- 此处也是与令牌桶的主要区别
if delay >= 0.001 then
ngx.sleep(delay)
end
end
return _M
修改nginx配置文件
lua_shared_dict limit_req_store 100m;
server {
listen 80;
server_name 192.168.8.188;
location /limit/req/bucket {
default_type text/html;
access_by_lua_block {
local limit_req = require "utils.limit_req_bucket"
-- 对于内部重定向或子请求,不进行限制。因为这些并不是真正对外的请求。
if ngx.req.is_internal() then
return
end
limit_req.incoming()
}
content_by_lua_block {
-- 模拟每个请求的耗时
ngx.sleep(0.1)
ngx.say('/limit/req/bucket')
}
}
}
重新加载配置文件
nginx.ext -s reload
测试
上面的配置是2/s,漏桶容量为1/s,即总共3/s,模拟的每个请求耗时为0.1s,那么1s内能处理至少10个请求
请求时间限制为1s
ab -t 1 http://localhost/limit/req/bucket
# 实际请求1.102s,成功3个请求,1s两个请求,一个是delay,符合预期
Time taken for tests: 1.103 seconds
Complete requests: 3
Failed requests: 0
七. 令牌桶
场景:限制 ip 每1min只能调用 120次(平滑处理请求,即每秒放过2个请求),但是允许一定的突发流量(突发的流量,就是桶的容量(桶容量为60),超过桶容量直接拒绝。
令牌桶其实可以看作是漏桶的逆操作,看我们对把超过请求速率而进入桶中的请求如何处理,如果是我们把这部分请求放入到等待队列中去,那么其实就是用了漏桶算法,但是如果我们允许直接处理这部分的突发请求,其实就是使用了令牌桶算法。
这边只要将上面漏桶算法关于桶中请求的延时处理的代码修改成直接送到后端服务就可以了,这样便是使用了令牌桶
新建utils/limit_req_token_bucket.lua模块
-- utils/limit_req_token_bucket.lua
local limit_req = require "resty.limit.req"
-- rate: 2/s即为120/min,增加桶容量为60/s,超过2/s不到(2+60)/s的突发流量直接放行
local lim, err = limit_req.new("limit_req_store", 2, 60)
if not lim then
ngx.log(ngx.ERR, "failed to instantiate a resty.limit.req object: ", err)
return ngx.exit(500)
end
local _M = {}
function _M.incoming()
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) -- 超过的请求直接返回503
end
ngx.log(ngx.ERR, "failed to limit req: ", err)
return ngx.exit(500)
end
if delay >= 0.001 then
-- 不做任何操作,直接放行突发流量
-- ngx.sleep(delay)
end
end
return _M
修改nginx配置文件
lua_shared_dict limit_req_store 100m;
server {
listen 80;
server_name 192.168.8.188;
location /limit/req/token/bucket {
default_type text/html;
access_by_lua_block {
local limit_req = require "utils.limit_req_token_bucket"
-- 对于内部重定向或子请求,不进行限制。因为这些并不是真正对外的请求。
if ngx.req.is_internal() then
return
end
limit_req.incoming()
}
content_by_lua_block {
-- 模拟每个请求的耗时
ngx.sleep(0.1)
ngx.say('/limit/req/token/bucket')
}
}
}
重新加载配置文件
nginx.exe -s reload
测试
上面模拟的每个请求耗时为0.1s,那么1s内能处理至少10个请求
时间限制为1s
ab -c 10 -t 5 http://localhost/limit/req/token/bucket
# 实际请求5s,成功66个请求,可以看到是远远超过2个请求的,多余就是在处理突发请求
Concurrency Level: 10
Time taken for tests: 5.001 seconds
Complete requests: 10555
Failed requests: 10489
(Connect: 0, Receive: 0, Length: 10489, Exceptions: 0)
Non-2xx responses: 10492
八. 组合各种limter
上面的三种限速器conn、count、req可以进行各种组合,比如一个限速器是限制主机名的,一个是限制ip的,可以组合起来使用