1. 程式人生 > >淺談限流(下)實戰

淺談限流(下)實戰

常見的應用限流手段

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

淺談限流(