解析 Nginx limit_req 模块的 burst 参数,实现限制 IP 地址 URL 访问频率

编辑于:2021年12月12日
解析 Nginx limit_req 模块的 burst 参数,实现限制 IP 地址 URL 访问频率

漏斗算法(Leaky Bucket),该算法有两种处理方式 Traffic ShapingTraffic Policing 在桶满水之后,常见的两种处理方式为:

  • ⭕暂时拦截住上方水的向下流动,等待桶中的一部分水漏走后,再放行上方水。
  • ⭕溢出的上方水直接抛弃。

将水看作网络通信中数据包的抽象,则 方式1 起到的效果称为 Traffic Shaping ,方式2 起到的效果称为 Traffic Policing。由此可见,Traffic Shaping的核心理念是"等待",Traffic Policing 的核心理念是"丢弃"。它们是两种常见的流速控制方法

源码:limit_req 模块的源码在 src/http/modules/ngx\_http\_limit\_req\_module.c ,关于 burst 的核心代码在这:

//src/http/modules/ngx_http_limit_req_module.c:396
ms = (ngx_msec_int_t) (now - lr->last);
excess = lr->excess - ctx->rate * ngx_abs(ms) / 1000 + 1000;
if (excess < 0) {
    excess = 0;
}
*ep = excess;
if ((ngx_uint_t) excess > limit->burst) {
   return NGX_BUSY;
}
if (account) {
    lr->excess = excess;
    lr->last = now;
   return NGX_OK;
}

excess 初始值是 0,假设现在 ctx->rate 是 2000(即2 request/s),这次请求距离上次请求是 400ms。那么 excess = 0 – 2000 * 400 / 1000 + 1000 = 200。如果 limit->burst 是 0,那么 200 > 0,会返回 NGX_BUSY 即是 503 了。

假如 burst 是 1,limit->burst 即是 1000,那么如果请求是每隔 400ms 来一个,共需 5 个才会填满 limit->burst (每个请求将会增加 200 excess),到第 6 个才会返回 503。

推导出公式,假设设置频率是 r request/s,每次请求距离上次请求 t ms,设置 burst 为 b,

那么返回 503 的临界请求个数 x 是

\begin{equation}
x = floor(b * \frac {( 1000 / r )} {( 1000 / r – t )})
\end{equation}

虽然知道了源码但是还是没有一个非常直观的感觉,所以不如实际操作一下:

实际操作:

nginx 配置

nginx 中该模块的使用配置 示例1

imit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;

server {
    location  /search/ {
        limit_req zone=one burst=5 nodelay;
    }

第一段配置参数:

$binary_remote_addr:表示通过 remote_addr 这个标识来做限制,“binary_” 的目的是缩写内存占用量,是限制同一客户端 ip 地址

zone=one:10m:表示生成一个大小为 10M,名字为 one 的内存区域,用来存储访问的频次信息

rate=1r/s:表示允许相同标识的客户端的访问频次,这里限制的是每秒 1 次,即每秒只处理一个请求,还可以有比如 30r/m 的,即限制每 2 秒访问一次,即每 2 秒才处理一个请求。

第二段配置参数:

zone=one:设置使用哪个配置区域来做限制,与上面 limit_req_zone 里的 name 对应

burst=5:重点说明一下这个配置,burst 爆发的意思,这个配置的意思是设置一个大小为 5 的缓冲区当有大量请求(爆发)过来时,超过了访问频次限制的请求可以先放到这个缓冲区内等待,但是这个等待区里的位置只有5个,超过的请求会直接报503的错误然后返回。

nodelay:如果设置,会在瞬时提供处理(burst + rate)个请求的能力,请求超过(burst + rate)的时候就会直接返回503,永远不存在请求需要等待的情况。(这里的 rate 的单位是:r/s)
如果没有设置,则所有请求会依次等待排队。

令牌桶算法

这里的 burst 参数主要采用了令牌桶算法。令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常用的一种算法。典型情况下,令牌桶算法用来控制发送到网络上的数据数目,并允许突发数据的发送。

令牌桶这种控制机制基于令牌桶中是否存在令牌来指示什么时候可以发送流量。令牌桶中的每一个令牌都代表一个字节。如果令牌桶中存在令牌,则允许发送流量;如果令牌桶中不存在令牌,则不允许发送流量。因此,如果突发门限被合理地配置并且令牌桶中有足够的令牌,那么流量就可以以峰值速率发送。

令牌桶算法的基本过程如下:

  • ⭕假如用户配置的平均发送速率为10r/s,则每隔0.1秒一个令牌被加入到桶中;
  • ⭕假设桶最多可以存发b个令牌。如果令牌到达时令牌桶已经满了,那么这个令牌会被丢弃;
  • ⭕当一个n个字节的数据包到达时,就从令牌桶中删除n个令牌,并且数据包被发送到网络;
  • ⭕如果令牌桶中少于n个令牌,那么不会删除令牌,并且认为这个数据包在流量限制之外;
  • ⭕算法允许最长b个字节的突发,但从长期运行结果看,数据包的速率被限制成常量r。

对于在流量限制外的数据包可以以不同的方式处理:

  • ⭕它们可以被丢弃;
  • ⭕它们可以排放在队列中以便当令牌桶中累积了足够多的令牌时再传输;
  • ⭕它们可以继续发送,但需要做特殊标记,网络过载的时候将这些特殊标记的包丢弃。

注意:

令牌桶算法不能与另外一种常见算法“漏斗算法(Leaky Bucket)”相混淆。这两种算法的主要区别在于“漏斗算法”能够强行限制数据的传输速率,而“令牌桶算法”在能够限制数据的平均传输数据外,还允许某种程度的突发传输。

在“令牌桶算法”中,只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的门限,因此它适合于具有突发特性的流量。(这部分是网上很多博客都错误的地方)

例子演示:

首先我们配置了limt_req_zone,rate=10r/m,即每六秒才处理一次请求,如下:

1. 首先测试不加 burst 和不加 nodelay 的情况:

查看当前的 tcp 连接数

netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

结果如下:

使用ab测试工具,发起 10 个并发请求:

ab -n 10 -c 10 url

ab 压测工具瞬间返回了结果

可以看到一共1 0 个请求,9 个请求都失败了。且 0.09 秒就完成了压测
接着查看当前的 tcp 连接数:

可以观察到此时服务端的 TIME_WAIT 为 10,这意味着是服务端主动要求断开了所有 TCP 连接
接着再查看 /var/log/nginx/access.log,印证了只有一个请求成功了,其它就是都直接返回了 503,即服务器拒绝了请求。

2. 只加 burst 和不加 nodelay 的情况:

查看当前的 tcp 连接数

netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

结果如下:

使用ab测试工具,发起 10 个并发请求:

ab -n 10 -c 10 url

可以看到测试经过 30s 才结束

压测中一共 10 个请求,有 4 个请求失败了,直接返回了 503
查看当前的 tcp 连接数

上图是ab测试第一秒时的截图,TIME_WAIT=5 表示有服务器端主动断开了 5 个 TCP 连接,即 5 个请求被瞬时拒绝,同时 ESTABLISHED 的数量由 23 增加到 28,即建立了 5 个 TCP 连接

上图是ab测试过程中的截图,TIME_WAIT=7 表示有服务器端主动断开了7个TCP连接,增加的 2 个 TIME_WAIT 是因为有 2 个在缓存队列的请求被处理完毕了,所以断开了连接。

接着查看 /var/log/nginx/access.log 日志

可以观察到在 39 分 35 秒,即压测第 1 秒时,成功处理了 1 个请求,另外有 4 个请求瞬间返回了 503,剩下的 5 个请求每隔 6s 处理一次。
这是因为设置了 burst=5,在服务器接收到 10 个并发请求后,先处理 1 个请求,同时将 5 个请求放入 burst 缓冲队列中,等待处理。而超过(burst+1)数量的请求就被直接抛弃了,即直接抛弃了4个请求。
查看 /var/log/nginx/error.log 日志

发现有 5 个 delaying request,4 个 limiting request

3. 加 burst 和加 nodelay 的情况:

查看当前的 tcp 连接数

netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

结果如下:

使用 ab 测试工具,发起 10 个并发请求:

ab -n 10 -c 10 url

可以看到压测在 0.1 s 内完成了,这也是添加 nodelay 参数的意义

压测中一共 10 个请求,有 4 个请求失败了,直接返回了 503
查看当前的 tcp 连接数:所有的请求都在 1s 内处理完成了

接着查看 /var/log/nginx/access.log 日志

可以发现在 1s 内,服务器端处理了 6 个请求(峰值速度:burst+原来的处理速度)。对于剩下的 4 个请求,直接返回 503,在下一秒如果继续向服务端发送 10 个请求,服务端会直接拒绝这 10 个请求并返回503。因为设定了没 6s 处理 1 个请求,所以直到 30 s 之后,才可以再处理一个请求,即如果此时向服务端发送 10 个请求,会返回 9 个 503,一个 200
查看 /var/log/nginx/error.log 日志,发现有 4 个请求被直接拒绝了,没有延时请求。

总结:

limit_req zone=req_zone;

严格依照在 limti_req_zone 中配置的 rate 来处理请求
超过 rate 处理能力范围的,直接 drop
表现为对收到的请求无延时

limit_req zone=req_zone burst=5;

依照在 limti_req_zone 中配置的 rate 来处理请求
同时设置了一个大小为 5 的缓冲队列,在缓冲队列中的请求会等待慢慢处理
超过了 burst 缓冲队列长度和 rate 处理能力的请求被直接丢弃
表现为对收到的请求有延时

limit_req zone=req_zone burst=5 nodelay;

依照在 limti_req_zone 中配置的 rate 来处理请求
同时设置了一个大小为 5 的缓冲队列,当请求到来时,会爆发出一个峰值处理能力,对于峰值处理数量之外的请求,直接丢弃
在完成峰值请求之后,缓冲队列不能再放入请求。如果 rate=10r/m,且这段时间内没有请求再到来,则每 6s 缓冲队列就能回复一个缓冲请求的能力,直到回复到能缓冲 5 个请求位置。

相关推荐