介绍
在前一篇文章中我们介绍了应用级限流方式,但是不能进行全局限流。本文我们就来介绍下可以进行全局限流的分布式限流和接入层限流。
接入层限流
接入层限流通常指请求流量的入口,主要目的有:负载均衡、非法请求过滤、请求聚合、缓存、限流、A/B测试、服务质量监控等。通常使用Nginx(OpenResty)接入层限流。
可以使用nginx自带的两个模块:连接数限流模块ngx_http_limit_conn_module和漏桶算法实现的请求限流模块ngx_http_limit_req_module.
ngx_http_limit_conn_module
limit_conn是对某个key对应的总的网络连接数进行限流,可以按照IP或者服务域名来限制总连接数。不是每个请求连接都会被计数器统计,只有被nginx处理的且已经读取整个请求头的请求连接会被计数器统计。配置示例如下:
http {
# 配置限流key、存放key对应信息的共享内存区域大小。此处key为$binary_remote_addr表示ip地址,也可以使用$server_name作为key
limit_conn_zone $binary_remote_addr zone=addr:10m;
# 配置记录被限流后的日志级别,默认error级别
limit_conn_log_level error;
# 配置被限流后返回的状态码,默认返回503
limit_conn_status 503;
...
server {
location /limit {
# 要配置存放key和计数器的共享内存区域和指定key的最大连接数
# 此处指定的最大连接数为1(nginx最多同时并发处理1个连接)
limit_conn addr 1;
}
...
}
复制代码
主要执行流程如下:
limit_conn可以限流某个key的总并发数/请求数,key可以根据需要变化。
ngx_http_limit_req_module
limit_req是漏桶算法实现,用于指定key对应的请求进行限流。
我们来看下配置示例:
http {
# 配置限流key、存放key对应信息的共享内存区域大小,固定请求速率。此处key为$binary_remote_addr表示ip地.固定请求速率使用rate参数配置,支持10r/s和60r/m
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
# 配置记录被限流后的日志级别,默认error级别
limit_conn_log_level error;
# 配置被限流后返回的状态码,默认返回503
limit_conn_status 503;
...
server {
location /limit {
# 配置限流区域、桶容量(突发容量,默认为0)、是否延迟模式(默认延迟)
limit_req zone=one burst=5 nodelay;
}
...
}
复制代码
执行过程如下所示:
1.请求进入首先判断最后一次请求时间相对于当前时间(第一次是0)是否需要限流,如果需要限流,则执行步骤2,否则执行步骤3
2.如果没有配置桶容量(burst=0),按照固定速率处理请求。如果请求被限流,则直接返回相应的错误码(503)。
如果配置了桶容量(burst>0)及延迟模式(没有配置nodelay)。如果桶满了,则新进入的请求被限流。如果没有满,则以固定平均速率处理(需要延迟处理请求,延迟使用休眠实现)
如果配置了桶容量(burst>0)及非延迟模式(配置了nodelay),则不会按照固定速率处理请求,允许突发处理请求。如果桶满了,则请求被限流,直接返回状态503
3.如果没有被限流,正常处理请求
4.Nginx会在相应时机选择一些(3个节点)限流key进行过期处理,进行内存回收
OpenResty中的lua-resty-limit-traffic
上面介绍的两种使用比较简单,指定key、指定限流速率等就可以了。如果根据实际情况变化key、速率、桶大小等这种动态特性那么使用标准模块就很难实现。因此需要一种可编程方式解决此问题。OpenResty提供了Lua限流模块lua-resty-limit-traffic,可以按照更复杂的业务逻辑进行动态限流处理。提供了limit.conn和limit.req实现,算法和nginx limit_conn和limit_req是一样的。
OpenResty 1.11.2.2+默认已经支持该限流库。低版本的话在使用之前需要下载lua-resty-limit-traffic模块并添加到OpenResty的lualib中。我们当前使用的OpenResty版本为1.19.9.1,所以可以直接使用。
我们来看一个limit.req的使用示例:
limit_traffic.lua
local limit_req = require "resty.limit.req"
local rate = 2 -- 固定平均速率 2r/s
local burst = 3 -- 桶容量
local error_status = 503
local nodelay = false -- 是否需要不延迟处理
local lim, err = limit_req.new("limit_req_store", rate, burst)
if not lim then -- 没定义共享字典
ngx.log(ngx.ERR, "没定义共享字典", delay)
ngx.exit(error_status)
end
local key = ngx.var.binary_remote_addr -- IP维度限流,如果想根据参数可以查询所需参数进行限流
-- 流入请求,如果请求需要被延迟,则delay > 0
local delay, err = lim:incoming(key, true)
if not delay and err == "rejected" then -- 超出桶大小了
ngx.log(ngx.ERR, "超出桶大小:", err)
ngx.exit(error_status)
end
if delay > 0 then -- 根据需要决定是延迟还是不延迟处理
if nodelay then
-- 直接突发处理
ngx.log(ngx.ERR, "突发处理,正常需要延迟时间", delay)
else
ngx.sleep(delay) -- 延迟处理
ngx.log(ngx.ERR, "延迟处理,延迟时间:", delay)
end
end
复制代码
上面我们配置了固定平均速率 2r/s,桶容量设置为3. 其中nginx.conf我们简单配置下:
worker_processes 1;
error_log logs/error.log;
events {
worker_connections 1024;
}
http {
lua_package_path "$prefix/lua/?.lua;$prefix/libs/?.lua;;";
lua_shared_dict limit_req_store 10m;
server {
server_name localhost;
listen 8080;
charset utf-8;
set $LESSON_ROOT lua/;
error_log logs/error.log;
access_log logs/access.log;
location /limit {
default_type text/html;
content_by_lua_file $LESSON_ROOT/limit_traffic.lua;
}
}
}
复制代码
启动openresty后,我们使用ab命令来并发请求下。
wukongdeMacBook-Pro:limit wukong$ ab -c 3 -n 3 http://127.0.0.1:8080/limit
复制代码
我们会发现有一个请求正常处理,另外两个进行了延迟处理。延迟处理的等待时间分别为500ms和1s,和我们设置的2r/s和桶容量3个是可以对应上的。如果想要突发处理的话可以在limit_traffic.lua中是否允许突发的变量nodelay设置为true.
2022/04/09 17:09:09 [error] 67100#12461017: *1008 [lua] limit_traffic.lua:24: 延迟处理,延迟时间:0.499, client: 127.0.0.1, server: localhost, request: "GET /limit HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 17:09:09 [error] 67100#12461017: *1009 [lua] limit_traffic.lua:24: 延迟处理,延迟时间:0.999, client: 127.0.0.1, server: localhost, request: "GET /limit HTTP/1.0", host: "127.0.0.1:8080"
复制代码
如果同一时间并发请求远超过3个那后面的请求就会被拒绝,我们来简单测试看下,我们就以同时10个并发为例:
wukongdeMacBook-Pro:limit wukong$ ab -c 10 -n 10 http://127.0.0.1:8080/limit
复制代码
我们再来看下日志:
2022/04/09 17:16:53 [error] 67100#12461017: *1014 [lua] limit_traffic.lua:15: 超出桶大小rejected, client: 127.0.0.1, server: localhost, request: "GET /limit HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 17:16:53 [error] 67100#12461017: *1015 [lua] limit_traffic.lua:15: 超出桶大小rejected, client: 127.0.0.1, server: localhost, request: "GET /limit HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 17:16:53 [error] 67100#12461017: *1016 [lua] limit_traffic.lua:15: 超出桶大小rejected, client: 127.0.0.1, server: localhost, request: "GET /limit HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 17:16:53 [error] 67100#12461017: *1017 [lua] limit_traffic.lua:15: 超出桶大小rejected, client: 127.0.0.1, server: localhost, request: "GET /limit HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 17:16:53 [error] 67100#12461017: *1018 [lua] limit_traffic.lua:15: 超出桶大小rejected, client: 127.0.0.1, server: localhost, request: "GET /limit HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 17:16:53 [error] 67100#12461017: *1019 [lua] limit_traffic.lua:15: 超出桶大小rejected, client: 127.0.0.1, server: localhost, request: "GET /limit HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 17:16:53 [error] 67100#12461017: *1011 [lua] limit_traffic.lua:24: 延迟处理,延迟时间:0.499, client: 127.0.0.1, server: localhost, request: "GET /limit HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 17:16:54 [error] 67100#12461017: *1012 [lua] limit_traffic.lua:24: 延迟处理,延迟时间:0.999, client: 127.0.0.1, server: localhost, request: "GET /limit HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 17:16:54 [error] 67100#12461017: *1013 [lua] limit_traffic.lua:24: 延迟处理,延迟时间:1.498, client: 127.0.0.1, server: localhost, request: "GET /limit HTTP/1.0", host: "127.0.0.1:8080"
复制代码
我们会发现有6个请求被拒绝,1个请求已正常处理,3个请求延迟处理。
上面根据Nginx+lua实现简单的limit.req限流,示例代码已放到github,实际使用根据自身需要灵活运用。
另外Nginx也提供了limit_rate对流量限速,例如limit_rate 50K,表示限制下载速度为50K。
分布式限流
分布式限流最关键的是将限流服务做成原子化,解决方案:Redis+Lua或者Nginx+Lua,通过以上方案可实现高并发和高性能。
下面我们来看一个使用Redis+Lua实现时间窗口内某个接口的请求数限流,后期可改造为限流总并发/请求总资源数。Lua本身就是一种变成语言,可实现复杂令牌桶或漏桶算法。
Redis+Lua实现
@Component
@Slf4j
public class RedisLimit {
@Autowired
private JedisPool jedisPool;
/**
* 限流lua
*/
private static final String LIMIT_LUA;
static {
LIMIT_LUA = new StringBuilder()
//限流key
.append("local key = KEYS[1] ")
//限流大小
.append("local limit = tonumber(ARGV[1]) ")
.append("local current = tonumber(redis.call('get', key) or '0') ")
//如果超出限流大小
.append("if current + 1 > limit then ")
.append(" return 0 ")
//请求数+1并设置2s过期
.append("else ")
.append("redis.call('INCRBY', key, '1') ")
.append("redis.call('expire', key, '2') ")
.append(" return 1")
.append(" end").toString();
}
/**
* 是否需要限流
* @return
*/
public boolean acquire(){
Jedis jedis = jedisPool.getResource();
try {
//ip
String key = IpUtils.getIp();
//限流大小(分布式配置)
String limit = "1000";
return (Long) jedis.eval(LIMIT_LUA, Lists.newArrayList(key), Lists.newArrayList(limit)) == 1;
} catch (Exception e) {
log.error("是否需要限流异常", e);
} finally {
if(null != jedis){
jedis.close();
}
}
return false;
}
}
复制代码
调用acquire来知道是否需要限流。上面我们限流大小设置的为1000,可以使用分布式配置进行配置,可实时修改。
Nginx+Lua
limit_lock.lua
local locks = require "resty.lock"
local function acquire()
local lock = locks:new("locks")
local elapsed, err = lock:lock("limit_key") --互斥锁
local limit_counter = ngx.shared.limit_counter -- 计数器
local key = "ip" ..os.time()
local limit = 5 -- 限流大小
local current = limit_counter:get(key)
if current ~= nil and current + 1 > limit then -- 如果超出限流大小
lock:unlock()
return 0
end
if current == nil then
limit_counter:set(key, 1, 1) -- 第一次需要设置过期时间,设置key的值为1,过期时间为1s
else
limit_counter:incr(key, 1) -- 第二次开始加1即可
end
lock:unlock()
return 1
end
ngx.log(ngx.ERR, "限流标识(1:正常 0:限流):", acquire())
复制代码
我们用ab命令简单测试下:
wukongdeMacBook-Pro:limit wukong$ ab -c 10 -n 10 http://127.0.0.1:8080/limit/lock
复制代码
输出日志如下:
2022/04/09 21:56:13 [error] 30012#12823604: *26 [lua] limit_lock.lua:22: 限流标识(1:正常 0:限流):1, client: 127.0.0.1, server: localhost, request: "GET /limit/lock HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 21:56:13 [error] 30012#12823604: *27 [lua] limit_lock.lua:22: 限流标识(1:正常 0:限流):1, client: 127.0.0.1, server: localhost, request: "GET /limit/lock HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 21:56:13 [error] 30012#12823604: *28 [lua] limit_lock.lua:22: 限流标识(1:正常 0:限流):1, client: 127.0.0.1, server: localhost, request: "GET /limit/lock HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 21:56:13 [error] 30012#12823604: *29 [lua] limit_lock.lua:22: 限流标识(1:正常 0:限流):1, client: 127.0.0.1, server: localhost, request: "GET /limit/lock HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 21:56:13 [error] 30012#12823604: *30 [lua] limit_lock.lua:22: 限流标识(1:正常 0:限流):1, client: 127.0.0.1, server: localhost, request: "GET /limit/lock HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 21:56:13 [error] 30012#12823604: *31 [lua] limit_lock.lua:22: 限流标识(1:正常 0:限流):0, client: 127.0.0.1, server: localhost, request: "GET /limit/lock HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 21:56:13 [error] 30012#12823604: *32 [lua] limit_lock.lua:22: 限流标识(1:正常 0:限流):0, client: 127.0.0.1, server: localhost, request: "GET /limit/lock HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 21:56:13 [error] 30012#12823604: *33 [lua] limit_lock.lua:22: 限流标识(1:正常 0:限流):0, client: 127.0.0.1, server: localhost, request: "GET /limit/lock HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 21:56:13 [error] 30012#12823604: *34 [lua] limit_lock.lua:22: 限流标识(1:正常 0:限流):0, client: 127.0.0.1, server: localhost, request: "GET /limit/lock HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 21:56:13 [error] 30012#12823604: *35 [lua] limit_lock.lua:22: 限流标识(1:正常 0:限流):0, client: 127.0.0.1, server: localhost, request: "GET /limit/lock HTTP/1.0", host: "127.0.0.1:8080"
复制代码
可以看到有五个正常执行,和预期一致。
在上面nginx+lua的代码实现中我们使用了lua-resty-lock互斥锁来解决原子性问题,在实际使用过程中我们需要考虑获取锁的超时问题。
聊到现在你可能会问如果并发量特别大,redis或者nginx是否能扛的住,可以从以下方面考虑:
1.综合考虑下流量是不是真的特别大,当前限流是否已经满足 2.通过一致性哈希将分布式限流进行分片 3.可以当并发量太大时降级为应用级限流 ...
总结
本文主要讨论了分布式限流和接入层限流,实际使用中结合自身需要进行选择。应用级限流可参考之前一篇文章应用级限流
参考书籍:《亿级流量网站架构核心技术》