浏览 2k
NGINX的速率控制用来控制新建连接的速度,并发控制用来控制并发连接数目,而带宽控制是用来控制单个连接上从服务器到客户端数据传输的速率。
我们前面已经分析了NGINX速率限制,并发限制的原理。作为NGINX流量控制系列的最后一篇文章,本文我们分析NGINX的带宽控制的原理。
NGINX采用了令牌桶算法进行带宽控制。使用一张经典的图偏来描述令牌桶算法:
具体流程是:
令牌桶算法用一只“桶”用来存储令牌,还用一个队列存储请求。从作用上来说,漏桶和令牌桶算法最明显的区别就是是否允许突发流量(burst)的处理,漏桶算法能够强行限制数据的实时传输(处理)速率,对突发流量不做额外处理,它对流量进行的是管制(policy);而令牌桶算法能够在限制数据的平均传输速率的同时允许某种程度的突发传输,它对流量进行的是整形(shapping)。
NGINX速率控制发生在HTTP的11个处理阶段的CONTENT阶段,通过过滤模块ngx_http_write_filter_module完成的。
过滤模块是对发往客户端的内容进行头部和内容处理(过滤)的模块。分为头部处理模块和内容处理模块。它是在获取回复内容之后,在向用户发送响应之前得到执行的。通过ngx_http_top_header_filter(r) 和 ngx_http_top_body_filter(r, in)两个函数分为两个阶段过滤HTTP回复的头部和内容主体。
从代码角度来说,NGINX把所有对头部处理的函数通过变量ngx_http_top_header_filter和ngx_http_next_header_filter连接起来形成一个单链表。同样的道理,通过变量ngx_http_top_body_filter和ngx_http_next_body_filter形成一个对内容处理的单链表。这样在往客户端发送数据是分别调用ngx_http_send_header和ngx_http_output_filter函数遍历头部链表和内容链表中的回调函数逐个进行处理。
过滤模块的回调函数在上述单链表的位置决定了它被执行次序。而过滤模块的回调函数在单链表中的位置是在编译期间模块声明的位置决定的。当编译完Nginx之后,在ngx_modules.c文件中有如下数据结构:
ngx_module_t *ngx_modules[] = {
...
&ngx_http_write_filter_module,
&ngx_http_header_filter_module,
&ngx_http_chunked_filter_module,
&ngx_http_range_header_filter_module,
&ngx_http_gzip_filter_module,
&ngx_http_postpone_filter_module,
&ngx_http_ssi_filter_module,
&ngx_http_charset_filter_module,
&ngx_http_userid_filter_module,
&ngx_http_headers_filter_module,
&ngx_http_copy_filter_module,
&ngx_http_range_body_filter_module,
&ngx_http_not_modified_filter_module,
NULL
};
按照上述声明,定义的第一个过滤模块是ngx_http_write_filter_module然后最后一个过滤模块是ngx_http_not_modified_filter_module。而各个模块的对应的回调函数是按照声明顺序相反的次序执行的。也就是说,ngx_http_not_modified_filter_module模块对应的回调函数是链表中的第一个,而ngx_http_write_filter_module对应的回调函数处在链表的末尾,也就是说它最后得到执行。
带宽控制在实现上是针对单个的连接进行带宽限制。从NGINX架构上来看,单个的连接整个生命周期的处理是在某一个单独的worker进程中进行的。所以,带宽控制不需要在各个worker进程之间共享和同步数据。从而不需要像速率控制、并发控制那样定义一个共享内存的zone来共享和同步的数据。基于此,带宽控制的指令就没有了速率控制和并发控制所需要定义共享内存区域的指令。它通过limit_rate 和limit_rate_after两条指令来实现,具体语法和意义如下:
Syntax:limit_rate rate;
Default:limit_rate 0;
Context:http, server, location, if in location
限制发向客户端响应的数据的速率。单位是BYTES每秒。默认值0表示不进行速率限制。此限制是针对每一个连接请求而言的,所以,如果客户端同时有并行的n个连接,那么这个客户端的整体速率就是n倍的limit_rate。
参数值也可以包含变量,这样可以做到根据不同的情况进行不同的带宽控制。如下所示:
map $slow $rate {
14k;
28k;
}
limit_rate $rate;
server {
if ($slow) {
set $limit_rate 4k;
}
...
}
Syntax:limit_rate_after size;
Default:limit_rate_after 0;
Context:http, server, location, if in location
在传输完一定数量的BYTES之后设开始实施带宽控制。与指令limit_rate一样,后面的参数数值可以通过变量来设置。
Example:
location /jikui/ {
limit_rate_after 500k;
limit_rate 50k;
}
指令limit_rate_after只有在配置了limit_rate的前提下才能生效。如果只配置limit_rate_after则不会有带宽控制的效果。
如果同时配置了sendfile_max_chunk 指令,按照两者较小的数值进行带宽控制。
通过limit_rate和limit_rate_after指令只能控制单个连接的带宽,没有办法对整个client的带宽进行限制。通过如下limit_conn和limit_rate组合能限制某一个客户端在单个worker进程中的总带宽是:worker进程数量*10*50k。
location /jikui/ {
limit_conn 10;
limit_rate 50k;
}
当NGINX解析配置时,在解析到limit_rate和limit_rate_after指令时,只需要把配置的数值分别设置到ngx_http_core_loc_conf_s结构中对应的limit_rate和limit_rate_after成员变量中。
模块ngx_http_write_filter_module只对发送到客户端的内容进行过滤。它通过函数ngx_http_write_filter_init 把ngx_http_write_filter挂接到ngx_http_top_body_filter函数链表中。
在处理客户端请求的CONTENT阶段,通过函数ngx_http_send_response准备好要送的数据以后,分别调用ngx_http_send_header和ngx_http_output_filter来对发送请求的头部和内容进行过滤。对应带宽控制功能的函数是ngx_http_write_filter就是通过函数ngx_http_output_filter来调用的。
它的逻辑是:
1. 如果结构体ngx_http_core_loc_conf_s中设置了limit_rate参数,通过下面的公式计算当前可以发送的数据的长度。
limit = (off_t) r->limit_rate * (ngx_time() - r->start_sec + 1) - (c->sent - r->limit_rate_after);
2. 如果计算的limit小于0,说明按照当前的速率配置和已经发送的数据的情况,需要暂缓发送。这时候根据delay = (ngx_msec_t) (- limit * 1000 / r->limit_rate + 1)计算需要推迟发送的时间。并且根据计算的时间生成一个timer加入到NGINX自身的timer系统中等待执行。
3. 如果limit大于0,说明按照当前的速率配置可以进行发送数据。调用send_chain(c, r->out, limit)进行发送特定数量的数据。
4. 如果通过step3发送完毕以后,还有剩余的数据没有发送,则根据上次发送数据所用的时间delay = (ngx_msec_t) ((nsent - sent) * 1000 / r->limit_rate)生成需要暂缓发送的时间然后生成timer来规划下一次发送。
5. 同时如果还有数据要发送,而且上次发送的数据大于等于limit减去两个page大小的数量limit - (off_t) (2 * ngx_pagesize),则生成一个1秒钟的timer尽快规划发送。
作为NGINX流量管理的三大功能之一,带宽控制的实现相对简单。它也只是实现了单个连接上的带宽控制,经常要和速率控制、并发控制一起使用来实现客户端的流量管理。
如果想更方便地对客户端总的带宽流量进行控制,可以采用类似ngx_limit_speed_module第三方模块。这个第三方模块通过定义共享内存区域可以让多个worker进程共享带宽数据从而方便地进行客户端总带宽的限制。