一:服務限流功能點
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