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