淺談限流(下)實戰
常見的應用限流手段
應用開發中常見的限流的都有哪些呢?其實常用的限流手段都比較簡單,關鍵都是限流服務的高併發。為了在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等等。如果有這方面需求的話可以關注下。
淺談限流(