淺談限流(下)實戰

常見的應用限流手段

應用開發中常見的限流的都有哪些呢?其實常用的限流手段都比較簡單,關鍵都是限流服務的高併發。爲了在LB上實現高效且有效的限流,普遍的做法都是Nginx+Lua或者Nginx+Redis去實現服務服務限流,所以市面上比較常用的waf框架都是基於Openresty去實現的。我們看下比較常用的幾個限流方式。

Openresty+共享內存實現的計數限流

先看下代碼限流代碼

lua_shared_dict limit_counter 10m;
server {
        listen       80;
        server_name  www.test.com;
        location / {
            root   html;
            index  index.html index.htm;
        }

        location /test {
access_by_lua_block {
		local function countLimit()
		    local limit_counter =ngx.shared.limit_counter
		    local key = ngx.var.remote_addr .. ngx.var.http_user_agent .. ngx.var.uri .. ngx.var.host
		    local md5Key = ngx.md5(key)
		    local limit = 10
		    local exp = 300
		    local current =limit_counter:get(key)
		    if current ~= nil and current + 1> limit then
		        return 1
		    end
		    if current == nil then
		        limit_counter:add(key, 1, exp)
		    else
		        limit_counter:incr(key, 1)
		    end
		    return 0
		end
		
		local ret = countLimit()
		if ret > 0 then
		    ngx.exit(405)
		end
}
content_by_lua 'ngx.say(111)';
        }
    }

解釋下上面這段簡單的代碼,對於相同的IP UA HOST URI組合的唯一KEY,就是同一個URI每個用戶在5分鐘內只允許有10次請求,如果超過10次請求,就返回405的狀態碼,如果小於10次,就繼續執行後面的處理階段。
看下訪問結果

curlhttp://www.test.com/test
111
 curl http://www.test.com/test
111
 curl http://www.test.com/test
111
 curl http://www.test.com/test
111
 curl http://www.test.com/test
111
 curl http://www.test.com/test
111
 curl http://www.test.com/test
111
 curl http://www.test.com/test
111
 curl http://www.test.com/test
111
curl http://www.test.com/test
<html>
<head><title>405 Not Allowed</title></head>
<body bgcolor="white">
<center><h1>405 Not Allowed</h1></center>
<hr><center>openresty/1.13.6.2</center>
</body>
</html>

這就是一個簡單的計數限流的例子

Openresty 限制連接數和請求數的模塊

限制連接數和請求數的模塊是 lua-resty-limit-traffic。它的限速實現基於以前說過的漏桶原理。
蓄水池一邊注水一邊放水的問題。 這裏注水的速度是新增請求/連接的速度,而放水的速度則是配置的限制速度。 當注水速度快於放水速度(表現爲池中出現蓄水),則返回一個數值 delay。調用者通過 ngx.sleep(delay) 來減慢注水的速度。 當蓄水池滿時(表現爲當前請求/連接數超過設置的 burst 值),則返回錯誤信息 rejected。調用者需要丟掉溢出來的這部份。
看下配置代碼

http {
    lua_shared_dict my_req_store 100m;
    lua_shared_dict my_conn_store 100m;

    server {
        location / {
            access_by_lua_block {
                local limit_conn = require "resty.limit.conn"
                local limit_req = require "resty.limit.req"
                local limit_traffic = require "resty.limit.traffic"
                
                local lim1, err = limit_req.new("my_req_store", 300, 150)
                --300r/s的頻率,大於300小於450就延遲大概0.5秒,超過450的請求就返回503錯誤碼
                local lim2, err = limit_req.new("my_req_store", 200, 100)
                local lim3, err = limit_conn.new("my_conn_store", 1000, 1000, 0.5)
				--1000c/s的頻率,大於1000小於2000就延遲大概1s,超過2000的連接就返回503的錯誤碼,估算每個連接的時間大概是0.5秒,
                local limiters = {lim1, lim2, lim3}

                local host = ngx.var.host
                local client = ngx.var.binary_remote_addr
                local keys = {host, client, client}

                local states = {}
                local delay, err = limit_traffic.combine(limiters, keys, states)
                if not delay then
                    if err == "rejected" then
                        return ngx.exit(503)
                    end
                    ngx.log(ngx.ERR, "failed to limit traffic: ", err)
                    return ngx.exit(500)
                end

                if lim3:is_committed() then
                    local ctx = ngx.ctx
                    ctx.limit_conn = lim3
                    ctx.limit_conn_key = keys[3]
                end

                print("sleeping ", delay, " sec, states: ",
                      table.concat(states, ", "))

                if delay >= 0.001 then
                    ngx.sleep(delay)
                end
            }
            log_by_lua_block {
                local ctx = ngx.ctx
                local lim = ctx.limit_conn
                if lim then
                    local latency = tonumber(ngx.var.request_time)
                    local key = ctx.limit_conn_key
                    local conn, err = lim:leaving(key, latency)
                    if not conn then
                        ngx.log(ngx.ERR,
                                "failed to record the connection leaving ",
                                "request: ", err)
                        return
                    end
                end
            }
        }
    }
}

簡單的註釋可以介紹它大概的參數說明了。具體的可以參看下官方文檔
https://github.com/openresty/lua-resty-limit-traffic
注意下,連接數限流在log階段有個leaving()的調用來動態調整請求時間。不要忘記leaving的調用
用了這麼長時間了沒感覺有啥需要注意的坑。就是測試的時候,要測出效果,需要ngx.sleep下,否則,簡單的程序,沒任何壓力,Nginx都能執行完,不會有延遲。所以需要測試延遲的時候 content階段做下sleep,就能測到效果了。

Openresty 共享內存 動態限流

我們的使用的過程中發現,攻擊或者流量打過來的時候我通常的流程都是:先通過日誌服務發現有流量,然後在查詢攻擊的IP 或者UID,最後再封禁這些IP或者UID。一直是滯後的。我們應該做的是,在流量進來的時候通過動態分析直接攔截,而不是滯後攔截,滯後攔截有可能服務都被流量打死了。
動態限流是基於前面的技術限流的。

lua_shared_dict limit_counter 10m;
server {
        listen       80;
        server_name  www.test.com;



        location / {
            root   html;
            index  index.html index.htm;
        }

        location /test {
access_by_lua_block {
	local function countLimit()
	    local limit_counter =ngx.shared.limit_counter
	    local key = ngx.var.remote_addr .. ngx.var.http_user_agent .. ngx.var.uri .. ngx.var.host
	    local md5Key = ngx.md5(key)
	    local limit = 5
	    local exp = 120
	    local disable = 7200
	    local disableKey = md5Key .. ":disable"
	    local disableRt = limit_counter:get(disableKey)
	    if disableRt then
	        return 1
	    end
	    local current =limit_counter:get(key)
	    if current ~= nil and current + 1> limit then
	        dict:set(disableKey, 1, disable)
	        return 1
	    end
	    if current == nil then
	        limit_counter:add(key, 1, exp)
	    else
	        limit_counter:incr(key, 1)
	    end
	    return 0
	end
	
	local ret = countLimit()
	if ret > 0 then
	    ngx.exit(405)
	end
}
content_by_lua 'ngx.say(111)';
        }
    }

看下這行結果

curl http://www.test.com/test
111
 curl http://www.test.com/test
111
 curl http://www.test.com/test
111
 curl http://www.test.com/test
111
 curl http://www.test.com/test
111
 curl http://www.test.com/test
<html>
<head><title>500 Internal Server Error</title></head>
<body bgcolor="white">
<center><h1>500 Internal Server Error</h1></center>
<hr><center>openresty/1.13.6.2</center>
</body>
</html>

大致的思路比較簡單,一旦發現請求觸發閥值(2分鐘5次),直接將請求的唯一值放到黑名單2個小時,以後的請求一旦發現在黑名單裏面,就直接返回503。如果沒有觸發閥值,那就給請求的唯一值加1,這個計數器的過期時間是2分鐘,過了兩分鐘就會重新計數。基本滿足了我們目前當前的動態限流。

最後

我目前工作中比較常見的限流方式就上面三種,第二種是oenresty官方的模塊,已經能夠滿足絕大多數限流需求,達到保護服務的目的。簡單的限流控制利用openresty+shared.DICT很容易實現,把shared.DICT換成Redis就可以實現分佈式限流。當然了,市場上已經有了很多特別優秀的開源的網關服務框架包含了waf的功能,使用比較多的比如kong、orange,已經有很多巨頭公司在使用了,最近比較熱門的apisix等等。如果有這方面需求的話可以關注下。

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