OpenResty+Lua限流实战
当业务量越来越大的时候,为了能保证服务的运行,限流是必不可少的!OpenResty是一个高性能网关
OpenResty® is a dynamic web platform based on NGINX and LuaJIT.
OpenResty = Nginx + Lua,Lua是高性能脚本语言,有着C语言的执行效率但是又比C简单,能很方便的扩展OpenResty 的功能。
Lua 是由巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个研究小组于1993年开发的一种轻量、小巧的脚本语言,用标准 C 语言编写,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
官网:http://www.lua.org/
实战环境
docker + CentOS8 + Openresty 1.17.8.2
Lua限流模块
https://github.com/openresty/lua-resty-limit-traffic
Lua的库一般都是小巧轻便且功能都具备,这个限流库核心文件一共就四个,几百行代码就能实现限流功能,Lua的其他库也是这样,比如redis的库还是Http的库,麻雀虽小五脏俱全!
环境准备
docker run -dit --name gw --privileged centos /usr/sbin/init
docker exec -it gw bash
在gw中
# 安装openresty
yum install -y yum-utils
yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
yum install -y openresty
# 安装工具等
yum install -y net-tools vim telnet git httpd
# Openresty自带了lua-resty-limit-traffic组件,如果没有带,下载到/usr/local/openresty/lualib/resty/limit/文件夹即可
# 下载lua-resty-limit-traffic组件
[ `ls /usr/local/openresty/lualib/resty/limit/ | wc -l` = 0 ] && echo '请安装限速组件' || echo '已经安装限速组件'
# 安装了请忽略
cd ~ && git clone https://github.com/openresty/lua-resty-limit-traffic.git
mkdir -p /usr/local/openresty/lualib/resty/limit/
cp lua-resty-limit-traffic/lib/resty/limit/*.lua /usr/local/openresty/lualib/resty/limit/
# 启动openresy
openresty
限并发
场景:按照 ip 限制其并发连
参考:
https://moonbingbing.gitbooks.io/openresty-best-practices/content/ngx_lua/lua-limit.html
https://github.com/openresty/lua-resty-limit-traffic/blob/master/lib/resty/limit/conn.md
https://developer.aliyun.com/article/759299
原理:lua_share_dict是nginx所有woker和lua runtime共享的,当一个请求来,往lua_share_dict记录键值对ip地址:1
,当请求完成时再-1,再来一个在+1,设置一个上限5,当超过5时则拒绝请求,一定要注意内部重定向的问题!
- OpenResty执行阶段 tag:lua执行流程;执行阶段;openresty执行流程
- [为啥access_by_lua执行两次](参考文章/openresty why does access_by_lua_file call twice when accessing the root directory - Stack Overflow.md)
环境搭建
新建utils/limit_conn.lua模块
mkdir -p /usr/local/openresty/lualib/utils
cat > /usr/local/openresty/lualib/utils/limit_conn.lua <<EOF
-- utils/limit_conn.lua
local limit_conn = require "resty.limit.conn"
-- new 的第四个参数用于估算每个请求会维持多长时间,以便于应用漏桶算法
local limit, limit_err = limit_conn.new("limit_conn_store", 8, 2, 0.05)
if not limit then
error("failed to instantiate a resty.limit.conn object: ", limit_err)
end
local _M = {}
function _M.incoming()
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
end
ngx.log(ngx.ERR, "failed to limit req: ", err)
return ngx.exit(500)
end
if limit:is_committed() then
local ctx = ngx.ctx
ctx.limit_conn_key = key
ctx.limit_conn_delay = delay
end
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
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
end
return _M
EOF
重点在于这句话local limit, limit_err = limit_conn.new("limit_conn_store", 8, 2, 0.05)
,允许的最大并发为常规的8个,突发的2个,一共8+2=10个并发,详情参考https://github.com/openresty/lua-resty-limit-traffic/blob/master/lib/resty/limit/conn.md#new
被拒绝的请求直接返回503
if err == "rejected" then
return ngx.exit(503) -- 超过的请求直接返回503
end
修改nginx配置文件
# 备份一下配置文件
cd /usr/local/openresty/nginx/conf/ && \cp nginx.conf nginx.conf.bak
# 添加配置
echo '' > /usr/local/openresty/nginx/conf/nginx.conf
vim /usr/local/openresty/nginx/conf/nginx.conf
添加如下内容
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
lua_code_cache on;
# 注意 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;
location / {
access_by_lua_block {
local limit_conn = require "utils.limit_conn"
-- 对于内部重定向或子请求,不进行限制。因为这些并不是真正对外的请求。
if ngx.req.is_internal() then
ngx.log(ngx.INFO,">> 内部重定向")
return
end
limit_conn.incoming()
ngx.log(ngx.INFO,">>> 请求进来了!")
}
content_by_lua_block {
-- 模拟请求处理时间,很重要,不加可能测试不出效果
-- 生产中没有请求是只返回一个静态的index.html的!
ngx.sleep(0.5)
}
log_by_lua_block {
local limit_conn = require "utils.limit_conn"
limit_conn.leaving()
ngx.log(ngx.INFO,">>> 请求离开了!")
}
}
}
}
重点在于这句话,模拟每个请求0.5秒处理完成
content_by_lua_block {
ngx.sleep(0.5)
}
注意在限制连接的代码里面,我们用 ngx.ctx
来存储 limit_conn_key
。这里有一个坑。内部重定向(比如调用了 ngx.exec
)会销毁 ngx.ctx
,导致 limit_conn:leaving()
无法正确调用。 如果需要限连业务里有用到 ngx.exec
,可以考虑改用 ngx.var
而不是 ngx.ctx
,或者另外设计一套存储方式。只要能保证请求结束时能及时调用 limit:leaving()
即可。
重新加载配置文件
openresty -s reload
测试
上面的配置是每个请求处理0.5秒,并发是10
- 10个请求,并发为1
ab -n 10 -c 1 127.0.0.1/
# 请求全部成功,用时5s左右
Concurrency Level: 1
Time taken for tests: 5.012 seconds
Complete requests: 10
Failed requests: 0
- 10个请求,并发为10
ab -n 10 -c 10 127.0.0.1/
# 请求全部成功,用时1.5s左右
Concurrency Level: 10
Time taken for tests: 1.505 seconds
Complete requests: 10
Failed requests: 0
- 20个请求,并发为10,并发为10并不会触发限制条件,所以能成功!注意和下面并发11的区别!
ab -n 20 -c 10 127.0.0.1/