限流(rate limiting)是Nginx众多特性中最有用的,也是经常容易被误解和错误配置的,特性之一。该特性可以限制某个用户在一个给定时间段内能够产生的HTTP请求数。请求可以简单到就是一个对于主页的GET请求或者一个登陆表格的POST请求。
限流可以用于安全目的上,通过限制请求速度来防止外部暴力扫描,或者减慢暴力密码破解攻击,可以结合日志标记出目标URL来帮助防范DDoS攻击,也可以解决流量突发的问题(如整点活动),一般地说,限流是用在保护上游应用服务器不被在同一时刻的大量用户请求湮没。
我们在访问内部域名时在1s时间内发起两个请求,操作上的一些时差会出现两种不同的结果,2个请求都返回200,或者1个请求返回200,而另外一个请求返回503,为此对Nginx限流模块进行了深入调研,Nginx在处理请求的时候是在1s内对请求的具体个数做拆分的,以2request/s为例,拆分为500ms内处理一个请求,下一个500ms才会处理第二个请求;除此之外,我们发现,在限制2request/s后,发现发送多个请求(requests>=2)也可以被处理掉,在此基础上上,调研了缓冲队列的相关配置。
1
漏桶和令牌桶算法的概念
漏桶算法(Leaky Bucket):主要目的是控制数据注入到网络的速率,平滑网络上的突发流量。漏桶算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量。漏桶算法的示意图如下图所示,请求先进入到漏桶里,漏桶以一定的速度出水,当水请求过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。
令牌桶算法(Token Bucket):是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。令牌桶算法示意图如下图所示,大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大小。
2
两种算法的区别
两者主要区别在于“漏桶算法”能够强行限制数据的传输速率,而“令牌桶算法”在能够限制数据的平均传输速率外,还允许某种程度的突发传输。在“令牌桶算法”中,只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的门限,所以它适合于具有突发特性的流量。
3
按请求速率限速
按请求速率限速是指限制IP发送请求的速率,超出指定速率后,Nginx将直接拒绝更多的请求。采用漏桶算法实现。下面从一些实验数据上来深入的了解这个模块,先简单介绍一下该模块的配置方式,如下图所示(配置需要在Nginx配置和域名配置里面同时修改),使用limit_req_zone关键字,定义一个名为tip大小为10MB的共享内存区域(zone),用来存放限速相关的统计信息,限速的key值为二进制的IP地址($binary_remote_addr),限速上限(rate)为2r/s。
将上述规则应用到/search目录(单个IP的访问速度被限制在了2请求/秒,超过这个限制的访问将直接被Nginx拒绝)。burst和nodelay的作用稍后解释。(zone=tip:10m表示会话空间的存储大小为10m)。
4
3个实验案例
实验1、讨论2个请求在1s内的执行过程
修改配置下图所示:
我们使用ab工具模拟1s发送2个请求。
只有一个请求成功了,查看了一下执行时间:
1ms内完成了所有请求,考虑到每秒两个请求可能是分时间段来来完成的,二分法做了大量延迟处理的尝试,当两个请求之间的时延大于0.5s时第二个请求才会成功。
结论:Nginx的限流统计是基于毫秒的,我们设置的速度是2r/s,转换一下就是500ms内单个IP只允许通过1个请求,从501ms开始才允许通过第二个请求。
实验2、burst允许缓存处理突发请求
如果短时间内发送了大量请求,Nginx按照毫秒级精度统计,超出限制的请求直接拒绝。这在实际场景中未免过于苛刻,真实网络环境中请求到来不是匀速的,很可能有请求“突发”的情况。Nginx考虑到了这种情况,可以通过burst关键字开启对突发请求的缓存处理,而不是直接拒绝。(类似令牌桶算法)
修改Nginx配置如下:
我们加入了burst=4,意思是每个key(此处是每个IP)最多允许4个突发请求的到来。使用ab工具发送6个请求,结果会怎样呢?
发送时间:
执行结果是请求全部被处理,加入缓冲队列后,按照之前时间轴的规则 ,在0.001s、0.501s、1.001s、1.501s、2.001s、2.501s分别发送一个请求,与之前的有些偏差,理论上数据应该是同时发送到ngnix,我们做了抓包统进行验证,可以观察到6个请求分别开辟了6个端口进行发送,只有当第一个包三次握手完成,第二个包才开始发送,时间间隔500ms,这就验证了ab工具确实是单个发送的。
另外,理论上缓冲区之外的2个请求应该只有一个是200,另外一个是503,缓冲区的4个请求是按照2r/s的速度依次处理,这里是由于ab工具本身的问题,在加入缓冲队列后,发送时间由之前的1ms内完成变成了2501ms完成,所以导致了请求都被处理掉,若是使用其他工具短时间内发送6个请求,则只能成功5个。
发送耗时2ms,完成处理时间2437ms,每个请求的处理时间。
由于ngnix 500ms处理完第一个请求后,501ms才会处理第二个请求,所以5个请求(去掉503那个)耗时500ms*4+416ms=2416ms(本地实测,不同Nginx性能有所差异),或者使用ab工具并发来处理这些请求,也会有同样的效果。
我们再来观察一下发送时间,所有的请求基本在10ms内发起,这样便导致了6个请求中,去掉第一个和缓冲区的4个,第二个被拒绝掉。
实验3、nodelay降低排队时间
通过设置burst参数,我们可以允许Nginx缓存处理一定程度的突发,多余的请求可以先放到队列里,慢慢处理,这起到了平滑流量的作用。但是如果队列设置的比较大,请求排队的时间就会比较长,这对用户很不友好。nodelay参数允许请求在排队的时候就立即被处理,也就是说只要请求能够进入burst队列,就会立即被后台worker处理。
延续实验2的配置,我们加入nodelay选项:
同样发送6个请求发送时间:
实验结果如下图所示:
处理时间:
与实验2相比直观上的效果就是Nginx同时出现6条日志,即6个请求是同时被处理的,而实验2日志是逐条生成的,间隔0.5s,视觉上有卡顿。
虽然设置burst和nodelay能够降低突发请求的处理时间,但是长期来看并不会提高吞吐量的上限,长期吞吐量的上限是由rate决定的,因为nodelay只能保证burst的请求被立即处理,加入了nodelay参数之后的限速算法还算是漏桶算法,当令牌桶算法的token为耗尽时,由于它有一个请求队列,所以会把接下来的请求缓存下来,缓存多少受限于队列大小。假如server已经过载,缓存队列越来越长,即使过了很久请求被处理了,对用户来说也没什么价值了。所以当token不够用时,最明智的做法就是直接拒绝用户的请求,即漏桶算法。