秒杀架构(二) -- nginx实现限流

限流(Rate Limitting)是服务降级的一种方式,通过限制系统的输入和输出流量以达到保护系统的目的。比如我们的网站暴露在公网环境中,除了用户的正常访问,网络爬虫、恶意攻击或者大促等突发流量都可能都会对系统造成压力,如果这种压力超出了服务器的处理能力,会造成响应过慢甚至系统崩溃的问题。因此,当并发请求数过大时,我们通过限制一部分请求(比如限制同一IP的频繁请求)来保证服务器可以正确响应另一部分的请求。
nginx 提供了两种限流方式,一种是限制请求速率,一种是限制连接数量。

1. 限制请求速率

nginx 的 ngx_http_limit_req_module 模块提供限制请求处理速率的能力,使用了漏桶算法(leaky bucket algorithm)。

我们可以想像有一只上面进水、下面匀速出水的桶,如果桶里面有水,那刚进去的水就要存在桶里等下面的水流完之后才会流出,如果进水的速度大于水流出的速度,桶里的水就会满,这时水就不会进到桶里,而是直接从桶的上面溢出。

对应到处理网络请求,水代表从客户端来的请求,而桶代表一个队列,请求在该队列中依据先进先出(FIFO)算法等待被处理。漏的水代表请求离开缓冲区并被服务器处理,溢出代表了请求被丢弃并且永不被服务。
在这里插入图片描述

1.1 配置限流

“流量限制”配置两个主要的指令,limit_req_zonelimit_req

limit_req_zone指令定义了流量限制相关的参数,而limit_req指令在出现的上下文中启用流量限制(示例中,对于”/login/”的所有请求)。

limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s;
server {
    location /login/ {
        limit_req zone=mylimit;

        proxy_pass http://my_upstream;
    }
}
1.1.1 limit_req_zone

limit_req_zone指令定义了流量限制相关的参数,格式为:limit_req_zone key zone rate;limit_req_zone指令通常在 HTTP 块中定义,使其可在多个上下文中使用,它需要以下三个参数:

  • Key:定义限流对象,$binary_remote_addr 是 nginx 中的变量,表示基于 remote_addr(客户端IP) 来做限流,binary_ 是二进制存储。使用 $binary_remote_addr 而不是 $remote_addr 是因为二进制存储可以压缩内存占用量。 $remote_addr 变量的大小从7到15个字节不等,而 $binary_remote_addr变量的大小对于 IPv4 始终为4个字节,对于 IPv6 地址则为16个字节
  • Zone:定义用于存储每个 IP 地址状态以及被限制请求 URL 访问频率的共享内存区域。保存在内存共享区域的信息,意味着可以在 Nginx 的 worker 进程之间共享。定义分为两个部分:通过zone=keyword标识区域的名字,以及冒号后面跟区域大小。myLimit:10m 表示一个大小为10M,名字为 myLimit 的内存区域。1M 能存储16000个 IP 地址的访问信息,myLimit 大概可以存储约160000个地址。nginx 创建新记录的时候,会移除前60秒内没有被使用的记录,如果释放的空间还是存储不了新的记录,会返回503的状态码。
  • Rate - 定义最大请求速率。在示例中,速率不能超过每秒 2 个请求。Nginx 实际上以毫秒的粒度来跟踪请求,所以速率限制相当于每 500 毫秒 1 个请求。因为不允许”突发情况”,这意味着在前一个请求 500 毫秒内到达的请求将被拒绝(默认返回503,如果想修改返回值,可以设置limit_req_status)。

当 Nginx 需要添加新条目时存储空间不足,将会删除旧条目。如果释放的空间仍不够容纳新记录,Nginx 将会返回 503 状态码(Service Temporarily Unavailable)。另外,为了防止内存被耗尽,Nginx 每次创建新条目时,最多删除两条 60 秒内未使用的条目

1.1.2 limit_req

limit_req_zone 只是设置限流参数,如果要生效的话,必须和 limit_req 配合使用。limit_req 的格式为:limit_req zone=name [burst=number] [nodelay]

所以需要通过添加limit_req指令,将流量限制应用在特定的 location 或者 server 块。在上面示例中,我们对/login/请求进行流量限制。

我们可以理解为这个桶目前没有任何储存水滴的能力,到达的所有不能立即漏出的请求都会被拒绝。如果我1秒内发送了10次请求,其中前500毫秒1次,后500毫秒9次,那么只有前500毫秒的请求和后500毫秒的第一次请求会响应,其余请求都会被拒绝。

在这里插入图片描述

1.2 处理突发请求

如果我们在 500 毫秒内接收到 2 个请求,怎么办?对于第二个请求,Nginx 将给客户端返回状态码 503。这可能并不是我们想要的结果,因为应用本质上趋向于突发性。相反地,我们希望==缓冲(缓存)==任何超额的请求,然后及时地处理它们。我们更新下配置,在limit_req中使用 burst 参数:

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

burst 参数定义了超出 zone 指定速率的情况下(示例中的 mylimit 区域,速率限制在每秒 2 个请求,或每 500 毫秒一个请求),客户端还能发起多少请求。上一个请求 500 毫秒内到达的请求将会被放入队列。

我们将队列大小设置为 5,如果同时有10个请求到达,nginx 会处理第1个请求,剩余9个请求中,会有5个被放入队列,剩余的4个请求会直接被拒绝。然后每隔500ms从队列中获取一个请求进行处理,此时如果后面继续有请求进来,如果队列中的请求数目超过了5,会被拒绝,不足5的时候会添加到队列中进行等待。我们可以理解为现在的桶可以存5滴水:
在这里插入图片描述

1.3 无延迟的排队

配置 burst 之后,虽然同时到达的请求不会全部被拒绝,但是仍需要等待500ms 一次的处理时间,放入桶中的第5个请求需要等待500ms * 4的时间才能被处理,更长的等待时间意味着用户的流失,在许多场景下,这个等待时间是不可接受的。此时我们需要增加 nodelay 参数,和 burst 配合使用。

location /login/ {
    limit_req zone=mylimit burst=5 nodelay;

    proxy_pass http://my_upstream;
}

使用 nodelay 参数,Nginx 仍将根据 burst 参数分配队列中的位置,并应用已配置的速率限制,而不是清理队列中等待转发的请求。相反地,当一个请求到达“太早”时,只要在队列中能分配位置,Nginx 将立即转发这个请求。将队列中的该位置标记为”taken”(占据),并且不会被释放以供另一个请求使用,直到一段时间后才会被释放(在这个示例中是,500 毫秒后)。

假设如前所述,队列中有 5 个空位,从给定的 IP 地址发出的 11个请求同时到达。Nginx会立即转发这个 11 个请求,并且标记队列中占据的 5个位置,然后每 500 毫秒释放一个位置。如果是11个请求同时到达,Nginx 将会立即转发其中的 6 个请求,标记队列中占据的 5个位置,并且返回 503 状态码来拒绝剩下的 5 个请求。

现在假设,第一组请求被转发后 501 毫秒,另 5个请求同时到达。队列中只会有一个位置被释放,所以 Nginx 转发一个请求并返回503状态码来拒绝其他 4 个请求。如果在 5个新请求到达之前已经过去了 501 毫秒,3 个位置被释放,所以 Nginx 立即转发 3个请求并拒绝另外 2个。

效果相当于每秒 2 个请求的“流量限制”。如果希望不限制两个请求间允许间隔的情况下实施“流量限制”,nodelay 参数是很实用的。

注意:对于大部分部署,我们建议使用 burst 和 nodelay 参数来配置limit_req指令。

1.4 白名单

如果遇到不需要限流的情况,比如测试要压测,可以通过配置白名单,取消限流的设置。白名单要用到 nginx 的 ngx_http_geo_module 和 ngx_http_map_module 模块。

geo $limit {
    default         1;
    10.0.0.0/8         0;
    192.168.0.0/64     0;
}
map $limit $limit_key {
    0 "";
    1 $binary_remote_addr;
}
limit_req_zone $limit_key zone=req_zone:10m rate=2r/s;
server {
    location / {
        limit_req zone=req_zone burst=5 nodelay;

        # ...
    }
}

geo 块将给在白名单中的 IP 地址对应的 $limit 变量分配一个值 0,给其它不在白名单中的分配一个值 1。然后我们使用一个映射将这些值转为 key,如下:

  • 如果变量的值是0,limit_key变量将被赋值为空字符串
  • 如果变量的值是1,limit_key变量将被赋值为客户端二进制形式的 IP 地址
  • 两个指令配合使用,白名单内 IP 地址的$limit_key变量被赋值为空字符串,不在白名单内的被赋值为客户端的 IP 地址。当limit_req_zone后的第一个参数是空字符串时,不会应用“流量限制”,所以白名单内的 IP 地址(10.0.0.0/8 和192.168.0.0/24 网段内)不会被限制。其它所有 IP 地址都会被限制到每秒 2 个请求。

limit_req指令将限制应用到 / 的location块,允许在配置的限制上最多超过 5个数据包的突发,并且不会延迟转发。

1.5 location包含多limit_req指令

我们可以在一个 location 块中配置多个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_wl,并且被限制到每秒 15 个请求。不在白名单内的 IP 地址两个限制能匹配到,所以应用限制更强的那个:每秒 5 个请求。

1.6 发送到客户端的错误代码:

一般情况下,客户端超过配置的流量限制时,Nginx 响应状态码为 503(Service Temporarily Unavailable)。可以使用limit_req_status指令来设置为其它状态码(例如下面的 444 状态码):

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

1.7 指定location拒绝所有请求

如果你想拒绝某个指定 URL 地址的所有请求,而不是仅仅对其限速,只需要在 location 块中配置 deny all 指令:

location /foo.php {
    deny all;
}

1.8 日志记录:

日志记录 默认情况下,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”

日志条目中包含的字段:

  • limiting requests - 表明日志条目记录的是被“流量限制”请求
  • excess - 每毫秒超过对应“流量限制”配置的请求数量
  • zone - 定义实施“流量限制”的区域
  • client - 发起请求的客户端 IP 地址
  • server - 服务器 IP 地址或主机名
  • request - 客户端发起的实际 HTTP 请求
  • host - HTTP 报头中 host 的值

默认情况下,Nginx 以 error 级别来记录被拒绝的请求,如上面示例中的[error]所示(Ngin 以较低级别记录延时请求,一般是 info 级别)。如要更改 Nginx 的日志记录级别,需要使用limit_req_log_level指令。这里,我们将被拒绝请求的日志记录级别设置为 warn:

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

2. 限制连接数量

nginx 的 ngx_http_limit_conn_module 模块提供限制连接数的能力,包含两个指令limit_conn_zone 和 limit_conn,格式为limit_conn_zone key zone。

limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;
server {
    location ~* \.(html)$ {
        limit_conn perip 10;
        limit_conn perserver 100;
    }
} 
  • limit_conn perip 10:key 是 $binary_remote_addr,表示限制单个IP同时最多能持有10个连接。
  • limit_conn perserver 100: key 是 $server_name,表示虚拟主机(server) 同时能处理并发连接的总数为100。

需要注意的是:只有当 request header 被后端server处理后,这个连接才进行计数。

参考:
https://mp.weixin.qq.com/s?__biz=MzI4NjE4NTUwNQ==&mid=2247494915&idx=2&sn=b37a2aca654a9b345a8dadd84e0d8b9b&chksm=ebe26c4ddc95e55b99c1b3b1c2d2969e33ecb49215f29716e6cd3e7f3101fb7019170fc647f7&scene=27

https://blog.csdn.net/qq_35760825/article/details/127596936

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值