限流(Rate Limitting)是服务降级的一种方式,通过限制系统的输入和输出流量以达到保护系统的目的。比如我们的网站暴露在公网环境中,除了用户的正常访问,网络爬虫、恶意攻击或者大促等突发流量都可能都会对系统造成压力,如果这种压力超出了服务器的处理能力,会造成响应过慢甚至系统崩溃的问题。因此,当并发请求数过大时,我们通过限制一部分请求(比如限制同一IP的频繁请求)来保证服务器可以正确响应另一部分的请求。
nginx 提供了两种限流方式,一种是限制请求速率,一种是限制连接数量。
1. 限制请求速率
nginx 的 ngx_http_limit_req_module 模块提供限制请求处理速率的能力,使用了漏桶算法(leaky bucket algorithm)。
我们可以想像有一只上面进水、下面匀速出水的桶,如果桶里面有水,那刚进去的水就要存在桶里等下面的水流完之后才会流出,如果进水的速度大于水流出的速度,桶里的水就会满,这时水就不会进到桶里,而是直接从桶的上面溢出。
对应到处理网络请求,水代表从客户端来的请求,而桶代表一个队列,请求在该队列中依据先进先出(FIFO)算法等待被处理。漏的水代表请求离开缓冲区并被服务器处理,溢出代表了请求被丢弃并且永不被服务。
1.1 配置限流
“流量限制”配置两个主要的指令,limit_req_zone
和limit_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