服務網關---基於Nginx+lua+Redis的服務降級設計(一)

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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章