Nginx 限流特技

限流(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):

主要目的是控制数据注入到网络的速率,平滑网络上的突发流量。漏桶算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量。漏桶算法的示意图如下图所示,请求先进入到漏桶里,漏桶以一定的速度出水,当水请求过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。

0bfdd75624a64a06890c68e51e03890b.png

令牌桶算法(Token Bucket):

是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。令牌桶算法示意图如下图所示,大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大小。

a7419453e4a64713b81fae4655154ab2.png

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)。

fe357f6d21954c06869022f1adee7786.png

4.3 个实验案例

实验 1、讨论 2 个请求在 1s 内的执行过程 **

修改配置下图所示:

6050ca50de0b4b62b82f6046e829fd16.png

我们使用 ab 工具模拟 1s 发送 2 个请求。

7568db0e6808450fb098163f66c3bcbc.png

只有一个请求成功了,查看了一下执行时间:

4ae1ac7631e14126954efb46dd2f70f0.png

1ms 内完成了所有请求,考虑到每秒两个请求可能是分时间段来来完成的,二分法做了大量延迟处理的尝试,当两个请求之间的时延大于 0.5s 时第二个请求才会成功。

199203ce24a240ff819eb51a7072702a.png

结论:Nginx 的限流统计是基于毫秒的,我们设置的速度是 2r/s,转换一下就是 500ms 内单个 IP 只允许通过 1 个请求,从 501ms 开始才允许通过第二个请求。

b9b84a4e90074c68ac62ecc97a593075.png

实验 2、burst 允许缓存处理突发请求

如果短时间内发送了大量请求,Nginx 按照毫秒级精度统计,超出限制的请求直接拒绝。这在实际场景中未免过于苛刻,真实网络环境中请求到来不是匀速的,很可能有请求“突发”的情况。Nginx 考虑到了这种情况,可以通过 burst 关键字开启对突发请求的缓存处理,而不是直接拒绝。(类似令牌桶算法)

修改 Nginx 配置如下:

16ab610bc3a541dd890a572b23cb8c0e.png

我们加入了 burst=4,意思是每个 key(此处是每个 IP) 最多允许 4 个突发请求的到来。使用 ab 工具发送 6 个请求,结果会怎样呢?

2cae41e705cd48f08d178732acf3d2a0.png

发送时间:

2d8e666ea26345d38209a567c878a2b5.png

执行结果是请求全部被处理,加入缓冲队列后,按照之前时间轴的规则 ,在 0.001s、0.501s、1.001s、1.501s、2.001s、2.501s 分别发送一个请求,与之前的有些偏差,理论上数据应该是同时发送到 ngnix,我们做了抓包统进行验证,可以观察到 6 个请求分别开辟了 6 个端口进行发送,只有当第一个包三次握手完成,第二个包才开始发送,时间间隔 500ms,这就验证了 ab 工具确实是单个发送的。

4a6a3ce7c7f4409496f655a898944b4a.png

另外,理论上缓冲区之外的 2 个请求应该只有一个是 200,另外一个是 503,缓冲区的 4 个请求是按照 2r/s 的速度依次处理,这里是由于 ab 工具本身的问题,在加入缓冲队列后,发送时间由之前的 1ms 内完成变成了 2501ms 完成,所以导致了请求都被处理掉,若是使用其他工具短时间内发送 6 个请求,则只能成功 5 个。

bc3988cc70864d7698a16b2695ed4f7d.png

发送耗时 2ms,完成处理时间 2437ms,每个请求的处理时间。

c816a413e786480abbb20bacb73c5d13.png

由于 ngnix 500ms 处理完第一个请求后,501ms 才会处理第二个请求,所以 5 个请求(去掉 503 那个)耗时 500ms*4+416ms=2416ms(本地实测,不同 Nginx 性能有所差异),或者使用 ab 工具并发来处理这些请求,也会有同样的效果。

6565dcb476694a869b3266151174900f.png

我们再来观察一下发送时间,所有的请求基本在 10ms 内发起,这样便导致了 6 个请求中,去掉第一个和缓冲区的 4 个,第二个被拒绝掉。

e63ec1c4b472420eaabdedbaebfb8ad0.png

实验 3、nodelay 降低排队时间

通过设置 burst 参数,我们可以允许 Nginx 缓存处理一定程度的突发,多余的请求可以先放到队列里,慢慢处理,这起到了平滑流量的作用。但是如果队列设置的比较大,请求排队的时间就会比较长,这对用户很不友好。nodelay 参数允许请求在排队的时候就立即被处理,也就是说只要请求能够进入 burst 队列,就会立即被后台 worker 处理。

延续实验 2 的配置,我们加入 nodelay 选项:

509af6a3679f454995f935a324446362.png

同样发送 6 个请求发送时间:

86b28010f0ec469db968386770f60b9c.png

实验结果如下图所示:

29148cd424674432a255813217b75987.png

处理时间:

c395d798d7e544fba0f5bcd5eb688258.png

与实验 2 相比直观上的效果就是 Nginx 同时出现 6 条日志,即 6 个请求是同时被处理的,而实验 2 日志是逐条生成的,间隔 0.5s,视觉上有卡顿。

虽然设置 burst 和 nodelay 能够降低突发请求的处理时间,但是长期来看并不会提高吞吐量的上限,长期吞吐量的上限是由 rate 决定的,因为 nodelay 只能保证 burst 的请求被立即处理,加入了 nodelay 参数之后的限速算法还算是漏桶算法,当令牌桶算法的 token 为耗尽时,由于它有一个请求队列,所以会把接下来的请求缓存下来,缓存多少受限于队列大小。假如 server 已经过载,缓存队列越来越长,即使过了很久请求被处理了,对用户来说也没什么价值了。所以当 token 不够用时,最明智的做法就是直接拒绝用户的请求,即漏桶算法。