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