ngx_http_limit_req_module用于依据指定的KEY来限制请求处理速度。比如,用来限制单个IP的访问频率。
文档地址: http://nginx.org/en/docs/http/ngx_http_limit_req_module.html
下面来看具体实现:
NGINX模块从逻辑中主要有两部分:
- 配置解析: 将配置文件的内容解析到各配置结构中
- 请求处理: 在当前请求上完成模块逻辑处理
首先看配置解析部分:
limit_req模块的ngx_http_module_t结构指定了配置解析的HOOK函数:
1 | static ngx_http_module_t ngx_http_limit_req_module_ctx = { |
limit_req模块只使用location级别的配置,只注册了location级别配置的create和merge函数。
ngx_http_limit_req_create_conf()函数简单地从配置内存池中分配一个ngx_http_limit_req_conf_t结构。
1 | typedef struct { |
该结构用于保存一个location{}配置块中的limit_req模块配置。location{}配置块下允许使用多个limit_req指令。如:
1 | location /dummy { |
limits数组的每个元素为ngx_http_limit_req_limit_t结构,每条limit_req指令配置都保存到该结构中。
1 | typedef struct { |
下面看模块limit_req_zone和limit_req指令的解析:
1 | { ngx_string("limit_req_zone"), |
limit_req_zone指令处理函数为ngx_http_limit_req_zone函数。
首先,函数从配置内存池分配一个ngx_http_limit_req_ctx_t结构。
1 | ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_limit_req_ctx_t)); |
每个ngx_http_limit_req_ctx_t保存limit_req_zone指令的配置内容:
1 | typedef struct { |
接着,预编译该ZONE所指定的KEY,将编译结果保存key成员变量中,在请求处理阶段模块根据key成员变量得到KEY的值。
1 | ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); |
接下来,解析各参数的值,如共享内存区的名称和大小,限制的访问频率。为了避免计算时使用浮点数而提高处理效率,代码中将1r/s对应的rate值设为1000。
1 | ctx->rate = rate * 1000 / scale; |
再接着,添加共享内存信息:
1 | shm_zone = ngx_shared_memory_add(cf, &name, size, |
NGINX此时并不会分配共享内存,只是将信息保存下来,在所有配置解析完后再统一进行分配。NGINX分配完共享内存后,调用设置的ngx_http_limit_req_init_zone来初始化该共享内存区域。
1 | ctx->shpool = (ngx_slab_pool_t *) shm_zone->shm.addr; |
ngx_http_limit_req_init_zone函数在共享内存区创建了一个RBTREE和一个QUEUE结构,RBTREE NODE用于保存检查命中各KEY的请求是否应限速所需的信息,QUEUE结构用于回收长期没有访问的节点内存。
NGINX解析完配置后,调用注册在postconfiguration阶段的ngx_http_limit_req_init函数。
1 | static ngx_int_t |
该函数在NGINX请求处理的PREACCESS阶段注册了处理函数ngx_http_limit_req_handler。
下面来看请求阶段的处理。
当请求到达时,NGINX按阶段执行到ngx_http_limit_req_handler函数。
handler函数首先判断是否已经进行过limit_req检查。若已进行过,则直接跳到下一个阶段,保证每个请求只由limit_req模块处理一次。
1 | if (r->main->limit_req_set) { |
接着依据当前请求所属location{}配置中的所有limit_req策略依次调用ngx_http_limit_req_lookup进行检查:
1 | for (n = 0; n < lrcf->limits.nelts; n++) { |
ngx_http_limit_req_lookup()首先从RBTREE中查找当前KEY所属节点。
若查找到节点,则把当前节点移到队列首端,保证不会在近期被回收内存。
1 | ngx_queue_remove(&lr->queue); |
然后,计算该请求是否超出了限制频率:
1 | ms = (ngx_msec_int_t) (now - lr->last); |
这里用的是”leaky bucket”算法。lr->excess表示上次处理后剩余的请求数,ms为现在距上次处理请求的时间,最后的”1000”为当前请求(一个请求为1000),excess为按限定速率进行处理到现在应该剩余的请求数。如果excess超过了设定的暴发阈值,则函数直接返回NGX_BUSY。这会让NGINX直接以limit_req_status_code设定的状态码结束请求(默认值为503)。若没有超过暴发阈值,若当前检查的limit_req规则是LOCATION所配置的最后一个,则将excess和当前时间更新到NODE中,返回NGX_OK, 否则,返回NGX_AGAIN,表示通过了该条limit_req策略,应该继续检查下一条limit_req策略。
若没有找到节点,则创建新的节点插入到RBTREE中。同样的,若是最后一个策略返回NGX_OK, 否则返回NGX_AGAIN。
ngx_http_limit_req_lookup()返回值总结如下:
- NGX_ERROR: 分配内存错误
- NGX_AGAIN: 通过了一个limit_req策略,需要检查下一个策略
- NGX_OK: 通过了所有limit_req策略
- NGX_BUSY: 超过了设置的最大请求频率,直接以状态码结束请求
当通过所有策略后,handler调用ngx_http_limit_req_account检测是否需要delay请求。
1 | delay = ngx_http_limit_req_account(limits, n, &excess, &limit); |
delay为处理完当前所有剩余请求所需的时间:
1 | tp = ngx_timeofday(); |
若需要delay请求,则添加一个NGINX TIMER来延迟请求处理。
1 | r->read_event_handler = ngx_http_test_reading; |
至此,limit_req的逻辑处理完成。
limit_req模块在使用NGINX CORE的rbtree代码有些繁琐,比如ngx_http_limit_req_node_t结构用于保存”leaky bucket”算法所需的数据。limit_req模块需要将这些数据附在ngx_rbtree_node_t结构之后,从而复用rbtree操作的相关函数。limit_req模块在该结构的开始用了两个成员变量来标识来ngx_rbtree_node_t的拼接点。两结构定义如下:
1 | struct ngx_rbtree_node_s { |
NODE大小这样计算:
1 | size = offsetof(ngx_rbtree_node_t, color) |
这样理解起来不够直观。可以简单的定义结构为:
1 | typedef struct { |
这样计算NODE大小则为:
1 | size = offsetof(ngx_http_limit_req_node_t, data) |
另外对于我们特定的需求,limit_req_zone限定的访问频率有两个不方便之处:
- 不能针对不同的KEY,限定不同的访问频率
- 不能实时动态更改, 只能通过修改配置RELOAD NGINX来生效
针对上述两个不方便之处,可以给limit_req_zone添加一个RATE变量表示需要对该请求的限制频率,在处理请求时动态获取到该值。而该变量可以在另外的模块中根据更情况来设置,比如,可以由NGX_LUA模块从REDIS等动态存储中获取相关信息而设置。
但这种方式有两点需注意:
- 针对同一个KEY的请求要保证频率值不变,否则失去了频率的意义
- RATE变量的设置要在PREACCESS阶段前,比如在SERVER REWRITE阶段
文中代码为nginx-1.8.0。