限流算法和nginx请求限流

一、限流算法

常见的限流算法有计数器(固定窗口)、滑动窗口、漏桶、令牌桶

1、计数器(固定窗口)

最简单的限流算法,计数器限制每一分钟或者每一秒钟内请求不能超过一定的次数,在下一秒钟计数器清零重新计算

计数器限流存在一个缺陷,比如限制每分钟访问不能超过100次,客户端在第一分钟的59秒请求100次,在第二分钟的第1秒又请求了100次,那么在这2秒内后端会受到200次请求的压力,形成了流量突刺

2、滑动窗口

滑动窗口其实是细分后的计数器,它将每个时间窗口又细分成若干个时间片段,每过一个时间片段,整个时间窗口就会往右移动一格

比如限制每分钟访问不能超过100次,如图每分钟被分成了4个时间片段,每个时间片段15秒,假设客户端在第一分钟的50秒请求了100次,时间到了第二分钟的10秒,时间窗口向右滑动一格,这时这个时间窗口其实已经打满了100次,客户端将被拒绝访问

时间窗口划分的越细,滑动窗口的滚动就越平滑,限流的效果就会越精确

3、漏桶

漏桶算法类似一个限制出水速度的水桶,通过一个固定大小FIFO队列+定时取队列元素的方式实现,请求进入队列后会被匀速的取出处理(桶底部开口匀速出水),当队列被占满后后来的请求会直接拒绝(水倒的太快从桶中溢出来)

漏桶桶的优点是可以削峰填谷,不论请求多大多快,都只会匀速发给后端,不会出现突刺现象,保证下游服务正常运行

缺点就是在桶队列中的请求会排队,响应时间拉长

4、令牌桶

令牌桶算法是以一个恒定的速度往桶里放置令牌(如果桶里的令牌满了就废弃),每进来一个请求去桶里找令牌,有的话就拿走令牌继续处理,没有就拒绝请求

令牌桶的优点是可以应对突发流量,当桶里有令牌时请求可以快速的响应,也不会产生漏桶队列中的等待时间

缺点就是相对漏桶一定程度上减小了对下游服务的保护

 

二、nginx请求限流(ngx_http_limit_req_module)

对于nginx接入层限流可以使用nginx自带的两个模块:连接数限流模块ngx_http_limit_conn_module和漏桶算法实现的请求限流模块ngx_http_limit_req_module,还可以使用OpenResty提供的Lua限流模块lua-resty-limit-traffic进行更复杂的限流场景

本文只介绍请求限流模块ngx_http_limit_req_module,主要的指令是limit_req_zone和limit_req

1、指令介绍

(1)limit_req_zone

设置共享内存区域的大小和请求的速率

语法:limit_req_zone key zone=name:size rate=rate;
位置:http
版本:1.7.6之前key只可包含一个变量
示例:limit_req_zone $binary_remote_addr zone=test123:10m rate=10r/s;

key

定义要限流的对象,通常是nginx内置变量,多个key可以用逗号分隔,示例中$binary_remote_addr是限制每个ip的请求速率

一般有$binary_remote_addr(客户的ip)、$server_name(服务器名称)、$uri(不带参数的请求地址)、$request_uri(带参数的请求地址),更多变量可以在nginx包的"\src\http\ngx_http_variables.c"文件中查看,或者查看本文的最后

zone

定义存放限流信息的共享内存区域,记录每类客户端的访问频率,在worker进程间共享,size表示区域大小

比如使用$binary_remote_addr的情况,“binary_”表示内存占用量经过缩减,IPv4固定占用4字节、IPv6固定占用16字节,在32位系统,每一个IP在32位系统将占用64字节、在64位系统将占用128字节来保存状态,1m空间在32位系统能保存1w6多个IP的状态,在64位系统能保存8k多个IP的状态

当内存空间耗尽时nginx使用lru算法淘汰最长时间未使用的key,如果释放的空间仍不足以容纳新记录,nginx将直接限制请求返回状态码,所以需要提前预估key的数量分配合理的内存空间,避免指定的内存空间被耗尽

rate

设置最大请求速率,在示例中速率不能超过每秒10个请求,nginx以毫秒粒度跟踪请求,因此实际上是限制每100ms1个请求

如果希望限制每分钟可以指定“r/m”

limit_req_zone指令只是定义了共享区域和速率的参数,实际并没有限制请求,需要在server或者location中设置limit_req来搭配使用

(2)limit_req

设置所属共享区域名称和请求最大突发大小,并在指令出现的上下文中启用速率限制

语法:limit_req zone=name [burst=number] [nodelay | delay=number];
位置:http, server, location
版本:1.15.7后可以使用delay参数
示例:limit_req zone=test123 burst=5;

zone:和需要对应的limit_req_zone内存区域名称一致

burst:可选参数,设置允许突发请求的数量

nodelay:无延迟排队

delay:分段限速

burst、nodelay、delay参数不同的组合可以产生4种限流效果,在下一节限流效果演示中会逐一说明

指令可以叠加使用,示例中配置了单个ip地址的处理速度,同时限制了整个服务的处理速度

limit_req_zone $binary_remote_addr zone=perip:10m rate=1r/s;
limit_req_zone $server_name zone=perserver:10m rate=10r/s;

server {
    ...
    limit_req zone=perip burst=5 nodelay;
    limit_req zone=perserver burst=10;
}

将基本速率限制与其他nginx功能结合使用,可以实现更细微的流量限制,比如搭配geo和map指令可以实现对来自不在“白名单”上的任何人的请求施加速率限制:

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;
 
        # ...
    }
}
(3)limit_req_log_level

设置速率超出而拒绝请求或延迟请求处理的日志记录级别

语法:limit_req_log_level info | notice | warn | error;
默认:error
位置:http, server, location
版本:该指令出现在版本0.8.18以后

延迟请求比拒绝请求第一个等级,比如配置的是error,拒绝请求日志记录为error,延迟请求日志记录为warn

(4)limit_req_status

设置响应被拒绝请求的状态码

语法:limit_req_status code;
默认:503
位置:http, server, location
版本:该指令出现在1.3.15版以后
(5)limit_req_dry_run

启用空运行模式,开启后请求速率不受限制,但在共享内存区域中请求的数量将照常计算

语法:limit_req_dry_run on | off;
默认:off	
位置:http, server, location
版本:该指令出现在1.17.1版以后

2、限流效果演示

(1)无burst的情况

 没有配置burst桶容量,桶容量为0,按照固定速率处理请求,如果请求被限流,直接返回503

limit_req_zone $server_name zone=test123:10m rate=50r/s;
limit_req zone=test123;
jmeter线程数1,次数20

可以看到请求每隔20ms成功一次

(2)burst的情况

配置了burst桶容量,没有配置nodelay就是延迟模式,来不及处理的请求会进入桶中,桶内的请求会以固定速率被处理,如果桶满了,新进入的请求被限流

limit_req_zone $server_name zone=test123:10m rate=2r/s;
limit_req zone=test123 burst=3;
jmeter线程数6,一起请求2次,2批间隔300ms

速率为500ms成功一次,设置了burst桶容量为3,相当于一个长度3的缓冲队列

我们的预期是当同时有6个请求到达时,nginx将第1个请求立即处理,并将其余3个请求放入桶队列,然后它每500毫秒处理一个排队的请求,在请求使排队请求的数量超过3时返回503到客户端

第一次6个请求进入后,请求1-1第一个被执行,请求1-6、1-3、1-4幸运的进入桶队列中等待匀速执行,看到这4个请求间隔500ms,请求1-5、1-2因为来不及处理且桶满了被限流

第二次6个请求在最后一个请求1-4执行完后间隔300ms进入,这时距离下一次还不到500ms,所以新的请求得先进入桶队列中等待,看到请求1-3、1-5、1-6幸运的进入桶队列中,请求1-4、1-2被限流

如果2批请求间隔600ms呢?

那第二批请求将会成功4个,和第一批的情况一样

(3)burst+nodelay的情况

配置了burst桶容量,同时配置了nodelay就是非延迟模式,桶队列是一个有状态的插槽队列,当请求“过早”到达时,只要桶队列中有可用的插槽,nginx就会立即处理请求,并将该插槽标记为“已占用”,当某一次限流间隔过后没有请求时,该插槽就会被标记为“可用”

这种逻辑和令牌桶非常像,只要桶的插槽没有被占用完,突发的请求就能迅速被处理,不用像延迟模式一样需要进入队列排队等待,在流量洪峰过去后插槽可以慢慢被恢复,类似令牌慢慢被填充满桶

limit_req_zone $server_name zone=test123:10m rate=2r/s;
limit_req zone=test123 burst=3 nodelay;
jmeter线程数6,一起请求2次,2批间隔600ms

速率为500ms成功一次,设置了burst桶容量为3,相当于有3个插槽可用

我们的预期是当同时有6个请求到达时,nginx将第1个请求立即处理,同时也立即处理之后3个请求,同时将桶中的3个插槽标记占用,将其他2个请求限流,在第二批请求间隔600ms到达后,有一个请求被处理

第一次6个请求进入后,请求1-6第一个被执行,请求1-4、1-1、1-5幸运的使用了桶队列中的插槽被执行,看到这4个请求没有等待时间都是立即执行,请求1-3、1-2因为来不及处理且桶插槽用完被限流

第二次6个请求在第一批请求执行完后间隔600ms进入,这时距离下一次间隔超过了500ms,一个插槽被重置,请求1-5进入后幸运的使用了这个插槽被执行,其他5个请求因为来不及处理且桶插槽用完被限流

再看一下复杂一点的情况:

limit_req_zone $server_name zone=test123:10m rate=2r/s;
limit_req zone=test123 burst=3 nodelay;
jmeter线程数6,一起请求7次,分别间隔1000ms、300ms、300ms、300ms、1500ms、2000ms

一样的参数,线程请求7次,每批分别间隔1000ms、300ms、300ms、300ms、1500ms、2000ms

第一批序号1-6同第一个例子

第二批序号7-12,因为这次间隔是1000ms,所以有2个插槽被重置,成功了2个请求

第三批序号12-18,因为间隔300ms太短,没有到500ms间隔,请求全部限流

第四批序号19-24,序号1的第一个请求是51.815执行的,每隔500ms恢复一个插槽的话,第3次恢复是在53.315,刚好53.447序号19的请求1-1拿到了这个插槽,之后其他请求被限流

第五批序号25-30,间隔300ms,因为上一批53.447刚用掉插槽,下一个插槽恢复是在53.815(51.815+4*500ms),这一批是53.754没有到时间,所以请求全部被限流

第六批序号31-36,间隔了1500ms,到55.261已经恢复了3个插槽了(53.815,54.315,54.815),所以成功了3个请求

第七批序号37-42,间隔了2000ms,到57.307恢复了4个插槽(55.315,55.815,56.315,56.815),所以成功了4个请求

(4)burst+delay的情况

配置了burst桶容量和delay参数后,就是部分延迟模式,比如burst=12,delay=8,则桶的前8位是插槽队列,后4位是缓冲队列

假设有这样的配置:

limit_req_zone $server_namezone=test123:10m rate=5r/s;
limit_req zone=test123 burst=12 delay=8;

该配置最多允许12个突发请求,其中前8个突发请求将被立即处理,后4个请求被强制以5 r / s的匀速执行,在缓冲队列空出之前多于12个的请求被限流

使用此配置后,以8 r / s连续发出请求流的客户端将表现为图中的情况

通过测试可以发现nginx会先等待缓冲队列清空后再恢复插槽队列

limit_req_zone $server_name zone=test123:10m rate=2r/s;
limit_req zone=test123 burst=6 delay=4;
jmeter线程数10,一起请求2次,2批间隔300ms

速率为500ms成功一次,设置了burst桶容量为6,delay为4,相当于有4个插槽可用,附带一个长度为2的缓冲队列

我们的预期是第一批10个请求进来立即执行1+4个请求,有2个请求进入队列缓慢执行,执行完后间隔300ms,第二批10个请求进来,有2个请求进入空出的缓冲队列,其他8个请求限流(因为300ms,没有到500ms的间隔,插槽没有来得及恢复)

第一批10个请求进入后,前5个请求立即被执行(4个使用了插槽),请求1-10、1-1进入缓冲队列匀速执行,可以看到请求间隔了500ms,其他3个请求既没有使用插槽,也没有进缓冲队列,被限流

第二批10个请求间隔300ms进入,这时虽然缓冲队列是空的,但是插槽来不及恢复,所以只有2个缓冲队列的位置可用,所以看到请求1-6、1-8进入了队列匀速执行

如果把两批请求的间隔延长一些呢?

limit_req_zone $server_name zone=test123:10m rate=2r/s;
limit_req zone=test123 burst=6 delay=4;
jmeter线程数10,一起请求2次,2批间隔1600ms

间隔改为1600ms,足够3个插槽恢复了

可以看到表现和预期的一样,和间隔300ms的图唯一区别,序号11-13的3个请求拿到了恢复的3个插槽,立刻被执行了

3、附nginx内置变量

$args                      请求中的参数;
$binary_remote_addr        远程地址的二进制表示
$body_bytes_sent           已发送的消息体字节数
$content_length            HTTP请求信息里的"Content-Length"
$content_type              请求信息里的"Content-Type"
$document_root             针对当前请求的根路径设置值
$document_uri              与$uri相同
$host                      请求信息中的"Host",如果请求中没有Host行,则等于设置的服务器名;    
$http_cookie               cookie 信息 
$http_referer              来源地址
$http_user_agent           客户端代理信息
$http_via                  最后一个访问服务器的Ip地址
$http_x_forwarded_for      相当于网络访问路径。    
$limit_rate                对连接速率的限制          
$remote_addr               客户端地址
$remote_port               客户端端口号
$remote_user               客户端用户名,认证用
$request                   用户请求信息
$request_body              用户请求主体
$request_body_file         发往后端的本地文件名称      
$request_filename          当前请求的文件路径名
$request_method            请求的方法,比如"GET"、"POST"等
$request_uri               请求的URI,带参数   
$server_addr               服务器地址,如果没有用listen指明服务器地址,使用这个变量将发起一次系统调用以取得地址(造成资源浪费)
$server_name               请求到达的服务器名
$server_port               请求到达的服务器端口号
$server_protocol           请求的协议版本,"HTTP/1.0"或"HTTP/1.1"
$uri                       请求的URI,可能和最初的值有不同,比如经过重定向之类的
posted @ 2020-02-23 21:13  syxsdhy  阅读(1824)  评论(0编辑  收藏  举报