Nginx限速模块分为哪几种?按请求速率限速的burst和nodelay参数是什么意思?漏桶算法和令牌桶算法究竟有什么不一样?本文将带你一探究竟。咱们会经过一些简单的示例展现Nginx限速模块是如何工做的,而后结合代码讲解其背后的算法和原理。html
在探究Nginx限速模块以前,咱们先来看看网络传输中经常使用两个的流量控制算法:漏桶算法和令牌桶算法。这两只“桶”到底有什么异同呢?node
漏桶算法(leaky bucket)算法思想如图所示:nginx
一个形象的解释是:git
这个算法的核心是:缓存请求、匀速处理、多余的请求直接丢弃。github
令牌桶(token bucket)算法思想如图所示:算法
算法思想是:shell
相比漏桶算法,令牌桶算法不一样之处在于它不但有一只“桶”,还有个队列,这个桶是用来存放令牌的,队列才是用来存放请求的。缓存
从做用上来讲,漏桶和令牌桶算法最明显的区别就是是否容许突发流量(burst)的处理,漏桶算法可以强行限制数据的实时传输(处理)速率,对突发流量不作额外处理;而令牌桶算法可以在限制数据的平均传输速率的同时容许某种程度的突发传输。网络
Nginx按请求速率限速模块使用的是漏桶算法,即可以强行保证请求的实时处理速度不会超过设置的阈值。数据结构
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开始才容许经过第二个请求。
实验1咱们看到,咱们短期内发送了大量请求,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个请求,设置burst send 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
相比实验1成功数增长了4个,这个咱们设置的burst数目是一致的。具体处理流程是:1个请求被当即处理,4个请求被放到burst队列里,另一个请求被拒绝。经过burst
参数,咱们使得Nginx限流具有了缓存处理突发流量的能力。
可是请注意:burst的做用是让多余的请求能够先放到队列里,慢慢处理。若是不加nodelay参数,队列里的请求不会当即处理,而是按照rate设置的速度,以毫秒级精确的速度慢慢处理。
实验2中咱们看到,经过设置burst参数,咱们能够容许Nginx缓存处理必定程度的突发,多余的请求能够先放到队列里,慢慢处理,这起到了平滑流量的做用。可是若是队列设置的比较大,请求排队的时间就会比较长,用户角度看来就是RT变长了,这对用户很不友好。有什么解决办法呢?nodelay
参数容许请求在排队的时候就当即被处理,也就是说只要请求可以进入burst队列,就会当即被后台worker处理,请注意,这意味着burst设置了nodelay时,系统瞬间的QPS可能会超过rate设置的阈值。nodelay
参数要跟burst
一块儿使用才有做用。
延续实验2的配置,咱们加入nodelay选项:
... limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s; server { location / { limit_req zone=mylimit burst=4 nodelay; } } ...
单个IP 10ms内并发发送6个请求,结果以下:
# 单个IP 10ms内发送6个请求 实验3, 设置burst和nodelay | 实验2, 只设置burst 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
跟实验2相比,请求成功率没变化,可是整体耗时变短了。这怎么解释呢?实验2中,有4个请求被放到burst队列当中,工做进程每隔500ms(rate=2r/s)取一个请求进行处理,最后一个请求要排队2s才会被处理;实验3中,请求放入队列跟实验2是同样的,但不一样的是,队列中的请求同时具备了被处理的资格,因此实验3中的5个请求能够说是同时开始被处理的,花费时间天然变短了。
可是请注意,虽然设置burst和nodelay可以下降突发请求的处理时间,可是长期来看并不会提升吞吐量的上限,长期吞吐量的上限是由rate决定的,由于nodelay只能保证burst的请求被当即处理,但Nginx会限制队列元素释放的速度,就像是限制了令牌桶中令牌产生的速度。
看到这里你可能会问,加入了nodelay参数以后的限速算法,到底算是哪个“桶”,是漏桶算法仍是令牌桶算法?固然还算是漏桶算法。考虑一种状况,令牌桶算法的token为耗尽时会怎么作呢?因为它有一个请求队列,因此会把接下来的请求缓存下来,缓存多少受限于队列大小。但此时缓存这些请求还有意义吗?若是server已通过载,缓存队列愈来愈长,RT愈来愈高,即便过了好久请求被处理了,对用户来讲也没什么价值了。因此当token不够用时,最明智的作法就是直接拒绝用户的请求,这就成了漏桶算法,哈哈~
通过上面的示例,咱们队请求限速模块有了必定的认识,如今咱们深刻剖析代码实现。按请求速率限流模块ngx_http_limit_req_module
代码位于src/http/modules/ngx_http_limit_req_module.c,900多好代码可谓短小精悍。相关代码有两个核心数据结构:
这两个关键对象存储在ngx_http_limit_req_shctx_t
中:
typedef struct { ngx_rbtree_t rbtree; /* red-black tree */ ngx_rbtree_node_t sentinel; /* the sentinel node of red-black tree */ ngx_queue_t queue; /* used to expire info(LRU algorithm) */ } ngx_http_limit_req_shctx_t;
其中除了rbtree和queue以外,还有一个叫作sentinel的变量,这个变量用做红黑树的NIL节点。
该模块的核心逻辑在函数ngx_http_limit_req_lookup()
中,这个函数主要流程是怎样呢?对于每个请求:
流程很清晰,可是代码中牵涉到红黑树、LRU队列等高级数据结构,是否是会写得很复杂?好在Nginx做者功力深厚,代码写得简洁易懂,哈哈~
// 漏桶算法核心流程 ngx_http_limit_req_lookup(...){ while (node != sentinel) { // search rbtree if (hash < node->key) { node = node->left; continue;} // 1. 从根节点开始查找红黑树 if (hash > node->key) { node = node->right; continue;} rc = ngx_memn2cmp(key->data, lr->data, key->len, (size_t) lr->len); if (rc == 0) {// found ngx_queue_remove(&lr->queue); // 2. 修改该点在LRU队列中的位置,表示该点最近被访问过 ngx_queue_insert_head(&ctx->sh->queue, &lr->queue);// 2 ms = (ngx_msec_int_t) (now - lr->last); excess = lr->excess - ctx->rate * ngx_abs(ms) / 1000 + 1000; // 3. 执行漏桶算法 if (excess < 0) excess = 0; if ((ngx_uint_t) excess > limit->burst) return NGX_BUSY; // 超过了突发门限,拒绝 if (account) {// 是不是最后一条规则 lr->excess = excess; lr->last = now; return NGX_OK; // 未超过限制,经过 } ... return NGX_AGAIN; // 6. 执行下一条限流规则 } node = (rc < 0) ? node->left : node->right; // 1 } // while ... // not found ngx_http_limit_req_expire(ctx, 1); // 4. 根据LRU淘汰,腾出空间 node = ngx_slab_alloc_locked(ctx->shpool, size); // 5. 生成新的红黑树节点 ngx_rbtree_insert(&ctx->sh->rbtree, node);// 5. 插入该节点,从新平衡红黑树 ngx_queue_insert_head(&ctx->sh->queue, &lr->queue); if (account) { lr->last = now; lr->count = 0; return NGX_OK; } ... return NGX_AGAIN; // 6. 执行下一条限流规则 }
代码有三种返回值,它们的意思是:
上述代码不难理解,但咱们还有几个问题:
LRU算法的实现很简单,若是一个节点被访问了,那么就把它移到队列的头部,当空间不足须要淘汰节点时,就选出队列尾部的节点淘汰掉,主要体如今以下代码中:
ngx_queue_remove(&lr->queue); // 2. 修改该点在LRU队列中的位置,表示该点最近被访问过 ngx_queue_insert_head(&ctx->sh->queue, &lr->queue);// 2 ... ngx_http_limit_req_expire(ctx, 1); // 4. 根据LRU淘汰,腾出空间
漏桶算法的实现也比咱们想象的简单,其核心是这一行公式excess = lr->excess - ctx->rate * ngx_abs(ms) / 1000 + 1000
,这样代码的意思是:excess表示当前key上遗留的请求数,本次遗留的请求数 = 上次遗留的请求数 - 预设速率 X 过去的时间 + 1。这个1表示当前这个请求,因为Nginx内部表示将单位缩小了1000倍,因此1个请求要转换成1000。
excess = lr->excess - ctx->rate * ngx_abs(ms) / 1000 + 1000; // 3. 执行漏桶算法 if (excess < 0) excess = 0; if ((ngx_uint_t) excess > limit->burst) return NGX_BUSY; // 超过了突发门限,拒绝 if (account) { // 是不是最后一条规则 lr->excess = excess; lr->last = now; return NGX_OK; // 未超过限制,经过 } ... return NGX_AGAIN; // 6. 执行下一条限流规则
上述代码受限算出当前key上遗留的请求数,若是超过了burst,就直接拒绝;因为Nginx容许多条限速规则同时起做用,若是已经是最后一条规则,则容许经过,不然执行下一条规则。
没有单个key相关的burst队列。上面代码中咱们看到当到达最后一条规则时,只要excess<limit->burst
限速模块就会返回NGX_OK,并无把多余请求放入队列的操做,这是由于Nginx是基于timer来管理请求的,当限速模块返回NGX_OK时,调度函数会计算一个延迟处理的时间,同时把这个请求放入到共享的timer队列中(一棵按等待时间从小到大排序的红黑树)。
ngx_http_limit_req_handler(ngx_http_request_t *r) { ... for (n = 0; n < lrcf->limits.nelts; n++) { ... ngx_shmtx_lock(&ctx->shpool->mutex);// 获取锁 rc = ngx_http_limit_req_lookup(limit, hash, &key, &excess, // 执行漏桶算法 (n == lrcf->limits.nelts - 1)); ngx_shmtx_unlock(&ctx->shpool->mutex);// 释放锁 ... if (rc != NGX_AGAIN) break; } ... delay = ngx_http_limit_req_account(limits, n, &excess, &limit);// 计算当前请求须要的延迟时间 if (!delay) { return NGX_DECLINED;// 不须要延迟,交给后续的handler进行处理 } ... ngx_add_timer(r->connection->write, delay);// 不然将请求放到定时器队列里 return NGX_AGAIN; // the request has been successfully processed, the request must be suspended until some event. http://www.nginxguts.com/2011/01/phases/ }
咱们看到ngx_http_limit_req_handler()
调用了函数ngx_http_limit_req_lookup()
,并根据其返回值决定如何操做:或是拒绝,或是交给下一个handler处理,或是将请求放入按期器队列。当限速规则都经过后,该hanlder经过调用函数ngx_http_limit_req_account()
得出当前请求须要的延迟时间,若是不须要延迟,就将请求交给后续的handler进行处理,不然将请求放到定时器队列里。注意这个定时器队列是共享的,并无为单独的key(好比,每一个IP地址)设置队列。关于handler模块背景知识的介绍,可参考Tengine团队撰写的Nginx开发从入门到精通
关于按请求速率限速的原理讲解,可参考Rate Limiting with NGINX and NGINX Plus,关于源码更详细的解析可参考ngx_http_limit_req_module 源码分析以及y123456yz的Nginx源码分析的git项目
本文主要讲解了Nginx按请求速率限速模块的用法和原理,其中burst和nodelay参数是容易引发误解的,虽然可经过burst容许缓存处理突发请求,结合nodelay可以下降突发请求的处理时间,可是长期来看他们并不会提升吞吐量的上限,长期吞吐量的上限是由rate决定的。须要特别注意的是,burst设置了nodelay时,系统瞬间的QPS可能会超过rate设置的阈值。
本文只是对Nginx管中窥豹,更多关于Nginx介绍的文章,可参考Tengine团队撰写的Nginx开发从入门到精通。