NGINX 和 NGINX Plus 的流量控制 (翻译)

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 实例设置了一个固定值来限制入口流量,而不管请求到达哪个实例。详情参见 blogNGINX Plus Admin Guide

NGINX 的流量控制是怎么工作的?

leak bucket.png

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 发出连续请求的客户端将经历如下:

two-stage-rate-limiting-example.png

如上架构图,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;
 
        # ...
    }
}

范例同时使用了 geomap 指令。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 is 0, $limit_key is set to the empty string
  • If $limit is 1, $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;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容