openresty IP限流

一、針對大流量大併發網絡請求下,爲了保證服務的正常運行,不得不針對性採起限流的方式來解決大流量帶來的服務器的壓力。html

二、在目前項目中對於接入了不一樣的平臺,因此須要針對具體的平臺作相對應的限流,或者針對全部的平臺作ip白名單的限制,針對ip限流。nginx

三、如下代碼是經過平臺上報的ip對平臺作相對應的限流,主要使用的是redis+openresty來作處理;涉及代碼只作過基本的壓測,未投入實際生產web

相關代碼記錄以下:redis

  1 --
  2 -- Created by IntelliJ IDEA.
  3 -- User: tiemeng
  4 -- Date: 2019/3/3
  5 -- Time: 10:00
  6 -- To change this template use File | Settings | File Templates.
  7 --
  8 ngx.header.content_type = "text/html;charset=utf8"
  9 
 10 
 11 -- redis配置
 12 local redisConfig = {
 13     redis_a = {
 14         host = '127.0.0.1',
 15         port = 6379,
 16         pass = '',
 17         timeout = 200,
 18         database = 0,
 19     }
 20 }
 21 
 22 local limitCount = 5
 23 
 24 local time = 10000 -- 時間,單位爲毫秒
 25 
 26 --[[
 27       獲取請求IP
 28  ]]
 29 local function getIp()
 30     local headers = ngx.req.get_headers()
 31     local ip = headers["X-REAL-IP"] or headers["X_FORWARDED_FOR"] or ngx.var.remote_addr or "0.0.0.0"
 32     return ip
 33 end
 34 
 35 
 36 
 37 --[[
 38         鏈接redis
 39  ]]
 40 local function redisConn()
 41     local redis = require('resty.redis_factory')(redisConfig)
 42     local ok, redis_a = redis:spawn('redis_a')
 43     if ok ~= nil then
 44         return redis, redis_a
 45     end
 46 
 47     return redis, nil
 48 end
 49 
 50 
 51 --[[
 52     經過ip獲取平臺名稱
 53  ]]
 54 local function getPlatformNameByIp(ip)
 55     local handle, redis = redisConn()
 56     if redis == nil then
 57         return nil
 58     end
 59     local platform = redis:hget('iplist', ip)
 60     handle.destruct()
 61     if platform ~= ngx.null then
 62         return platform
 63     end
 64     ngx.log(ngx.ERR, "ip:" .. ip .. ",未在白名單中,禁止訪問")
 65     return nil
 66 end
 67 
 68 local function forbid2()
 69     local ip = getIp();
 70     -- 二、獲取當前ip是那個平臺
 71     local platfromName = getPlatformNameByIp(ip)
 72     if platfromName == nil then
 73         return false
 74     end
 75     -- 三、獲取當前平臺的總數
 76     local key = 'forbid_' .. platfromName
 77     local handle, redis = redisConn()
 78     if redis == nil then
 79         return nil
 80     end
 81     local curTime = ngx.now() * 1000
 82     local ok, err = redis:eval([[
 83         local len = redis.call('llen',KEYS[1])
 84         if len < 10 then
 85             redis.call('rpush',KEYS[1],ARGV[2])
 86             return true
 87         end
 88         local times = redis.call('lrange',KEYS[1],0,0)
 89         local timeSum = tonumber(times[1])+tonumber(ARGV[1])
 90         if timeSum > tonumber(ARGV[2]) then
 91             return false
 92         end
 93         redis.call('lpop',KEYS[1])
 94         redis.call('rpush',KEYS[1],ARGV[2])
 95         return true
 96     ]], 1, key, time, curTime)
 97     handle.destruct()
 98     return ok
 99 end
100 
101 
102 
103 if forbid2() ~= 1 then
104     ngx.exit(403)
105 end

測試中出現的問題:服務器

起初是使用如下代碼實現的,從代碼表面看是沒有任何問題,可是在壓力測試下併發數達到50的時候就會出現限流失效;出現失效的主要緣由是,在redis中list的操做並非所謂的原子操做,因此經過翻閱相關資料瞭解到,能夠在redis中嵌入相關的lua腳本,能夠達到原子的操做;因此在一開始的代碼82-96行使用redis的eval函數來調用lua的腳本,已達到原子操做的要求;修改後通過壓測後達到相對用的效果網絡

 1 local function isForbid()
 2     local ip = getIp();
 3     -- 二、獲取當前ip是那個平臺
 4     local platfromName = getPlatformNameByIp(ip)
 5     if platfromName == nil then
 6         return false
 7     end
 8     -- 三、獲取當前平臺的總數
 9     local key = 'forbid_' .. platfromName
10     local handle, redis = redisConn()
11     if redis == nil then
12         return nil
13     end
14     -- 四、校驗是否超過限制
15     local len = redis:llen(key)
16 
17     if len < limitCount then
18         redis:rpush(key, ngx.now() * 1000)
19         handle.destruct()
20         return true
21     end
22     local times = redis:lrange(key, 0, 0)
23     if times == ngx.null then
24         return false
25     end
26 
27     if tonumber(times[1]) + time >= ngx.now() * 1000 then
28         ngx.log(ngx.ERR, "forbid_platform :" .. platfromName)
29         return false
30     end
31     os.execute("sleep " .. 1)
32     redis:lpop(key)
33     redis:rpush(key, ngx.now() * 1000)
34     handle.destruct()
35     return true
36 end

 

nginx部分相關配置:併發

http {
    include       mime.types;
    default_type  application/octet-stream;
    lua_package_path '/websys/nginx/lua/?.lua;/websys/lualib/?/init.lua;;';

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    #keepalive_timeout  65;
    #    resolver 127.0.0.1 192.168.1.1 8.8.8.8;
    #gzip  on;
    access_by_lua_file lua/access.lua;

此限流主要在openresty的access層作了限制,主要引入方式爲上方紅色字體app

 

起初想的是經過redis的incr來實現針對ip作限流,可是其中會有鍵失效的時間問題;若是使用incr作相對應的操做,若是10秒鐘請求量爲50的話,是無法保證時間的連續性;因此最後採用了經過list來保證了時間的連續性;tcp

 

本文主要記錄相關的問題及知識點,如簡述和實現方式有問題歡迎吐槽函數

相關文章
相關標籤/搜索