Iawen's Blog

我喜欢这样自由的随手涂鸦,因为我喜欢风......

NGINX最有用但经常被误解和配置错误的功能之一是速率限制. 它允许您限制用户在给定时间段内可以发出的HTTP请求的数量. 请求可以很简单, 例如GET对网站首页的POST请求或登录表单上的请求.

速率限制可以出于安全目的使用, 例如, 可以降低暴力破解密码猜测攻击的速度. 通过将传入请求速率限制为实际用户的典型值, 并(使用日志记录)标识目标URL, 它可以帮助防御DDoS攻击. 更一般而言, 它用于防止上游应用程序服务器同时被太多用户请求所淹没.

在此, 我们将介绍NGINX的速率限制基础以及更高级的配置. 速率限制在NGINX Plus中的工作方式相同. NGINX Plus R16和更高版本支持"全局速率限制”:群集中的NGINX Plus实例对传入的请求应用一致的速率限制, 而不管请求到达群集中的哪个实例. (集群中的状态共享也可用于其他NGINX Plus功能. )有关详细信息, 请参见我们的博客和NGINX Plus管理指南.

1. NGINX速率限制如何工作

NGINX速率限制使用漏斗算法, 该算法广泛应用于电信和分组交换计算机网络中, 以在带宽受限时处理突发性问题. 类比是用一个水桶, 在水桶的顶部浇水, 然后从底部漏水. 如果倒水的速度超过漏水的速度, 则水桶会溢出. 在请求处理方面, 水代表来自客户端的请求, 存储桶代表队列, 根据先进先出(FIFO)调度算法, 请求等待处理. 泄漏的水表示退出缓冲区以供服务器处理的请求, 溢出表示已丢弃且从未得到服务的请求. 1

1.1 配置基本速率限制

速率限制使用两个主要指令 limit_req_zone 和进行配置 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: 定义用于存储每个IP地址状态及其访问请求限制URL的频率的共享内存区域. 将信息保存在共享内存中意味着可以在NGINX工作进程之间共享信息. 该定义分为两部分:由 zone= 关键字标识的区域名称, 以及冒号后面的大小. 大约16,000个IP地址的状态信息需要1兆字节, 因此我们的区域可以存储大约160,000个地址. 如果NGINX需要添加新条目时存储空间耗尽, 它将删除最旧的条目. 如果释放的空间仍然不足以容纳新记录, 则NGINX返回状态码. 另外, 为防止内存耗尽, NGINX每次创建一个新条目时, 都会删除最多60秒钟内未使用的两个条目. 503 (Service Temporarily Unavailable)
  • Rate: 设置最大请求速率. 在该示例中, 速率不能超过每秒10个请求. NGINX实际上以毫秒粒度跟踪请求, 因此此限制对应于每100毫秒(ms)1个请求. 因为我们不允许突发(请参阅下一节), 所以这意味着如果请求在上一个允许的请求之后不到100毫秒到达, 则该请求将被拒绝.

该limit_req_zone指令设置速率限制和共享内存区域的参数, 但实际上并没有限制请求速率. 为此, 您需要通过在其中包含指令来将限制应用于特定location或server块limit_req. 在示例中, 我们将对 /login/ 的请求进行速率限制. 因此, 现在每个唯一IP地址每秒只能对 /login/ 请求10个请求, 或者更确切地说, 不能在前一个IP地址的100ms内对该URL发出请求.

1.2 处理突发 Handling Bursts

如果我们在100ms内收到2个请求怎么办?对于第二个请求, NGINX将状态代码返回503给客户端. 这可能不是我们想要的, 因为应用程序本质上往往是突发性的. 相反, 我们希望缓冲任何多余的请求并及时为它们提供服务. 在此, 我们在此burst参数中使用参数limit_req, 如在此更新的配置中:

location /login/ {
    limit_req zone=mylimit burst=20;
 
    proxy_pass http://my_upstream;
}

该burst参数定义了一个客户端可以发出的请求超出该区域指定的速率(对于我们的示例mylimit区域, 该速率限制为每秒10个请求, 或每100ms 1个). 将比上一个请求晚100ms到达的请求放入队列, 在这里, 我们将队列大小设置为20.

这意味着, 如果同时有21个请求从给定IP地址到达, NGINX将第一个请求立即转发到上游服务器组, 并将其余20个请求放入队列. 然后, 它每100毫秒转发一个排队的请求, 并且503仅在传入请求使排队的请求数超过20时才返回到客户端.

1.3 无延迟排队 Queueing with No Delay

具有的配置可burst带来顺畅的流量, 但不是很实用, 因为它可能会使您的站点显得很慢. 在我们的示例中, 队列中的第20个数据包等待2秒被转发, 这时对它的响应可能不再对客户端有用. 要解决这种情况, 请将nodelay参数与参数一起添加burst:

location /login/ {
    limit_req zone=mylimit burst=20 nodelay;
 
    proxy_pass http://my_upstream;
}

使用该nodelay参数, NGINX仍会根据该burst参数在队列中分配时隙, 并强加配置的速率限制, 但不会通过间隔排队请求的转发来实现. 相反, 当请求"过早"到达时, 只要队列中有可用的插槽, NGINX就会立即转发该请求. 它将该插槽标记为"已占用”, 并且直到经过适当的时间(在我们的示例中为100ms之后)之前, 它不会释放该插槽供其他请求使用.

像以前一样, 假定20插槽队列为空, 并且从给定IP地址同时到达21个请求. NGINX立即转发所有21个请求, 并将队列中的20个时隙标记为已占用, 然后每100毫秒释放1个时隙. (如果有25个请求, NGINX将立即转发其中的21个请求, 将20个插槽标记为已占用, 并拒绝4个请求(返回状态 503) .

现在, 假设在转发第一组请求之后101毫秒, 又有20个请求同时到达. 队列中只有1个插槽已被释放, 因此NGINX转发1个请求, 并拒绝其他19个(返回状态 503). 相反, 如果在20个新请求到达之前已经过去了501ms, 则5个时隙是空闲的, 因此NGINX立即转发5个请求并拒绝15个.

该效果相当于每秒10个请求的速率限制. nodelay如果您想施加速率限制而不限制请求之间允许的间隔, 则此选项很有用.

注意:对于大多数部署, 我们建议 在指令中包含burst和nodelay参数limit_req.

2. 两阶段限速 Two-Stage Rate Limiting

使用NGINX Plus R17或NGINX开源1.15.7, 您可以配置NGINX以允许突发请求以适应典型的Web浏览器请求模式, 然后将额外的过多请求限制到一个点, 超过该限制, 额外的多余请求将被拒绝. 指令的delay参数启用了两阶段速率限制limit_req.

为了说明两阶段的速率限制, 在这里我们将NGINX配置为通过施加每秒5个请求 (5r/s) 的速率限制来保护网站. 该网站通常每页有4–6个资源, 并且永远不会超过12个资源. 该配置最多允许突发12个请求, 其中第一个8个请求将被立即处理. 在强制执行 5r/s 限制的8个过多请求之后添加了延迟. 在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参数定义了在突发大小内限制(延迟)过多请求以符合定义的速率限制的点. 使用此配置后, 以 8r/s 连续发出请求流的客户端将遇到以下行为. 00 Illustration of rate‑limiting behavior with rate=5r/s burst=12 delay=8

前8个请求(the value of delay)由NGINX Plus代理, 没有延迟. 接下来的4个请求(burst - delay)被延迟, 因此不会超过所定义的 5r/s 的速率. 接下来的3个请求被拒绝, 因为超出了总突发大小. 后续请求被延迟.

3. 限制连接数

您可以将单个客户端IP地址可以打开的连接数限制为适合真实用户的值. 例如, 您可以允许每个客户端IP地址打开到网站的 /store/ 区域的连接不超过10个(只有在服务器处理了请求并且已经读取了整个请求头时, 连接才被计数):

limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;

server {
    # ...
    location /store/ {
        limit_conn perip 10;

        # 限制连接到虚拟服务器的总数
        limit_conn perserver 100; 

        # ...
    }
}

所述limit_conn_zone 指令配置的共享存储器区称为地址到指定键存储请求, 在这种情况下(如在前面的例子中)的客户端的IP地址, $binary_remote_addr. /store 块中的limit_conn指令引用共享内存区域, 并从每个客户端IP地址设置最多10个连接.

4. 限制特定URL的带宽 Limiting Bandwidth for Particular URLs

如果服务器提供较大的文件(或较小但非常受欢迎的文件, 例如表单或报表), 则设置客户端下载文件的最大速度可能很有用。如果您的站点已经承受了很高的网络负载, 则限制下载速度会留出更多带宽, 以使应用程序的关键部分保持响应速度。这是硬件制造商使用的非常受欢迎的解决方案–您可能需要等待更长的时间才能为打印机下载3 GB的驱动程序, 但是同时有成千上万的其他人下载您仍然可以下载。😉

使用limit_rate指令限制特定URL的带宽。在这里, 我们将/ download 下每个文件的传输速率限制为每秒50 KB。

location /download/ {
    limit_rate 50k;
}

您可能还希望仅对较大的文件进行速率限制, 这可以使用limit_rate_after指令进行。在此示例中, 每个文件(来自任何目录)的前500 KB都不受速度限制地进行传输, 之后的所有内容均以50 KB / s为上限。这样可以更快地交付网站的关键部分, 同时降低其他部分的速度。

location / {
    limit_rate_after 500k;
    limit_rate 50k;
}

请注意, 速率限制适用于浏览器和NGINX之间的单个HTTP连接, 因此请不要阻止用户使用下载管理器来绕开速率限制。

最后, 您还可以限制与服务器的并发连接数或请求速率。有关详细信息, 请参见文档

5. ngx_http_geoip_module 模块

nginx默认不编译这个模块, 需要开启–with-http_geoip_module编译选项。模块依赖MaxMind GeoIP库。 配置示例

http {
    geoip_country         GeoIP.dat;
    geoip_city            GeoLiteCity.dat;
    geoip_proxy           192.168.100.0/24;
    geoip_proxy           2001:0db8::/32;
    geoip_proxy_recursive on;
    ...

指定数据库, 用于根据客户端IP地址得到其所在国家。 使用这个数据库时, 配置中可用下列变量:

  • $geoip_country_code 双字符国家代码, 比如 “RU”, “US”。
  • $geoip_country_code3 三字符国家代码, 比如 “RUS”, “USA”。
  • $geoip_country_name 国家名称, 比如 “Russian Federation”, “United States”。
  • $geoip_city_country_code 双字符国家代码, 比如 “RU”, “US”。
  • $geoip_city_country_code3 三字符国家代码, 比如 “RUS”, “USA”。
  • $geoip_city_country_name 国家名称, 比如 “Russian Federation”, “United States”。
  • $geoip_region 国家行政区名(行政区、直辖区、州、省、联邦管辖区, 诸如此类), 比如 “Moscow City”, “DC”。
  • $geoip_city 城市名称, 比如 “Moscow”, “Washington”。
  • $geoip_postal_code 邮编

6. 高级配置示例

通过将基本速率限制与其他NGINX功能结合使用, 您可以实现更细微的流量限制.

6.1 允许列出 Allowlisting

此示例说明了如何对不在"允许列表"上的任何人的请求施加速率限制.

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块为允许列表中的IP地址的 $limit 赋0, 所有其他IP地址 的 $limit 赋 为1 . 然后, 我们使用映射将这些值转换为键, 例如:

  • 如果 $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个数据包突发, 而转发没有延迟

6.2 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个请求.

7. 配置相关功能

7.1 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"

日志条目中的字段包括:

  • 2015/06/13 04:20:00 –日志条目的写入日期和时间
  • [error] –严重级别
  • 120315#0 – NGINX工作进程的进程ID和线程ID, 用#符号分隔
  • *32086 –受速率限制的代理连接的ID
  • limiting requests –指示日志条目记录了速率限制的指示符
  • excess –超出此请求代表的配置速率的每毫秒请求数
  • zone –定义施加的速率限制的区域
  • client –发出请求的客户端的IP地址
  • server –服务器的IP地址或主机名
  • request –客户端发出的实际HTTP请求
  • host – HostHTTP标头的值

默认情况下, NGINX在该error级别记录拒绝的请求, 如上[error]例所示. (它会将延迟的请求记录在较低的一级, 因此warn默认情况下. )要更改日志记录级别, 请使用 limit_req_log_level | limit_conn_log_level 伪指令. 在这里, 我们将拒绝的请求设置为登录warn级别(info | notice | warn | error;):

location /login/ {
    limit_req zone=mylimit burst=20 nodelay;
    limit_req_log_level warn;
    limit_conn_log_level warn;
 
    proxy_pass http://my_upstream;
}

7.2 发送给客户端的错误代码

默认情况下, 当客户端超出其速率限制时, NGINX会以状态代码 503(Service Temporarily Unavailable) 进行响应. 使用 limit_req_status | limit_conn_status 指令设置不同的状态代码(值只能设置 400 到 599 之间):

location /login/ {
    limit_req zone=mylimit burst=20 nodelay;
    limit_req_status 444;
    limit_conn_status 444;
}

7.3 拒绝所有到特定位置的请求 Denying All Requests to a Specific Location

如果要拒绝对特定URL的所有请求, 而不仅仅是限制它们, 请location为其配置一个块并包含指令:deny all

location /foo.php {
    deny all;
}

8. 其他

8.1 $remote_addr 和 $binary_remote_addr 变量

  • $remote_addr: 变量的大小可以从7至15个字节而变化. 存储状态在32位平台上占用32或64字节的内存, 在64位平台上始终占用64字节的内存.
  • $binary_remote_addr: 对于IPv4地址, 该变量的大小始终为4个字节, 对于IPv6地址, 该变量的大小始终为16个字节. 在32位平台上, 存储状态始终占据32或64字节, 在64位平台上, 存储状态始终占据64字节. 一个1兆字节的区域可以保留大约3.2万个32字节状态或大约1.6万个64字节状态. 如果区域存储空间已用完, 服务器将把错误返回 给所有其他请求.

8.2 $server_name 虚拟服务器

8.3 zone

如果区域存储已用尽, 则删除最近最少使用的状态. 如果即使在此之后仍无法创建新状态, 则该请求将以error终止.

8.4 使用变量

下面配置可以限制特定UA(比如搜索引擎)的访问:

limit_req_zone $anti_spider  zone=one:10m   rate=10r/s;
limit_req zone=one burst=100 nodelay;
if ($http_user_agent ~* "Baiduspider|googlebot|bingbot|Feedfetcher-Google") {
    set $anti_spider $http_user_agent;
}

参考