淺談限流(下)實戰

  • 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等等。如果有這方面需求的話可以關注下。

淺談限流(上)