🍃流量限制
(rate-limiting)是nginx
最有用的功能之一,却经常被错误理解和错误配置。它允许我们限制用户在给定时间内可以发出的HTTP请求数量。例如请求网站首页的GET请求,表单登录的POST请求等。
🍃速率限制可以出于安全目的使用。例如,可以降低暴力破解账号密码的攻击速度。通过将传入请求速率限制为实际用户的典型值,并(通过记录)标识目标URL,它可以帮助我们防御DDOS攻击。而更为通用的用法是,防止上游应用程序服务器同时被太多用户请求所淹没。
1. nginx流量限制的工作原理
🍂 流量限制(rate-limiting)
使用的是漏斗算法leaky bucket algorithm
,该算法在带宽受限的情况下广泛用于电信和分组交换计算机网络中,以处理突发性问题。类比是用一个水桶,在水桶的顶部浇水,然后从底部漏水。如果倒水的速度超过漏水的速度,则水桶会溢出。在请求处理方面,水代表来自客户端的请求,存储桶代表队列,根据先进先出(FIFO)调度算法,请求等待处理。漏水表示离开缓冲区以供服务器处理的请求,溢出表示已丢弃且从未得到服务的请求。
2. nginx限流的基本配置
🍂 限流主要依靠2个指令来配置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/
的请求都将受到mylimit配置的影响) -
⚡
limit_req_zone
指令,它通常定义在http
区块中,在多个server
上下文中都可以使用,其包含3个参数,解释如下👇Key
:定义限制请求时提取的辨认特征。在范例中,我们使用的是nginx变量$binary_remote_addr
,该变量表示的是客户端IP地址的二进制表示,这意味着第三个参数:请求速率的判断条件是IP的二进制表示。(当然我们也可以使用IP的字符串表示$remote_addr
,但是它占用的空间大于二进制)zone=name:size
:定义共享内存区域大小,用于存储每个IP地址状态以及限制请求URL的访问频率。这些信息存储在nginx的共享内存中,意味着可以在nginx的worker进程间共享。name
定义共享区域名称,size
定义共享区域大小,1m
可以存储16,000个IP,范例可以存储16,0000个IP。如果
nginx
需要添加新条目时存储空间耗尽,它将删除最旧的条目。如果释放的空间仍然不足以容纳新记录,则nginx
返回503 (Service Temporarily Unavailable)状态码。除此之外,为防止内存耗尽,nginx
每次创建新条目时,都会删除过去60秒钟内未使用过的两个条目。rate=rate
:定义请求速率,范例中10r/s
代表同一IP 1秒不能超过10个请求,实际上nginx
以毫秒为单位跟踪请求,因此此限制相当于每100毫秒1个请求。因为我们没有突发事件配置,所以这意味着如果请求在前一个允许的请求之后不到100毫秒到达,则该请求将被拒绝。 -
⚡
limit_req_zone
设置的指令并不会直接生效,需要limit_req
在http
或者server
或者location
中使用配置,范例中limit_req
在location上使用了mylimit
配置,限制了在上一个请求发送后100ms接受下一个请求。
🍂 实际范例验证:
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/m;
server {
listen 80;
server_name 192.168.124.128;
location / {
limit_req zone=mylimit;
proxy_pass http://192.168.1.84:8080;
}
}
- ⚡
rate=10r/m
限定为每6秒接受一个请求,r/s
以秒为单位限流,r/m
以分钟为单位限流。 - ⚡正常请求如下:
超过速率请求,返回[root@localhost opt]# curl http://192.168.124.128/ hello, this is server
503 Service Temporarily Unavailable
错误:[root@localhost opt]# curl http://192.168.124.128/ <html> <head><title>503 Service Temporarily Unavailable</title></head> <body> <center> <h1>503 Service Temporarily Unavailable</h1> </center> <hr> <center>nginx/1.19.2</center> </body> </html>
3. 处理突发情况
🍂突发情况:rate=10r/s
配置代表100
毫秒只接受1个请求。若100
毫秒收到2个请求,对于第二个请求,nignx
将直接返回503给客户端。这并不是我们希望的,因为程序应用的请求往往不是线性的,而是突发性的。我们希望将多余的请求缓存下来,而不是粗暴的返回503错误。针对这种情况,我们在指令limit_req
上使用burst
参数,配置如下:👇
location /login/ {
limit_req zone=mylimit burst=20;
proxy_pass http://my_upstream;
}
- ⚡
burst
参数,定义了超出rate
请求速率,还可以接受多少请求放入到待处理的缓存队列中(示例mylimit,速率限制为每秒10个请求,或每100毫秒1个请求,若100毫秒内,接收了2个以上的请求,nginx会将多余的请求放入到缓存队列中)。 - ⚡对于本例配置,若nginx同一时间接收到指定IP的21个请求,第一个请求立即转发到上游服务器组,并将其余20个请求放入队列。然后,每100毫秒转发一个排队的请求,当排队的请求超过20个的时候,将返回503错误。
🍂 实际范例验证:
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=6r/m;
server {
listen 80;
server_name 192.168.124.128;
location / {
limit_req zone=mylimit burst=3;
proxy_pass http://192.168.1.84:8080;
}
}
- ⚡代表每10秒转发一个请求,最多缓存3个请求。意味着,同一IP,同一时间发送5个请求,第五个请求将会被拒绝,第一个请求被转发到上游服务,队列中排队3个,最快40秒全部处理完成。
4. 无延迟排队
🍂无延迟排队实现了如下功能,同一IP指定时间段内接收并转发固定数量的请求,超量请求将会被拒接。
🍂nodelay配置:burst可以增加接纳请求的容量,但是并不实用,因为这会使我们的网站显得很慢。在示例中rate=10r/s
,队列中第20个请求在2秒后才会被转发,这时客户端已经等待2秒了。若要解决这个情况,我们使用nodelay
参数,配合burst
参数使用:
location /login/ {
limit_req zone=mylimit burst=20 nodelay;
proxy_pass http://my_upstream;
}
- ⚡在
limit_req
指令中配置了nodelay
参数,nginx依旧会遵循rate
配置的速率限制,但是不会通过间隔排队转发来实现。相反,当请求“过早”到达时,只要队列中有可用的插槽,nginx就会立即转发该请求。并将该插槽标记为“已占用”,直到经过适当的时间(在我们的示例中为100毫秒rate=10r/s
)后,该插槽才会释放给其他请求使用。 - ⚡
burst=20 nodelay
配置代表排队队列有20个插槽为空,当有21个请求同时从给定IP地址到达。nginx立即转发所有21个请求,并将队列中的20个插槽标记为已占用,然后每100毫秒释放1个插槽。(如果有25个请求,nginx将立即转发其中的21个请求,并将20个插槽标记为已占用,并拒绝最后4个请求返回503) - ⚡现在假设在转发第一组21个请求后101毫秒,同时有20个请求同时到达。队列中只有1个插槽被释放,因此nginx转发1个请求,并拒绝其他19个的请求返回503状态码。相反,如果在20个新请求到达之前已经过去501毫秒,则5个插槽是空闲的,因此nginx立即转发5个请求并拒绝15个。
- ⚡整体效果相当于每秒10个请求的速率限制。nodelay将不限制请求间隔,则此选项很有用。
🍂 实际范例验证:
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=6r/m;
server {
listen 80;
server_name 192.168.124.128;
location / {
limit_req zone=mylimit burst=3 nodelay;
proxy_pass http://192.168.1.84:8080;
}
}
- ⚡上述配置相当于40秒,处理4个请求(10秒一个请求处理,3个队列等待,一个直接转发,(3+1)*10,若burst设置为20,则为(20+1)*10=210秒处理21个请求),每10秒释放一个队列插槽,接收一个外部请求。如果没有
nodelay
参数,从第二个请求开始客户端就开始漫长的等待返回。
❗ 对于大多数部署,我们都应当在limit_req
指令中包含burst
和nodelay
参数