Nginx主要有两种限速方式:按连接数限速(ngx_http_limit_conn_module)、按请求速率限速(ngx_http_limit_req_module)。我们着重讲解按请求速率限速。
【按连接数限速】
按毫秒级统计毫秒级统计毫秒级统计毫秒级统计毫秒级统计毫秒级统计毫秒级统计连接数限速是指限制单个IP(或者其他的key)同时发起的连接数,超出这个限制后,Nginx将直接拒绝更多的连接。这个模块的配置比较好理解,详见ngx_http_limit_conn_module官方文档。
【按请求速率限速】
按请求速率限速是指限制单个IP(或者其他的key)发送请求的速率,超出指定速率后,Nginx将直接拒绝更多的请求。采用leaky bucket算法实现。为深入了解这个模块,我们先从实验现象说起。开始之前我们先简单介绍一下该模块的配置方式,以下面的配置为例:
http { limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s; ... server { ... location /search/ { limit_req zone=mylimit burst=4 nodelay; }
使用limit_req_zone关键字,我们定义了一个名为mylimit大小为10MB的共享内存区域(zone),用来存放限速相关的统计信息,限速的key值为二进制的IP地址($binary_remote_addr),限速上限(rate)为2r/s;接着我们使用limit_req关键字将上述规则作用到/search/上。burst和nodelay的作用稍后解释。
使用上述规则,对于/search/目录的访问,单个IP的访问速度被限制在了2请求/秒,超过这个限制的访问将直接被Nginx拒绝。
【测试一、毫秒级统计】
... limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s; server { location / { limit_req zone=mylimit; } } ...
上述规则限制了每个IP访问的速度为2r/s,并将该规则作用于跟目录。如果单个IP在非常短的时间内并发发送多个请求,结果会怎样呢?
# 单个IP 10ms内并发发送6个请求send 6 requests in parallel, time cost: 2 ms HTTP/1.1 503 Service Temporarily Unavailable HTTP/1.1 200 OK HTTP/1.1 503 Service Temporarily Unavailable HTTP/1.1 503 Service Temporarily Unavailable HTTP/1.1 503 Service Temporarily Unavailable HTTP/1.1 503 Service Temporarily Unavailable end, total time cost: 461 ms
我们使用单个IP在10ms内发并发送了6个请求,只有1个成功,剩下的5个都被拒绝。我们设置的速度是2r/s,为什么只有1个成功呢,是不是Nginx限制错了?当然不是,是因为Nginx的限流统计是基于毫秒的,我们设置的速度是2r/s,转换一下就是500ms内单个IP只允许通过1个请求,从501ms开始才允许通过第二个请求。
【测试二、burst允许缓存处理突发请求】
测试一我们看到,我们短时间内发送了大量请求,Nginx按照毫秒级精度统计,超出限制的请求直接拒绝。这在实际场景中未免过于苛刻,真实网络环境中请求到来不是匀速的,很可能有请求“突发”的情况,也就是“一股子一股子”的。Nginx考虑到了这种情况,可以通过burst关键字开启对突发请求的缓存处理,而不是直接拒绝。
... limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s; server { location / { limit_req zone=mylimit burst=4; } } ...
我们加入了burst=4,意思是每个key(此处是每个IP)最多允许4个突发请求的到来。如果单个IP在10ms内发送6个请求,结果会怎样呢?
# 单个IP 10ms内发送6个请求,设置burstsend 6 requests in parallel, time cost: 2 ms HTTP/1.1 200 OK HTTP/1.1 503 Service Temporarily Unavailable HTTP/1.1 200 OK HTTP/1.1 200 OK HTTP/1.1 200 OK HTTP/1.1 200 OK end, total time cost: 2437 ms
相比测试一成功数增加了4个,这个我们设置的burst数目是一致的。具体处理流程是:1个请求被立即处理,4个请求被放到burst队列里,另外一个请求被拒绝。通过burst参数,我们使得Nginx限流具备了缓存处理突发流量的能力。
但是请注意:burst的作用是让多余的请求可以先放到队列里,慢慢处理。如果不加nodelay参数,队列里的请求不会立即处理,而是按照rate设置的速度,以毫秒级精确的速度慢慢处理。
【测试三、nodelay降低排队时间】
测试二中我们看到,通过设置burst参数,我们可以允许Nginx缓存处理一定程度的突发,多余的请求可以先放到队列里,慢慢处理,这起到了平滑流量的作用。但是如果队列设置的比较大,请求排队的时间就会比较长,用户角度看来就是RT变长了,这对用户很不友好。有什么解决办法呢?nodelay参数允许请求在排队的时候就立即被处理,也就是说只要请求能够进入burst队列,就会立即被后台worker处理,请注意,这意味着burst设置了nodelay时,系统瞬间的QPS可能会超过rate设置的阈值。nodelay参数要跟burst一起使用才有作用。
延续测试二的配置,我们加入nodelay选项:
... limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s; server { location / { limit_req zone=mylimit burst=4 nodelay; } } ...
测试二:只设置burst,测试三:设置burst和nodelay。结果如下
# 单个IP 10ms内发送6个请求 send 6 requests, time cost: 4 ms | time cost: 2 ms HTTP/1.1 200 OK | HTTP/1.1 200 OK HTTP/1.1 200 OK | HTTP/1.1 503 ... HTTP/1.1 200 OK | HTTP/1.1 200 OK HTTP/1.1 200 OK | HTTP/1.1 200 OK HTTP/1.1 503 ... | HTTP/1.1 200 OK HTTP/1.1 200 OK | HTTP/1.1 200 OK total time cost: 465 ms | total time cost: 2437 ms
跟测试二相比,请求成功率没变化,但是总体耗时变短了。这怎么解释呢?
测试二中,有4个请求被放到burst队列当中,工作进程每隔500ms(rate=2r/s)取一个请求进行处理,最后一个请求要排队2s才会被处理;测试三中,请求放入队列跟测试二是一样的,但不同的是,队列中的请求同时具有了被处理的资格,所以测试三中的5个请求可以说是同时开始被处理的,花费时间自然变短了。
但是请注意,虽然设置burst和nodelay能够降低突发请求的处理时间,但是长期来看并不会提高吞吐量的上限,长期吞吐量的上限是由rate决定的,因为nodelay只能保证burst的请求被立即处理,但Nginx会限制队列元素释放的速度,就像是限制了令牌桶中令牌产生的速度。
看到这里你可能会问,加入了nodelay参数之后的限速算法,到底算是哪一个“桶”,是漏桶算法还是令牌桶算法?当然还算是漏桶算法。考虑一种情况,令牌桶算法的token为耗尽时会怎么做呢?由于它有一个请求队列,所以会把接下来的请求缓存下来,缓存多少受限于队列大小。但此时缓存这些请求还有意义吗?如果server已经过载,缓存队列越来越长,RT越来越高,即使过了很久请求被处理了,对用户来说也没什么价值了。所以当token不够用时,最明智的做法就是直接拒绝用户的请求,这就成了漏桶算法
【根据IP限制】
location /{ limit_conn one 2; limit_rate 16k; limit_req zone=allips burst=4 nodelay; }
限制请求数(limit_req)、限制连接数(limit_conn)和限制响应速度(limit_rate)。
登录后可发表评论