nginx 设置限制单个 ip 请求速率

最近遇到电商网站突然流量激增,流量穿透 nginx 到后端,导致后端服务器 502,本以为是突发流量没太重视,经过日志分析发现是单个 ip 在1分钟内高并发请求 1200 次所致。

这种行为已经不是正常的爬虫,而是恶意攻击行为,那么 nginx 可否对这种攻击行为进行限制呢?

一、限速模块

Nginx 有三种类型的模块用来限速,且各有其特点:

  1. limit_conn_zone 模块 - 限制同一 IP 地址并发连接数;
  2. limit_request 模块 - 限制同一 IP 某段时间的访问量;
  3. core 模块提供 - limit_rate 限制同一 IP 流量。

以上三个功能都是 Nginx 的内置模块,配置简单,开箱即用,今天主要说下第二点,limit_request, 通过限制单个 ip 的请求次数来达到防止单 ip 的高频词访问行为。

二、配置频率限制

频率限制主要有2个主要指令,limit_req_zone 和limit_req, 示例如下:

limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/sm;
server {
    location /login/ {
        limit_req zone=mylimit;
        proxy_pass http://my_upstream;
    }
}

limit_req_zone指令定义了速度限制的参数,同时在出现的上下文中启用速率限制,通常定义在HTTP块中,这样可以用于多个上下文,它包含3个参数:

Key - 定义应用限制的请求特征。 在这个例子中,它是 Nginx 变量 $binary_remote_addr ,它保存着客户端 IP 地址的二进制表示。 这意味着我们将每个唯一的IP地址限制为由第三个参数定义的请求速率。

Zone - 定义用于存储每个IP地址状态的共享内存区域以及访问请求受限URL的频率。 将信息保存在共享内存中意味着它可以在Nginx工作进程之间共享。

定义有两个部分: zone=keyword 标识的区域名称和冒号后面的大小。 大约16,000个IP地址的状态信息需要1兆字节,所以我们的区域可以存储大约160,000个地址。 如果Nginx需要添加一个新条目时,存储空间将被耗尽,它将删除最旧的条目。

如果释放的空间不足以容纳新记录,则 Nginx返回状态码503 (Temporarily Unavailable) 。 此外,为了防止内存耗尽,每当Nginx创建一个新条目时,最多可以删除两个在前60秒内没有使用的条目。

Rate - 设置最大请求率。 在这个例子中,速率不能超过每秒 10 个请求。 Nginx实际上以毫秒粒度跟踪请求,所以这个限制对应于每 100 毫秒 1 个请求。 由于我们不允许爆发,这意味着如果请求在前一个允许的时间之后小于 100 毫秒时被拒绝。

limit_req_zone 指令为速率限制和共享内存区域设置参数,但实际上并不限制请求速率。

因此,需要通过在其中包含 limit_req 指令来将限制应用于特定 location 或 server 块。 在这个例子中,我们是对 /login/ 的URI速率限制请求。

现在每个唯一的IP地址被限制,/login/每秒10个请求 - 或者更确切地说,在前一个100毫秒内不能请求该URL。

三、处理并发

如果我们在 100 毫秒内得到两个请求会怎么样? 对于第二个请求,Nginx将状态码503返回给客户端。 这可能不是我们想要的,因为应用程序本质上是突发性的。

相反,我们想要缓冲任何多余的请求并及时提供服务。 这时我们使用 burst 参数 limit_req,在这个更新的配置:

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

burst 参数定义了客户端可以超过区域指定的速率(使用我们的示例mylimit区域,速率限制为每秒10个请求,或每100毫秒1个)可以产生多少个请求。

在前一个请求到达100毫秒后的请求被放入一个队列中,这里我们将队列大小设置为20。

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

四、无延迟队列

具有 burst 的配置会导致流量畅通,但不是很实用,因为它可能会使您的网站显得很慢。

在我们的例子中,队列中的第 20 个数据包等待 2 秒钟被转发,此时对其的响应可能对客户端不再有用。 要解决这种情况,请将nodelay 参数与 burst 参数一起添加:

location /login/ {
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 选项非常有用。

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

添加新评论