限流算法和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,可能和最初的值有不同,比如经过重定向之类的