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来保证了时间的连续性;
本文主要记录相关的问题及知识点,如简述和实现方式有问题欢迎吐槽