一:服务限流功能点
1:根据请求入参中的服务标识判断nginx后端服务是否处于流量限制中。如果是,则全部限制访问,否则,转发请求到后端服务。
2:容错机制,如果Redis宕机等异常,限流模块失效,所有客户端请求放行。
3:是否开启限流,及限流类型(AF:全部请求限制访问,PF:设置阈值,每秒限制请求多少次)可热加载。
二:设计思路
1:在Reids中设置服务键值标识,Y标识限速,其它表示不限速。
2:在Nginx中获取请求体,判断服务标识是否流量控制中,如果是,即拦截请求返回403,否则通过。
3:如果在连接Redis和从Redis中读取数据时发生异常,则跳出限流模块,转发请求到服务端。
三:Nginx配置及服务限流开关代码
http {
include mime.types;
log_format main '$remote_addr - [$time_local] $request $status $request_body';
access_log /home/lws/soft/openResty/nginx/logs/access.log main;
upstream interServer{
# ip_hash; # 负载均衡算法,默认为轮询
server 127.0.0.1:55555 max_fails=2 fail_timeout=2;
server 127.0.0.1:55556 max_fails=2 fail_timeout=2;
server 127.0.0.1:55557 max_fails=2 fail_timeout=2;
}
lua_package_path '/home/lws/soft/openResty/nginx/conf/lua/?.lua;;';
lua_shared_dict ip_white_list 10m; # 分配一块10m共享内存空间,缓存ip白名单
lua_shared_dict limit_info 12m; # 分配一块12m共享内存空间,缓存限流开关信息
server {
listen 8000;
lua_need_request_body on; # nginx默认不读取请求体,需要配置开启
# 解决跨域问题
# add_header Access-Control-Allow-Origin * always;
# add_header Access-Control-Allow-Headers Origin,X-Requested-Width,Content-Type,Accept;
location /rest/eai {
# access_by_lua_file /home/lws/soft/openResty/nginx/conf/lua/ip_white_list.lua;
access_by_lua_file /home/lws/soft/openResty/nginx/conf/lua/limit_rate.lua;
proxy_pass http://interServer/rest/service; # proxy_pass用来将请求反向代理到url参数指定的服务器
proxy_redirect off;
# 修改从被代理服务器传来的应答头location和refresh字段
# proxy_redirect [ default|off|redirect replacement ]
# 默认值: proxy_redirect default
# 使用字段:http, server, location
# demo: proxy_redirect https://github.com 修改后的应答头;
# 默认情况,反向代理不会转发原始请求中的 Host 头部,如果需要,使用proxy_set_header设置
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# All forbidden
# Partial forbidden
# curl 'http://127.0.0.1:8000/set_limit_info?type=AF&value=Y'
location /set_limit_info {
content_by_lua '
local limit_info = ngx.shared.limit_info
local limit_type = ngx.var.arg_type
local limit_value = ngx.var.arg_value
if limit_type == "AF" then
limit_info:set("limit_AF_flag",limit_value)
ngx.say(limit_type .. ":" .. limit_info:get("limit_AF_flag"))
else
ngx.say("Your input incorrect")
end
';
}
}
四:限流源码(AF:全部限制)
local mylib = require "do_mylib"
local cjson = require "cjson"
ngx.req.read_body()
local reqstr = ngx.req.get_body_data()
------- 请求报文可能为nil多考虑异常情况 让后将报文设置为NULL -------
if not reqstr then
reqstr = ''
end
ngx.ctx.SRV_NAME = mylib.trim(mylib.get_xml_value(reqstr,'ActivityCode'))
-- 在此进行针对单个服务完全访问限制 调用本机 redis 的 6379 端口 进行控制
-- 需要在nginx.conf新增一个location,用于设置共享内存的标示,判断限速类型
-- 在redis中使用set设置键值,如 set 'T3000002' 'Y' 表示:T3000002的服务需要限速 Y表示限速,其它不限速
--[=[
# All forbidden
# Partial forbidden
# curl 'http://127.0.0.1:8000/set_limit_info?type=AF&value=Y'
location /set_limit_info {
content_by_lua '
local limit_info = ngx.shared.limit_info
local limit_type = ngx.var.arg_type
local limit_value = ngx.var.arg_value
if limit_type == "AF" then
limit_info:set("limit_AF_flag",limit_value)
ngx.say(limit_type .. ":" .. limit_info:get("limit_AF_flag"))
else
ngx.say("Your input incorrect")
end
';
}
]=]
local limit_info = ngx.shared.limit_info
local rip = '127.0.0.1'
local rport = 6379
if limit_info:get('limit_AF_flag') == 'Y' then
local key = string.format('%s',ngx.ctx.SRV_NAME)
local retcode,retmsg = mylib.get_redis_info(rip,rport,key)
if retcode == 0 then
if retmsg == 'Y' then
ngx.exit(ngx.HTTP_FORBIDDEN)
end
end
end
五:源码(自定义公共包)
--------- 定义库的函数 --------
module("do_mylib", package.seeall)
function get_xml_value(strxml,nodename)
if strxml == '' or nodename == '' then
return ''
end
local s1 = string.format('<%s>',nodename)
local s2 = string.format('%s>',nodename)
local f1s,f1e = string.find(strxml,s1)
if not f1s or not f1e then
return ''
end
local f2s = string.find(strxml,s2,f1e)
if f2s == nil then
return ''
end
return string.sub(strxml,f1e+1,f2s-1)
end
--- 字符串分割函数 ---
function split(inputstr, sep)
local t={}
local i=1
for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
t[i] = str
i = i + 1
end
return t
end
function trim(s)
local ss = s or ''
return ss:match'^%s*(.*%S)' or ''
end
function get_redis_info(ip,port,key)
local redis = require "resty.redis"
local red = redis:new()
local errmsg = ''
red:set_timeout(1000) -- 1 sec
local ok, err = red:connect(ip, port)
if not ok then
errmsg = string.format('MyRedisError:connect %s:%s:%s',ip,port,err)
ngx.log(ngx.ERR, errmsg)
return -1,errmsg
end
--添加redis的auth认证,如果无auth,则不需要
local count
count, err = red:get_reused_times()
if 0 == count then
ok, err = red:auth('liws')
if not ok then
errmsg = string.format("failed to auth %s",err)
return -1, errmsg
end
elseif err then
errmsg = string.format("failed to get reused times %s",err)
return -1, errmsg
end
local res, err = red:get(key)
if not res then
errmsg = string.format('MyRedisError:get key:%s fail:%s:%s:%s',key,ip,port,err)
ngx.log(ngx.ERR, errmsg)
return -1,errmsg
end
-- put it into the connection pool of size 100,
-- with 10 seconds max idle time
local ok, err = red:set_keepalive(100000000, 128)
if not ok then
errmsg = string.format('MyRedisError:set_keepalive %s:%s:%s',ip,port,err)
ngx.log(ngx.ERR, errmsg)
end
-- ngx.null lua_resty_redis中查询为空返回ngx.null
if res == ngx.null then
errmsg = string.format('MyRedisError:key:%s not found %s:%s',key,ip,port)
return -1, errmsg
end
return 0,res
end