Rate Limiting with NGINX and NGINX Plus
NGINX有一个很常用,但是常被误解、误配置的功能特性,rate limiting 流量控制
。 它允许你限制用户在给定时间内的HTTP请求数量。请求可以是简单的站点首页 GET
请求,或者是登录表单的 POST
请求。
限流也可以用于安全目的,比如降低暴力密码猜测攻击频率。它还可以通过限制入口流量到一个与真实用户对应的值,以及(通过登录)标识目的URLs来 阻止 DDoS 攻击 。 更多的是,保护上游应用服务器不被同一时间段内的大量访问请求压垮。
本文,我们会介绍 NGINX 流量控制的基本特性,以及一些高级配置项。NGINX Plus 相同。
学习更多关于NGINX的流量控制,参考 on-demand webinar.
NGINX Plus R16 及以后的版本支持全局流量控制 global rate limiting
: 集群的 NGINX Plus 实例设置了一个固定值来限制入口流量,而不管请求到达哪个实例。详情参见 blog 和 NGINX Plus Admin Guide。
NGINX 的流量控制是怎么工作的?
NGINX 流量控制使用 漏桶算法 leaky bucket algorithm
,该算法被广泛用于电信和分组交换计算机网络中,以处理带宽有限时的突发性。 比喻,水从桶的顶部倒入,从底部漏出;如果水倒入的速度超过了漏出的速度,那么水就会溢出。从请求的处理来看,水代表客户端的请求,桶代表请求队列,请求根据FIFO调度逻辑被处理。漏出的水表示请求退出缓存被服务处理,溢出表示请求被丢弃并不再得到服务。
流量控制的基本配置
通过两个主要指令limit_req_zone
and limit_req
来配置流量控制,范例如下:
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
server {
location /login/ {
limit_req zone=mylimit;
proxy_pass http://my_upstream;
}
}
limit_req_zone 指令定义流量控制参数,limit_req 在它出现的上下文开启流量控制(范例内,应用于访问 /login/ 的请求)。
limit_req_zone
指令通常定义于 http 区块,这样在多个上下文内可用。它有如下三个参数:
Key – 定义了流量控制被应用的请求特征。范例内,是 NGINX 变量
$binary_remote_addr
,该变量保存了客户端IP地址的二进制表示。这意味着,我们将每个唯一的IP都限制到了第三个参数定义的请求速度。 (我们使用该变量,是因为它比客户端IP字符串表示变量$remote_addr
占用更少的空间)。-
Zone – 定义了一个共享内存区(zone)来存放每个IP地址&它请求被流量控制的URLs的状态。 将这些信息保存在共享内存内,意味着NGINX的工作进程可以共享。定义分两个部分:以
zone=
标识的 zone name 部分,和:
后跟着的size部分。打开16,000 IP地址的状态信息约占 1 megabyte,所以这里定义的zone可以存储约 160,000 个地址。如果NGINX需要追加新条目的时候,存储空间用尽了,那么它会删除最老的条目。如果释放的空间仍然不足以容纳新纪录,NGINX 返回状态码
503
(Service
Temporarily
Unavailable)
。另外,为了避免内存被耗尽,每当NGINX 创建一个新条目的时候,它就会删除两个前面60秒内没有被使用的条目。 Rate – 设置最大请求速度。范例内,不可以超过每秒10个请求。NGINX 实际上以毫秒颗粒度追踪请求,因此该限制等同于每100毫秒(ms)1个请求。因为我们不允许
突发 bursts
,这意味着如果一个请求在前一个被允许的请求之后不到100ms到达,它会被丢弃。
limit_req_zone
指令设置速度限制的参数和共享内存区,但是实际上它不限速。需要通过包含limit_req
指令,将该限制应用到某个指定的位置或server区块。 在该范例内,我们对/login/的请求进行了限速。
因此,这里每个唯一的IP地址访问/login/被限制为每秒10个请求 - 或更准确的说,不能在前一个请求的100ms内再次请求该URL。
处理突发 Bursts
如果我们在100ms内收到2个请求怎么处理呢?第二个请求会被NGINX返回客户端状态码503。这不是我们期望的,应用天生就是会突发的。相反,我们希望缓存任何额外的请求,并及时服务。
如下配置,这就是我们在limit_req
使用burst
参数的地方:
location /login/ {
limit_req zone=mylimit burst=20;
proxy_pass http://my_upstream;
}
burst
参数定义了客户端可以发出多少个超过zone定义速度限制的请求(在范例mylimit zone中,速度限制为每秒10个请求,或每100ms1个请求)。在100ms内到达的下一个请求,会被放到一个队列,然后我们设置这个队列的长度为20。
这意味着,如果一个IP同时发来21个请求, NGINX 立即转发第一个到上游服务器,然后将剩余的20个请求放进队列。然后每100ms转发一个队列请求,如果传入的请求导致排队的请求数量大于20会向客户端返回503。
Queueing with No Delay
配置 burst 会使流量流畅,但是不实际,因为会使站点看起来响应缓慢。在范例中,队列中的第20个数据包会在2 seconds后才被转发, 在某种程度上它的响应对客户端已经没有意义了。要解决这个问题,追加 nodelay
参数跟 burst
一起:
location /login/ {
limit_req zone=mylimit burst=20 nodelay;
proxy_pass http://my_upstream;
}
使用 nodelay
参数,NGINX 仍然会根据burst
参数分配队列插槽(slot),并应用速度限制配置,但是不是通过space out消息队列的转发来实现的。相反,当一个请求来的太快,只要队列内有可用插槽(slot),NGINX 会立即转发。 它会标记该插槽(slot)为 taken
,并且直到应该的时间到达前(范例中,100ms)不会释放给其他请求使用。
假设,如前面方案,给定IP有21个请求同时到达,第20-slot是空的。NGINX 立即转发所有21个请求,并标记队列内的20个slots为taken
,然后每100ms释放1个slot。(如果有25个请求同时到达, NGINX 会立即转发其中21个,标记 20 slots 为 taken
,并返回4个请求503.)
现在假设第一批请求转发101ms后,第二批20个请求同时到达。队列内仅有 1 slot 被释放了,那么 NGINX 将转发1个请求,然后返回其他19个请求503。假设是501ms后,第二批20个请求同时到达,队列内有5个释放的slots,那么 NGINX 会转发5个请求,并拒绝15个。
其效果等同于每秒10个请求的流量限制。如果又想进行流量控制,又不想通过请求之间的spaceing来约束,那么nodelay
选项是非常有用的。
注意: 对于大多数开发,我们推荐在limit_req
指令内同时使用 burst
and nodelay
。
两级流量控制 Two-Stage Rate Limiting
在 NGINX Plus R17 or NGINX Open Source 1.15.7 版本,可用配置 NGINX 允许突发来适应典型的浏览器请求类型,然后设置一个过度请求的阈值,超过该值的请求会被拒绝。两级流控通过在 limit_req
指令以 delay
参数开启。
为了示例两级流控,我们配置 NGINX 速度限制为 5r/s 来保护web站点。该web站点通常每个页面有 4–6 个资源,不会超过12 个资源。配置允许突发12个请求,最前面的8个请求会被直接处理无需延迟(delay)。在8个额外请求后,会增加一个delay来强制 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
参数定义了在突发范围内进行额外请求delay以匹配流控限制(r/s)的值。 通过该配置,以 8 r/s 发出连续请求的客户端将经历如下:
如上架构图,rate=5r/s burst=12 delay=8
前面8个请求 (delay的值) 被NGINX Plus立即处理。接下来的4个请求 (burst - delay)会被延迟,这阿姨那个定义的 5 r/s 的流控限制才不会被超过。下面的3个请求会被拒绝,因为burst的空间已经超了。随后的请求会被延迟。
高级配置范例
通过将流控和其他NGINX特性绑定,你可以实现更多细节流控。
白名单 Allowlisting
如下范例,演示了如何对不在allowlist
内的IP进行流控。
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
指令。geo
块赋予allowlist 内的IP地址的 $limit
值为0,其他为1。block assigns a value of 0
to $limit
for IP addresses in the allowlist and 1
for all others. 我们使用一个map来转换这些值到key,比如:
- If
$limit
is0
,$limit_key
is set to the empty string - If
$limit
is1
,$limit_key
is set to the client’s IP address in binary format
将二者放在一起,对于允许的IP地址 $limit_key
被设置为空字符串,其他被设置为客户端IP地址。 当 limit_req_zone
指令的第一个参数 (the key) 是空字符串时,不加载限制,所以白名单内的IP(in the 10.0.0.0/8 and 192.168.0.0/24 subnets)将不被限制。其他的地址会被限制为 5 r/s。
limit_req
指令赋予流控到 /,允许突发10个包没有延迟。
复合 limit_req
指令
可以在某个位置应用多个 limit_req
指令。一个给定请求所应用上所有的流控限制,意味着采取最严格的限制。比如,如果>1条指定强制delay,那么会使用最长的delay。相似的,哪怕只有一条指令生效,其他所有的指定都允许放行,请求也会被拒绝。
扩展先前的范例,我们可以将流控应用到白名单:
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 r/s。非白名单内的地址都匹配,所以收到最严格的限制:5 r/s 。
Configuring Related Features
Logging
默认,NGINX 记录因为流控而延迟、丢弃的请求日志,如下范例:
2015/06/13 04:20:00 [error] 120315#0: *32086 limiting requests, excess: 1.000 by zone "mylimit", client: 192.168.1.2, server: nginx.com, request: "GET / HTTP/1.0", host: "nginx.com"
Fields in the log entry include:
- 2015/06/13 04:20:00 – Date and time the log entry was written
- [error] – Severity level
- 120315#0 – Process ID and thread ID of the NGINX worker, separated by the # sign
- *32086 – ID for the proxied connection that was rate‑limited
- limiting requests – Indicator that the log entry records a rate limit
- excess – Number of requests per millisecond over the configured rate that this request represents
- zone – Zone that defines the imposed rate limit
- client – IP address of the client making the request
- server – IP address or hostname of the server
- request – Actual HTTP request made by the client
- host – Value of the Host HTTP header
By default, NGINX logs refused requests at the error
level, as shown by [error]
in the example above. (It logs delayed requests at one level lower, so warn
by default.) To change the logging level, use the limit_req_log_level
directive. Here we set refused requests to log at the warn
level:
location /login/ {
limit_req zone=mylimit burst=20 nodelay;
limit_req_log_level warn;
proxy_pass http://my_upstream;
}
Error Code Sent to Client
By default NGINX responds with status code 503
(Service
Temporarily
Unavailable
) when a client exceeds its rate limit. Use the limit_req_status
directive to set a different status code (444
in this example):
location /login/ {
limit_req zone=mylimit burst=20 nodelay;
limit_req_status 444;
}
Denying All Requests to a Specific Location
If you want to deny all requests for a specific URL, rather than just limiting them, configure a location
block for it and include the deny
all
directive:
location /foo.php {
deny all;
}