openresty IP限流

1、针对大流量大并发网络请求下,为了保证服务的正常运行,不得不针对性采取限流的方式来解决大流量带来的服务器的压力。

2、在目前项目中对于接入了不同的平台,所以需要针对具体的平台做相对应的限流,或者针对所有的平台做ip白名单的限制,针对ip限流。

3、以下代码是通过平台上报的ip对平台做相对应的限流,主要使用的是redis+openresty来做处理;涉及代码只做过基本的压测,未投入实际生产

相关代码记录如下:

  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     -- 2、获取当前ip是那个平台
 71     local platfromName = getPlatformNameByIp(ip)
 72     if platfromName == nil then
 73         return false
 74     end
 75     -- 3、获取当前平台的总数
 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     -- 2、获取当前ip是那个平台
 4     local platfromName = getPlatformNameByIp(ip)
 5     if platfromName == nil then
 6         return false
 7     end
 8     -- 3、获取当前平台的总数
 9     local key = 'forbid_' .. platfromName
10     local handle, redis = redisConn()
11     if redis == nil then
12         return nil
13     end
14     -- 4、校验是否超过限制
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层做了限制,主要引入方式为上方红色字体

 

起初想的是通过redis的incr来实现针对ip做限流,但是其中会有键失效的时间问题;如果使用incr做相对应的操作,如果10秒钟请求量为50的话,是没法保证时间的连续性;所以最后采用了通过list来保证了时间的连续性;

 

本文主要记录相关的问题及知识点,如简述和实现方式有问题欢迎吐槽

posted on 2019-03-07 09:48  铁猛  阅读(1115)  评论(0编辑  收藏  举报

导航