NGINX 的速率限制(限流)
NGINX最有用但经常被误解和配置错误的功能之一是限流。它允许您限制用户在给定时间段内可以发出的HTTP请求量。
限流可以用于安全目的,例如减慢暴力破解密码的攻击。它可以通过限制请求速率为真实用户的典型值来帮助防御分布式拒绝服务(DDoS)攻击,并且(通过记录日志)可以识别被攻击的URL。更一般地说,它用于保护上游应用服务器免受过多用户请求同时到达而导致的压力过大。
在本博客中,我们将介绍 NGINX 限流的基础知识以及更高级的配置。当然限流在 NGINX Plus 中的工作方式相同。
NGINX限流的工作原理
NGINX 限流使用漏桶算法,该算法广泛用于电信和数据包交换计算机网络,用于在带宽有限时处理突发性。好比一个水桶,水从上面倒入,从下面漏出;如果倒入水的速度超过漏水的速度,水桶就会溢出。在请求处理方面,水代表客户端的请求,桶代表一个队列,请求按照先进先出(FIFO)调度算法等待处理。漏水代表退出缓冲区供服务器处理的请求,溢出代表被丢弃且从未得到服务的请求。
限流基本配置
限流由两个主要指令limit_req_zone
和进行配置limit_req
,如下例所示:
需要注意的一个点是rate速率是正整数,/s是每秒;/m是每分
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
server {
location /qhyu/ {
limit_req zone=mylimit;
proxy_pass http://my_upstream;
}
}
该limit_req_zone
指令定义了速率限制的参数,同时limit_req
在它出现的上下文中启用速率限制(在示例中,对于 /qhyu/ 的所有请求)。
我用postman做了一个并发测试,除了第一个接口返回200,其他的都被限流了。
nginx日志如下:
该limit_req_zone指令通常在http块中定义,使其可在多个上下文中使用。它需要以下三个参数:
-
key-定义应用的请求特征。在示例中,它是nginx变量
$binary_remote_addr
,它保存客户端IP地址的二进制表现形式。这意味着我们将每个唯一的IP地址限制为第三个参数定义的请求速率。(我们使用此变量是因为它比客户端 IP 地址的字符串表示占用的空间更少$remote_addr
) -
zone-定义用于存储每个IP地址的状态及其访问请求限制URL的频率的共享内存区域。将信息保存在共享内存中意味着它可以在nginx工作进程之间共享。该定义有两部分:由关键字标识的区域名称zone=,以及冒号后面的大小。大约16000个IP地址的状态信息需要1兆字节,因为我们的区域可以存储大约160000个地址。
如果nginx需要添加新条目时存储空间耗尽,它将删除最旧的条目。如果释放的空间仍不足以容纳新记录,nginx将返回状态码。此外,为了防止内存耗尽,每次nginx创建条目时,它都会删除最多两个在前60s内未使用的条目。503 Service Temporarily Unavailable
-
rate-设置最大请求速率。在示例中,速率不能超过每秒10个请求。nginx实际上以毫秒粒度跟踪请求,因此此限制对应于每 100 毫秒 (ms) 1 个请求。因为我们不允许突发(请参阅下一节),这意味着如果请求在上一个允许的请求之后不到 100 毫秒到达,则该请求将被拒绝。
该limit_req_zone
指令设置了速率限制和共享内存区域的参数,但它实际上并不限制请求速率。为此,您需要通过在其中包含指令来将限制应用于特定location
或块。在示例中,我们对 /qhyu/ 的请求进行速率限制。limit_req
因此,现在每个唯一的 IP 地址限制为每秒 10 次 /qhyu/ 请求 ,或者更准确地说,不能在前一个 URL 的 100 毫秒内对该 URL 发出请求。
处理突发
如果我们在 100 毫秒内收到 2 个请求怎么办?对于第二个请求,NGINX 将状态代码返回503给客户端。一些业务场景下这可能不是我们想要的,因为应用程序本质上往往是突发性的。相反,我们希望缓冲任何多余的请求并及时为它们提供服务。如这个更新的配置所示:
location /qhyu/ {
limit_req zone=mylimit burst=20;
proxy_pass http://my_upstream;
}
该burst参数定义客户端可以发出超出区域指定速率的请求数量(对于我们的示例mylimit区域,速率限制为每秒 10 个请求,或每 100 毫秒 1 个请求)。比上一个请求晚 100 毫秒到达的请求将被放入队列中,这里我们将队列大小设置为 20。
这意味着如果 21 个请求同时从给定的 IP 地址到达,NGINX 会立即将第一个请求转发到上游服务器组,并将剩余的 20 个放入队列中。然后,它每 100 毫秒转发一个排队请求,503只有当传入请求使排队请求数量超过 20 时才返回客户端。
排队无延迟
这种配置burst可以使流量流畅,但不太实用,因为它会使您的网站显得很慢。在我们的示例中,队列中的第 20 个数据包等待 2 秒才能转发,此时对其的响应可能不再对客户端有用。为了解决这种情况,将nodelay参数与参数burst一起添加:
location /qhyu/ {
limit_req zone=mylimit burst=20 nodelay;
proxy_pass http://my_upstream;
}
使用该nodelay
参数,NGINX 仍然根据该burst
参数在队列中分配插槽并施加配置的速率限制,但不会间隔转发排队的请求。相反,当请求“太快”到达时,NGINX 会立即转发它,只要队列中有可用的槽位。它将该插槽标记为“已占用”,并且在经过适当的时间(在我们的示例中,100 毫秒后)之前不会将其释放以供其他请求使用。
与之前一样,假设 20 槽队列为空,并且有 21 个请求同时从给定 IP 地址到达。NGINX 立即转发所有 21 个请求,并将队列中的 20 个槽标记为已占用,然后每 100 毫秒释放 1 个槽。(如果有 25 个请求,NGINX 将立即转发其中 21 个,将 20 个槽标记为已占用,并拒绝 4 个状态为503的请求。)
现在假设第一组请求转发后 101 毫秒,另外 20 个请求同时到达。队列中只有 1 个槽被释放,因此 NGINX 转发 1 个请求并拒绝其他 19 个状态为 的请求 503。相反,如果在 20 个新请求到达之前已经过去了 501 毫秒,则有 5 个插槽空闲,因此 NGINX 会立即转发 5 个请求并拒绝 15 个。
效果相当于每秒 10 个请求的速率限制。nodelay
如果您想要施加速率限制而不限制请求之间允许的间距,则该选项非常有用。
注意: 对于大多数部署,我们建议 在指令limit_req
中包含burst
和nodelay
参数。
两阶段限速
综合上述内容,我们可以将nginx配置为允许突发请求以适应典型的web浏览器请求模式,然后将额外的过多请求限制到某个点,通过阈值拒绝其他过多的请求。使用limit_req指令的delay参数启用两级速率限制。
为了说明两阶段速率限制,这里我们配置 NGINX 通过施加每秒 5 个请求 (r/s) 的速率限制来保护网站。该网站通常每页有 4-6 个资源,但永远不会超过 12 个资源。该配置允许突发最多 12 个请求,其中前 8 个请求将立即得到处理。在 8 个过多请求后添加延迟以强制执行 5 r/s 限制。超过 12 次请求后,任何进一步的请求都会被拒绝。
limit_req_zone $binary_remote_addr zone=ip:10m rate=5r/s;
server {
listen 80;
location / {
limit_req zone=ip burst=12 delay=8;
proxy_pass http://website;
}
}
该delay
参数定义了在突发大小内限制(延迟)过多请求以符合定义的速率限制的点。完成此配置后,以 8 r/s 发出连续请求流的客户端会经历以下行为。
前 8 个请求( delay的值)由 NGINX无延迟地代理。接下来的 4 个请求 ( burst - delay) 将被延迟,以便不超过定义的 5 r/s 速率。接下来的 3 个请求将被拒绝,因为已超出总突发大小。后续请求将被延迟。
高级配置示例
通过将基本速率限制与其他 NGINX 功能相结合,您可以实现更细致的流量限制。
列入白名单
此示例演示如何对不在“允许列表”中的任何人的请求施加速率限制。
geo $limit {
default 1;
10.0.0.0/8 0;
192.168.0.0/24 0;
}
map $limit $limit_key {
0 "";
1 $binary_remote_addr;
}
limit_req_zone $limit_key zone=req_zone:10m rate=5r/s;
server {
location / {
limit_req zone=req_zone burst=10 nodelay;
# ...
}
}
此示例同时使用了geo
和map
指令
- 如果
$limit
是0
则$limit_key
设置为空字符串 - 如果
$limit
是1
,$limit_key
则设置为二进制格式的客户端 IP 地址
将两者放在一起,$limit_key
对于列入白名单的 IP 地址,设置为空字符串,否则设置为客户端的 IP 地址。当目录的第一个参数limit_req_zone
(密钥)为空字符串时,不应用限制,因此列入白名单的 IP 地址(在 10.0.0.0/8 和 192.168.0.0/24 子网中)不受限制。所有其他 IP 地址限制为每秒 5 个请求。
该limit_req
指令将限制应用于 / 位置,并允许突发超过配置限制的最多 10 个数据包,且转发无延迟
在一个位置中包含多个limit_req指令
可以limit_req在一个位置包含多个指令。应用与给定请求匹配的所有限制,这意味着使用最严格的限制。例如,如果多个指令施加延迟,则使用最长的延迟。同样,如果这是任何指令的效果,则请求将被拒绝,即使其他指令允许它们通过。
扩展前面的示例,我们可以对白名单上的 IP 地址应用速率限制:
http {
# ...
limit_req_zone $limit_key zone=req_zone:10m rate=5r/s;
limit_req_zone $binary_remote_addr zone=req_zone_wl:10m rate=15r/s;
server {
# ...
location / {
limit_req zone=req_zone burst=10 nodelay;
limit_req zone=req_zone_wl burst=20 nodelay;
# ...
}
}
}
允许名单上的 IP 地址与第一个速率限制 ( req_zone )不匹配,但与第二个速率限制 ( req_zone_wl )匹配,因此限制为每秒 15 个请求。不在允许列表中的 IP 地址与两个速率限制均匹配,因此选择限制性更强的速率限制:每秒 5 个请求。
配置相关功能
记录
日志条目中的字段包括:
2023/08/25
04:20:00
– 写入日志条目的日期和时间[error]
– 严重程度120315#0
– NGINX工作线程的进程ID和线程ID,用符号#
分隔*32086
– 速率受限的代理连接的 IDlimiting
requests
– 指示日志条目记录速率限制excess
– 此请求所代表的配置速率上的每毫秒请求数zone
– 定义施加速率限制的区域client
– 发出请求的客户端的 IP 地址server
– 服务器的 IP 地址或主机名request
– 客户端发出的实际 HTTP 请求host
–Host
HTTP 标头的值
默认情况下,NGINX 在该级别记录拒绝的请求,如上例error
所示。[error]
(默认情况下,它会在低一级记录延迟请求warn
。)要更改日志记录级别,请使用该limit_req_log_level
指令。这里我们将拒绝的请求设置为日志级别warn
:
location /qhyu/ {
limit_req zone=mylimit burst=20 nodelay;
limit_req_log_level warn;
proxy_pass http://my_upstream;
}
发送给客户端的错误代码
默认情况下,当客户端超出其速率限制时,NGINX 会返回状态代码503
( Service
Temporarily
Unavailable
) 。使用limit_req_status
指令设置不同的状态代码(444
在本例中):
location /login/ {
limit_req zone=mylimit burst=20 nodelay;
limit_req_status 444;
}
拒绝对特定位置的所有请求
如果您想拒绝对特定 URL 的所有请求,而不仅仅是限制它们,请location
为其配置一个块并包含以下指令:
location /foo.php {
deny all;
}