浅谈限流(下)实战

常见的应用限流手段

应用开发中常见的限流的都有哪些呢?其实常用的限流手段都比较简单,关键都是限流服务的高并发。为了在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等等。如果有这方面需求的话可以关注下。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值