接入层的主要目的有:负载均衡、非法请求过滤、请求聚合、缓存、降级、限流、A/B测试、服务质量监控等。

对于Nginx接入层限流可以使用Nginx自带的两个模块:连接数限流模块ngx_http_limit_conn_module和漏桶算法实现的请求限流模块ngx_http_limit_req_module。还可以使用OpenResty提供的Lua限流模块lua-resty-limit-traffic应对更复杂的限流场景。

limit_conn用来对某个Key对应的总的网络连接数进行限流,可以按照如IP、域名维度进行限流。limit_req用来对某个key对应的请求的平均速率进行限流,有两种用法:平滑模式(delay)和允许突发模式(nodelay)。

一、ngx_http_limit_conn_module

limit_conn对某个Key对应的总的网络连接数进行限流。可以按照IP/域名来限制IP/域名维度的总连接数。只有被Nginx处理的且已经读取了整个请求头的请求连接才会被计数器统计。

1.配置示例

http {
limit_conn_zone $binary_remote_addr zone=addr:10m;
limit_conn_log_level error;
limit_conn_status 503;
....
server {
...
location /limit {
limit_conn addr 1;
}
}
}
  • limit_conn:要配置存放Key和计数器的共享内存区域和指定Key的最大连接数。此处指定的最大连接数是1,表示Nginx最多同时并发处理1个连接。
  • limit_conn_zone:用来配置限流Key及存放Key对应信息的共享内存区域大小。此处Key是”$binary_remote_addr“,表示IP地址,也可以使用$server_name作为key,限制域名级别的最大连接数。
  • limit_conn_status:配置被限流后返回的状态码,默认返回503.
  • limit_conn_log_level:配置记录被限流后的日志级别,默认error级别。

limit_conn可以限制某个key的总并发数/请求数,key可以根据需求变化。

3.按照IP限制并发连接数配置示例

(1)定义IP维度的限流区域

limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_log_level error;
limit_conn_status 503;

(2)在要限流的location中添加限流逻辑

location /limit {
limit_conn perip 2;
echo "123";
}

允许每个IP最大并发连接数为2。

使用AB测试工具进行测试,并发数为5个,总的请求数为5个,如下所示。

ab -n 5 -c 5 http://192.168.175.100/limit

输出的access.log日志如下:

[\xC0\xA8\xAF\x01] [12/Sep/2019:10:53:35 +0800] [1568256815.840] 200
[\xC0\xA8\xAF\x01] [12/Sep/2019:10:53:35 +0800] [1568256815.840] 200
[\xC0\xA8\xAF\x01] [12/Sep/2019:10:53:35 +0800] [1568256815.841] 503
[\xC0\xA8\xAF\x01] [12/Sep/2019:10:53:35 +0800] [1568256815.842] 503
[\xC0\xA8\xAF\x01] [12/Sep/2019:10:53:35 +0800] [1568256815.842] 503

此处,我们把access log日志格式设置为log_format main '[$binary_remote_addr] [$time_local] [$msec] $status';分别表示IP、时间、时间/毫秒值和状态码。如下所示,

log_format main '[$binary_remote_addr] [$time_local] [$msec] $status';
access_log logs/access.log main;

4.按照域名限制并发连接数配置示例

(1)定义域名维度的限流区域

limit_conn_zone $server_name zone=perserver:10m;

(2)在要限流的location中添加限流逻辑

location /limit {
limit_conn perserver 2;
echo "123";
}

允许每个域名最大并发请求连接数为2。测试方法与“3.按照IP限制并发连接数配置示例”一样。

二、ngx_http_limit_req_module

limit_req是漏桶算法实现,用于对指定key对应的请求进行限流,比如:按照IP维度限制请求速率。配置示例如下。

http {
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
limit_conn_log_level error;
limit_conn_status 503;
...
server {
...
location /limit {
limit_req zone=one burst=5 nodelay;
}
}
}
  • limit_req:配置限流区域、桶容量(突发容量,默认为0)、是否延迟模式(默认延迟)。
  • limit_req_zone:配置限流key、存放key对应信息的共享内存区域大小、固定请求速率。此处指定的key是$binary_remote_addr,表示IP地址。固定请求速率使用rate参数配置,支持10r/s和60r/m,表示每秒10个请求和每分钟60个请求。不过最终都会转换为每秒的固定请求速率(10r/s为每100毫秒处理一个请求,60r/m为每1000毫秒处理一个请求)。
  • limit_conn_status:配置被限流后返回的状态码,默认返回503。
  • limit_conn_log_level:配置记录被限流后的日志级别,默认级别为error。

limit_req的主要执行流程如下:

(1)请求进入后首先判断最后一次请求时间相对于当前时间(第一次是0)是否需要限流,如果需要限流,则执行步骤2,否则执行步骤3。

(2)如果没有配置桶容量(burst),则桶容量为0,按照固定速率处理请求。如果请求被限流,则直接返回相应的错误码(默认503)。

如果配置了桶容量(burst > 0)及延迟模式(没有配置nodelay)。如果桶满了,则新进入的请求被限流。如果没有满,则请求会以固定平均速率被处理(按照固定速率并根据需要延迟处理请求,延迟使用休眠实现)。

如果配置了桶容量(burst > 0)及非延迟模式(配置了nodelay),则不会按照固定速率处理请求,而是允许突发处理请求。如果桶满了,则请求被限流,直接返回相应的错误码。

(3)如果没有被限流,则正常处理请求。

(4)Nginx会在相应时机选择一些(3个节点)限流key进行过期处理,进行内存回收。

1.场景一测试

(1)定义IP维度的限流区域

limit_req_zone $binary_remote_addr zone=test:10m rate=500r/s;

限制为每秒500个请求,固定平均速率为2毫秒一个请求。

(2)在要限流的location中添加限流逻辑。

location /limit {
limit_req zone=test;
echo "123";
}

即桶容量为0(burst默认为0)并且是延迟模式。

使用AB测试并发数为2个,总的请求数为10个。

ab -n 10 -c 2 http://192.168.175.100/limit

access.log的输出信息如下:

[\xC0\xA8\xAF\x01] [12/Sep/2019:11:33:18 +0800] [1568259198.110] 200
[\xC0\xA8\xAF\x01] [12/Sep/2019:11:33:18 +0800] [1568259198.111] 503
[\xC0\xA8\xAF\x01] [12/Sep/2019:11:33:18 +0800] [1568259198.257] 200
[\xC0\xA8\xAF\x01] [12/Sep/2019:11:33:18 +0800] [1568259198.257] 503
[\xC0\xA8\xAF\x01] [12/Sep/2019:11:33:18 +0800] [1568259198.258] 200
[\xC0\xA8\xAF\x01] [12/Sep/2019:11:33:18 +0800] [1568259198.259] 503
[\xC0\xA8\xAF\x01] [12/Sep/2019:11:33:18 +0800] [1568259198.261] 200
[\xC0\xA8\xAF\x01] [12/Sep/2019:11:33:18 +0800] [1568259198.261] 503
[\xC0\xA8\xAF\x01] [12/Sep/2019:11:33:18 +0800] [1568259198.262] 503
[\xC0\xA8\xAF\x01] [12/Sep/2019:11:33:18 +0800] [1568259198.263] 200

如果被限流,在error.log日志文件中,会输出如下信息。

2019/09/12 11:33:18 [error] 15630#0: *100 limiting requests, excess: 0.500 by zone "test", client: 192.168.175.1, server: 192.168.175.100, request: "GET /limit HTTP/1.0", host: "192.168.175.100"

2.场景二测试

(1)定义IP维度的限流区域

limit_req_zone $binary_remote_addr zone=test:10m rate=2r/s;

设置速率为每秒2个请求,即固定平均速率为500毫秒一个请求。

(2)在要限流的location中添加限流逻辑。

location /limit {
limit_req zone=test burst=3;
echo "123";
}

固定平均速率为500毫秒一个请求,桶容量为3,如果桶满了,则新的请求被限流,否则可以进入桶中排队等待(实现延迟模式)。

桶容量为3,即桶中在时间窗口内最多流入3个请求,并且按照2r/s的固定速率处理请求(即每隔500毫秒处理一个请求)。桶计算时间窗口(1.5秒) = 速率(2r/s)/ 桶容量(2),也就是说在这个时间窗口内桶最多暂存3个请求。因此,要以当前时间往前推1.5秒和1秒来计算时间窗口内的总请求数。另外,因为默认是延迟模式,所以时间窗内的请求被暂存到桶中,并以固定平均速率处理请求。

这里,定义一个req.sh脚本

ab -c 6 -n 6 http://192.168.175.100/limit
sleep 0.3
ab -c 6 -n 6 http://192.168.175.100/limit

运行req.sh脚本,access.log日志文件输出的信息如下:

[\xC0\xA8\xAFd] [12/Sep/2019:11:43:52 +0800] [1568259832.260] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:43:52 +0800] [1568259832.260] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:43:52 +0800] [1568259832.761] 200
[\xC0\xA8\xAFd] [12/Sep/2019:11:43:53 +0800] [1568259833.259] 200
[\xC0\xA8\xAFd] [12/Sep/2019:11:43:53 +0800] [1568259833.260] 499
[\xC0\xA8\xAFd] [12/Sep/2019:11:43:54 +0800] [1568259834.260] 200

[\xC0\xA8\xAFd] [12/Sep/2019:11:43:54 +0800] [1568259834.261] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:43:54 +0800] [1568259834.261] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:43:54 +0800] [1568259834.261] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:43:54 +0800] [1568259834.760] 200
[\xC0\xA8\xAFd] [12/Sep/2019:11:43:55 +0800] [1568259835.259] 200
[\xC0\xA8\xAFd] [12/Sep/2019:11:43:55 +0800] [1568259835.260] 503

3.场景三测试

(1)定义IP维度的限流区域

limit_req_zone $binary_remote_addr zone=test:10m rate=2r/s;

每秒2个请求,固定平均速率为500毫秒一个请求。

(2)在限流的lcoation中添加限流逻辑。

location /limit {
limit_req zone=test burst=3 nodelay;
echo "123";
}

桶容量为3,如果桶满了,则直接拒绝新请求,并且每2秒最多两个请求,桶按照固定500毫秒的速率以nodelay模式处理请求。

桶容量为3(即桶中在时间窗口内最多流入3个请求),并且按照2r/s的固定速率请求(每隔500毫秒处理一个请求)。桶计算时间窗口(1.5秒)= 速率(2r/s)/ 桶容量(3),也就是说在这个时间窗口内桶最多暂存3个请求。因此,我们要以当前时间往前推1.5秒和1秒来计算时间窗口内的总请求数。另外,因为配置了nodelay,是非延迟模式,所以允许时间窗内的突发请求。

这里,定义一个req.sh脚本

ab -c 6 -n 6 http://192.168.175.100/limit
sleep 1
ab -c 6 -n 6 http://192.168.175.100/limit
sleep 0.3
ab -c 6 -n 6 http://192.168.175.100/limit
sleep 0.3
ab -c 6 -n 6 http://192.168.175.100/limit
sleep 0.3
ab -c 6 -n 6 http://192.168.175.100/limit
sleep 2
ab -c 6 -n 6 http://192.168.175.100/limit

运行req.sh脚本,access.log日志文件输出的信息如下:

[\xC0\xA8\xAFd] [12/Sep/2019:11:56:16 +0800] [1568260576.419] 200
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:16 +0800] [1568260576.419] 200
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:16 +0800] [1568260576.419] 200
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:16 +0800] [1568260576.419] 200
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:16 +0800] [1568260576.419] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:16 +0800] [1568260576.420] 503

[\xC0\xA8\xAFd] [12/Sep/2019:11:56:16 +0800] [1568260576.420] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:17 +0800] [1568260577.427] 200
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:17 +0800] [1568260577.428] 200
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:17 +0800] [1568260577.428] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:17 +0800] [1568260577.428] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:17 +0800] [1568260577.428] 503

[\xC0\xA8\xAFd] [12/Sep/2019:11:56:17 +0800] [1568260577.428] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:17 +0800] [1568260577.428] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:17 +0800] [1568260577.736] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:17 +0800] [1568260577.737] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:17 +0800] [1568260577.737] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:17 +0800] [1568260577.737] 503

[\xC0\xA8\xAFd] [12/Sep/2019:11:56:17 +0800] [1568260577.737] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:17 +0800] [1568260577.737] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:17 +0800] [1568260577.737] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:18 +0800] [1568260578.044] 200
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:18 +0800] [1568260578.045] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:18 +0800] [1568260578.045] 503

[\xC0\xA8\xAFd] [12/Sep/2019:11:56:18 +0800] [1568260578.045] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:18 +0800] [1568260578.045] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:18 +0800] [1568260578.045] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:18 +0800] [1568260578.045] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:18 +0800] [1568260578.353] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:18 +0800] [1568260578.353] 503

[\xC0\xA8\xAFd] [12/Sep/2019:11:56:18 +0800] [1568260578.353] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:18 +0800] [1568260578.354] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:18 +0800] [1568260578.354] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:18 +0800] [1568260578.354] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:18 +0800] [1568260578.354] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:20 +0800] [1568260580.360] 200

[\xC0\xA8\xAFd] [12/Sep/2019:11:56:20 +0800] [1568260580.361] 200
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:20 +0800] [1568260580.361] 200
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:20 +0800] [1568260580.361] 200
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:20 +0800] [1568260580.361] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:20 +0800] [1568260580.361] 503
[\xC0\xA8\xAFd] [12/Sep/2019:11:56:20 +0800] [1568260580.361] 503

如果限流出错了,可以配置错误页面,如下。

proxy_intercept_errors on;
recursive_error_pages on;
error_page 503 //www.binghe.com/error.html

limit_conn_zone/limit_req_zone定义的内存不足,则后续的请求一直被限流,所以需要根据需求设置好相应的内存大小。

以上介绍的限流方式都是单Nginx的,如果接入层有多个Nginx,一种解决方案是可以建立一个负载均衡层,按照限流key进行一致性哈希算法,将请求哈希到接入层Nginx上,从而相同key的请求将打到同一台接入层Nginx上。另一种解决方案就是使用Nginx+Lua(OpenResty)调用分布式限流逻辑实现。

三、lua-resty-limit-traffic

之前的ngx_http_limit_conn_module和ngx_http_limit_req_module模块只要指定key和限流速率就可以使用了。如果想根据实际情况变化key、速率、桶大小等这种动态特性,就需要使用OpenResty提供的Lua限流模块lua-resty-limit-traffic,通过它可以按照更复杂的业务逻辑进行动态限流处理。其提供了limit.conn和limit.req实现,算法与nginx_limit_conn和limit_req是一样的。

比如再次实现ngx_http_limit_req_module模块中的场景二测试,注意:需要下载lua-resty-limit-traffic模块并添加到OpenResty的lualib目录中。

注意:在OpenResty 1.11.2.2+版本中默认自带lua-resty-limit-traffic模块。

在nginx.conf中配置用来存放限流用的共享字典。

http {
......
lua_shared_dict limit_req_store 100m;
......
}

实现ngx_http_limit_req_module模块中的场景二测试的Lua代码如下。

/usr/local/nginx-1.17.2/lua/resty-limit-req.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.exit(error_status)
end
local key = ngx.var.binary_remote_addr --IP维度限流
local delay, err = lim:incoming(key, true) --流入请求,如果请求需要被延迟,则deley > 0
if not delay and err == "rejected" then --超出桶大小
ngx.exit(error_status)
end
if delay > 0 then --根据需要决定是延迟或者不延迟处理
if nodelay then --直接突发处理了

else
ngx.sleep(delay) --延迟处理
end
end

配置nginx.conf

http {
......
server {
location /limit {
access_by_lua_file /usr/local/nginx-1.17.2/lua/resty-limit-req.lua;
echo "success";
}
}
......
}

限流逻辑在Nginx access阶段被访问,如果不被限流,则继续后续流程。如果需要被限流,则要么sleep一段时间继续后续流程,要么返回相应的状态码拒绝请求。

在之前的分布式限流中,使用了Nginx+Lua进行分布式限流,也可以使用lua-resty-limit-traffic模块实现分布式限流。

接下来需要使用OpenResty启动Nginx,如下所示:

/usr/local/openresty/bin/openresty -p /usr/local/nginx-1.17.2 -c /usr/local/nginx-1.17.2/conf/nginx.conf

注意:有关OpenResty的安装和配置请参考博文《​​高可用之——Consul+OpenResty实现无reload动态负载均衡​​》

这里,定义一个req.sh脚本

ab -c 6 -n 6 http://192.168.175.100/limit
sleep 0.3
ab -c 6 -n 6 http://192.168.175.100/limit

执行req.sh脚本,access.log文件输出的信息如下:

[\xC0\xA8\xAFd] [12/Sep/2019:14:58:34 +0800] [1568271514.793] 503
[\xC0\xA8\xAFd] [12/Sep/2019:14:58:34 +0800] [1568271514.793] 503
[\xC0\xA8\xAFd] [12/Sep/2019:14:58:35 +0800] [1568271515.293] 200
[\xC0\xA8\xAFd] [12/Sep/2019:14:58:35 +0800] [1568271515.793] 200
[\xC0\xA8\xAFd] [12/Sep/2019:14:58:36 +0800] [1568271516.292] 200
[\xC0\xA8\xAFd] [12/Sep/2019:14:58:36 +0800] [1568271516.793] 200

[\xC0\xA8\xAFd] [12/Sep/2019:14:58:36 +0800] [1568271516.793] 503
[\xC0\xA8\xAFd] [12/Sep/2019:14:58:36 +0800] [1568271516.793] 503
[\xC0\xA8\xAFd] [12/Sep/2019:14:58:36 +0800] [1568271516.793] 503
[\xC0\xA8\xAFd] [12/Sep/2019:14:58:37 +0800] [1568271517.294] 200
[\xC0\xA8\xAFd] [12/Sep/2019:14:58:37 +0800] [1568271517.793] 200
[\xC0\xA8\xAFd] [12/Sep/2019:14:58:38 +0800] [1568271518.293] 200

在使用Nginx+Lua时,也可以获取ngx.var.connections_active进行过载保护,如果当前活跃连接数超过阈值,则进行限流保护。如下所示。

if tonumber(ngx.var.connections_active) >= tonumber(limit) then
//限流
end

Nginx也提供了limit_rate对流量进行限速,如limit_rate 50k,表示限制下载速度为50k。

涛哥的《亿级流量网站架构核心技术》