ngx_http_limit_req_module
是 Nginx 官方提供的一个 http 模块,它工作在 NGX_HTTP_PREACCESS_PHASE
阶段,通过在 nginx.conf
中进行简单地配置,我们可以轻易地对请求速率进行限制。
配置指令
Example Configuration
http {
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
...
server {
...
location /search/ {
limit_req zone=one burst=5;
}
limit_req
Syntax: limit_req zone=name [burst=number] [nodelay];
Default: —
Context: http, server, location
该指令为名为 name
的共享内存设置一个突发请求限制大小(burst
)和一个 nodelay
标志位
limit_req_log_level
Syntax: limit_req_log_level info | notice | warn | error;
Default:
limit_req_log_level error;
Context: http, server, location
limit_req_log_level
这条指令用来设置当触发请求限制时,记录日志的级别,默认是 error
limit_req_status
Syntax: limit_req_status code;
Default:
limit_req_status 503;
Context: http, server, location
limit_req_status
用来设置服务器因请求限制设置而拒绝一个请求时,返回的状态码,默认是 503
limit_req_zone
Syntax: limit_req_zone key zone=name:size rate=rate;
Default: —
Context: http
该指令用来分配一块名为 name
,大小为 size
的共享内存,
这块共享内存服务于一个特定的 key
,限制了请求频率不得超过 rate
,注意该指令只能配置在 http{}
块下
设计思想
为了能够让单机上所有的 worker 进程共享一份最新的关于请求限制的数据,Nginx 把这些“域”的数据都放在共享内存中。
这个模块很灵活得允许设置多个不同的“域”(我指的“域”是指同一类的 key
,例如 $binary_remote_addr$uri
,每块共享内存用来存对应“域”的信息),每当一个请求来临时,在 preaccess 阶段进行检查,通过遍历每个“域”,在每个“域”的红黑树上找对应 key
实例化以后 对应的节点,然后根据漏桶算法,计算同一个 key
上次访问的时间到现在,经过这段时间的处理,还应该剩下的请求数目,然后再允许有多个突发请求(这里就是令牌桶的思想了),根据是否超过突发请求限制,决定是吐对应的禁止状态码还是延迟处理这个请求。
另外一点为了不让每个“域”的红黑树急剧膨胀而导致这个“域”的内存耗尽,这个模块还设置了一个 LRU 队列(当然也是每个“域”一个队列),将红黑树上的节点按最近最久未使用排成一个队列,然后每当要新建节点的时候,都去尝试淘汰一些节点(为了不长时间处于淘汰的循环中,最多淘汰 3 个节点)。
如果当前需要延迟处理,Nginx 又会把请求放到定时器中,等到定时器过期以后,执行写事件回调 ngx_http_limit_req_delay
,这个函数里会执行 ngx_http_core_run_phases
,重新进行 HTTP 的 11 个阶段。
数据结构定义
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;
这个数据结构会被放在共享内存中,记录了一颗红黑树和一个队列。
红黑树用来记录每个 “域”(即根据
limit_req_zone
里定义的key
得到)目前的状况队列将红黑树节点按更新时间从早到晚串起来,用来淘汰过于陈旧的节点(LRU)
该数据结构可以由下面的 ngx_http_limit_req_ctx_t
里的 sh
成员索引得到。
ngx_http_limit_req_node_t
typedef struct {
u_char color;
u_char dummy;
u_short len;
ngx_queue_t queue;
ngx_msec_t last;
/* integer value, 1 corresponds to 0.001 r/s */
ngx_uint_t excess;
ngx_uint_t count; /* 标记使用的 */
u_char data[1];
} ngx_http_limit_req_node_t; /* rbtree node */
表示红黑树节点信息的数据结构,这个数据结构的设计十分巧妙,由于红黑树上用的节点结构只能是 ngx_rbtree_node_t
,所以和它实际挂到红黑树上的节点的数据结构是连续存放的,且共用了成员 color
和 dummy
(也就是 ngx_rbtree_node_t
的 data
成员),另外,由于 key
是变成的,这里它只存了这个 key
的第一个字符(data
),其他的字符紧跟在这个数据结构后面,也是连续的,所以在要新建一个节点的时候,计算需要的内存大小应该是
size = offsetof(ngx_rbtree_node_t, color)
+ offsetof(ngx_http_limit_req_node_t, data)
+ key->len;
计算出 color
在 ngx_rbtree_node_t
里的偏移(这样等于算出了 ngx_rbtree_node_t
在color
以前的成员占用内存大小);再计算出 data
在 ngx_http_limit_req_node_t
的偏移,最后加上 key
的长度,这就是整个节点信息结构需要的内存的大小。
另外 len
是变长 key
的长度;queue
成员是用来标识这个节点在 LRU 队列里的位置的,记录了上个节点和下一个节点;last
是上次更新时间;excess
表示上次处理完后剩下来的请求数 * 1000(leaky bucket algorithm)
ngx_http_limit_req_ctx_t
typedef struct {
ngx_http_limit_req_shctx_t *sh;
ngx_slab_pool_t *shpool; /* slab shared memory pool */
/* integer value, 1 corresponds to 0.001 r/s */
ngx_uint_t rate; /* about limit_req_zone */
ngx_http_complex_value_t key; /* about limit_req_zone */
ngx_http_limit_req_node_t *node; /* point to one node in red-black tree */
} ngx_http_limit_req_ctx_t;
该结构体存放根据 limit_req_zone
指令创建的共享内存的相关上下文信息。其中
rate
和key
均是根据指令limit_req_zone
而解析得到node
成员指向了一个节点
ngx_http_limit_req_limit_t
typedef struct {
ngx_shm_zone_t *shm_zone;
/* integer value, 1 corresponds to 0.001 r/s */
ngx_uint_t burst;
ngx_uint_t nodelay; /* unsigned nodelay:1 */
} ngx_http_limit_req_limit_t;
存放了 limit_req
指令的相关配置信息,例如 burst
表示一个 “域”(key
)最多允许的突发请求数,nodelay
表示是否要延迟处理那些超出请求速率的请求。
这个结构体可以通过 ngx_http_limit_req_conf_t
的 limits
成员索引得到。并且shm_zone
的 data
指向了 ngx_http_limit_req_ctx_t
结构体。
ngx_http_limit_req_conf_t
typedef struct {
ngx_array_t limits;
ngx_uint_t limit_log_level;
ngx_uint_t delay_log_level;
ngx_uint_t status_code;
} ngx_http_limit_req_conf_t;
这个结构体存放配置项信息,值得提一下的是 limits
成员,这个动态数组把所有创建的共享内存信息给存放起来了,每个成员都指向一个 ngx_http_limit_req_limit_t
结构。
函数分析
ngx_http_limit_req_zone
功能:对指令 limit_req_zone
指令进行解析,创建对应的共享内存,设置 rate
, key
等参数到 ngx_http_limit_req_ctx_t
结构体变量中
static char *
ngx_http_limit_req_zone(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
......
/* 只要解析到一条 limit_req_zone 指令,就会创建一个 ctx */
ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_limit_req_ctx_t));
if (ctx == NULL) {
return NGX_CONF_ERROR;
}
......
size = 0;
rate = 1;
scale = 1; /* 单位转换时用 */
name.len = 0;
for (i = 2; i < cf->args->nelts; i++) {
if (ngx_strncmp(value[i].data, "zone=", 5) == 0) {
/*
* 这里主要是在解析 zone 的 name 和 size
* 代码比较简单,可以自行阅读
*/
......
}
if (ngx_strncmp(value[i].data, "rate=", 5) == 0) {
/*
* 这里主要是解析 rate,包括解析单位 r/s 和 r/m
* 计算对应的 scale
*/
......
}
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid parameter \"%V\"", &value[i]);
return NGX_CONF_ERROR;
}
......
/* 实际使用的 rate 会被放大 1000 倍 */
ctx->rate = rate * 1000 / scale;
/* 创建一块共享内存 name size tag */
shm_zone = ngx_shared_memory_add(cf, &name, size,
&ngx_http_limit_req_module);
if (shm_zone == NULL) {
return NGX_CONF_ERROR;
}
if (shm_zone->data) {
ctx = shm_zone->data;
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"%V \"%V\" is already bound to key \"%V\"",
&cmd->name, &name, &ctx->key.value);
return NGX_CONF_ERROR;
}
/* 设置好自定义的初始化方法,设置好 ctx 的索引 */
shm_zone->init = ngx_http_limit_req_init_zone;
shm_zone->data = ctx;
return NGX_CONF_OK;
}
ngx_http_limit_req_init_zone
功能:该函数负责初始化放在共享内存中的上下文信息,包括红黑树的初始化,队列初始化,所以每个“域”
static ngx_int_t
ngx_http_limit_req_init_zone(ngx_shm_zone_t *shm_zone, void *data)
{
ngx_http_limit_req_ctx_t *octx = data;
size_t len;
ngx_http_limit_req_ctx_t *ctx;
ctx = shm_zone->data;
if (octx) {
/*
* 这个过程发生在 reload 的时候
* 如果对应共享内存的 key 没变,直接复用就行了
*/
if (ctx->key.value.len != octx->key.value.len
|| ngx_strncmp(ctx->key.value.data, octx->key.value.data,
ctx->key.value.len)
!= 0)
{
ngx_log_error(NGX_LOG_EMERG, shm_zone->shm.log, 0,
"limit_req \"%V\" uses the \"%V\" key "
"while previously it used the \"%V\" key",
&shm_zone->shm.name, &ctx->key.value,
&octx->key.value);
return NGX_ERROR;
}
ctx->sh = octx->sh;
ctx->shpool = octx->shpool;
return NGX_OK;
}
ctx->shpool = (ngx_slab_pool_t *) shm_zone->shm.addr;
if (shm_zone->shm.exists) {
ctx->sh = ctx->shpool->data;
return NGX_OK;
}
/* 从 slab 池申请一块存放 ngx_http_limit_req_shctx_t 的内存 */
ctx->sh = ngx_slab_alloc(ctx->shpool, sizeof(ngx_http_limit_req_shctx_t));
if (ctx->sh == NULL) {
return NGX_ERROR;
}
ctx->shpool->data = ctx->sh;
/* 初始化这个“域”的红黑树和 LRU 队列 */
ngx_rbtree_init(&ctx->sh->rbtree, &ctx->sh->sentinel,
ngx_http_limit_req_rbtree_insert_value);
ngx_queue_init(&ctx->sh->queue);
......
return NGX_OK;
}
ngx_http_limit_req
功能:对指令 limit_req
指令进行解析,判断出设置的共享内存名字,将其挂到 ngx_http_limit_req_limit_t
的 limits
数组
static char *
ngx_http_limit_req(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
......
value = cf->args->elts;
shm_zone = NULL;
burst = 0;
nodelay = 0;
for (i = 1; i < cf->args->nelts; i++) {
if (ngx_strncmp(value[i].data, "zone=", 5) == 0) {
s.len = value[i].len - 5;
s.data = value[i].data + 5;
/*
* 如果这条 limit_req 指令在对应声明共享内存的 limit_req_zone 指令
* 之前的话,这里也会先创建好这个 shm_zone, 下次执行到相应的
* limit_req_zone 指令,只是把 size 改变了下
* 反之如果 limit_req_zone 先执行,这次操作就是从 cycle->shared_memory
* 上面把对应的 shm_zone 拿下来而已
*/
shm_zone = ngx_shared_memory_add(cf, &s, 0,
&ngx_http_limit_req_module);
if (shm_zone == NULL) {
return NGX_CONF_ERROR;
}
continue;
}
if (ngx_strncmp(value[i].data, "burst=", 6) == 0) {
/* 解析 burst,这个“域”允许的最大突发请求数 */
burst = ngx_atoi(value[i].data + 6, value[i].len - 6);
if (burst <= 0) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid burst rate \"%V\"", &value[i]);
return NGX_CONF_ERROR;
}
continue;
}
if (ngx_strcmp(value[i].data, "nodelay") == 0) {
nodelay = 1;
continue;
}
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid parameter \"%V\"", &value[i]);
return NGX_CONF_ERROR;
}
......
limits = lrcf->limits.elts;
if (limits == NULL) {
if (ngx_array_init(&lrcf->limits, cf->pool, 1,
sizeof(ngx_http_limit_req_limit_t))
!= NGX_OK)
{
return NGX_CONF_ERROR;
}
}
/* 假如 limit_req 重复指定一块相同的共享内存(由 limit_req_zone 指令指定),则会返回错误 */
for (i = 0; i < lrcf->limits.nelts; i++) {
if (shm_zone == limits[i].shm_zone) {
return "is duplicate";
}
}
/* 将这个“域”的 ngx_http_limit_req_limit_t 结构体设置好,放到 limits 数组 */
limit = ngx_array_push(&lrcf->limits);
if (limit == NULL) {
return NGX_CONF_ERROR;
}
/*
* 到时候会把 shm_zone->data 指向 ngx_http_limit_req_ctx_t
* 这样就和 ngx_http_limit_req_ctx_t 联系起来了
*/
limit->shm_zone = shm_zone;
limit->burst = burst * 1000; /* burst 也放大 1000 倍 */
limit->nodelay = nodelay;
return NGX_CONF_OK;
}
ngx_http_limit_req_create_conf
功能:创建 ngx_http_limit_req_conf_t
结构体,代码比较简单。
ngx_http_limit_req_merge_conf
功能:合并配置项,代码很简单。
ngx_http_limit_req_init
功能:设置钩子函数 ngx_http_limit_req_handler
到 ngx_http_core_main_conf
的 phases 数组里(NGX_HTTP_PREACCESS_PHASE
),代码很简单。
ngx_http_limit_req_handler
功能:遍历设置好的共享内存,调用 ngx_http_limit_req_lookup
来判断是否需要进行禁用或者延迟,如果禁用,则返回设置的对应状态码;如果需要延迟,则将这条连接上的写事件处理方法设置为 ngx_http_limit_req_delay
,并放入定时器中,过期时间通过 ngx_http_limit_req_account
计算出来
static ngx_int_t
ngx_http_limit_req_handler(ngx_http_request_t *r)
{
uint32_t hash;
ngx_str_t key;
ngx_int_t rc;
ngx_uint_t n, excess;
ngx_msec_t delay;
ngx_http_limit_req_ctx_t *ctx;
ngx_http_limit_req_conf_t *lrcf;
ngx_http_limit_req_limit_t *limit, *limits;
if (r->main->limit_req_set) {
/* 如果这个请求的主请求已经进行了该阶段的检查
* 直接返回 NGX_DCLIEND,让下一个 HTTP 模块介入请求
*/
return NGX_DECLINED;
}
lrcf = ngx_http_get_module_loc_conf(r, ngx_http_limit_req_module);
limits = lrcf->limits.elts;
excess = 0;
rc = NGX_DECLINED;
#if (NGX_SUPPRESS_WARN)
limit = NULL;
#endif
/* 遍历设置好的“域” */
for (n = 0; n < lrcf->limits.nelts; n++) {
limit = &limits[n];
ctx = limit->shm_zone->data;
if (ngx_http_complex_value(r, &ctx->key, &key) != NGX_OK) {
return NGX_HTTP_INTERNAL_SERVER_ERROR;
}
if (key.len == 0) {
continue;
}
if (key.len > 65535) {
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
"the value of the \"%V\" key "
"is more than 65535 bytes: \"%V\"",
&ctx->key.value, &key);
continue;
}
/* 计算 hash */
hash = ngx_crc32_short(key.data, key.len);
ngx_shmtx_lock(&ctx->shpool->mutex);
/* 在这个"域" 的红黑树上找这个 key 对应的节点 */
rc = ngx_http_limit_req_lookup(limit, hash, &key, &excess,
(n == lrcf->limits.nelts - 1));
ngx_shmtx_unlock(&ctx->shpool->mutex);
ngx_log_debug4(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
"limit_req[%ui]: %i %ui.%03ui",
n, rc, excess / 1000, excess % 1000);
if (rc != NGX_AGAIN) {
/* 只要 ngx_http_limit_req_lookup 返回的不是 NGX_AGAIN,就 break */
break;
}
}
if (rc == NGX_DECLINED) {
return NGX_DECLINED;
}
r->main->limit_req_set = 1;
if (rc == NGX_BUSY || rc == NGX_ERROR) {
if (rc == NGX_BUSY) {
ngx_log_error(lrcf->limit_log_level, r->connection->log, 0,
"limiting requests, excess: %ui.%03ui by zone \"%V\"",
excess / 1000, excess % 1000,
&limit->shm_zone->shm.name);
}
while (n--) {
/* 经历过的 n 个“域”,取出 node,将 count-- */
ctx = limits[n].shm_zone->data;
if (ctx->node == NULL) {
continue;
}
ngx_shmtx_lock(&ctx->shpool->mutex);
ctx->node->count--;
ngx_shmtx_unlock(&ctx->shpool->mutex);
ctx->node = NULL;
}
return lrcf->status_code;
}
/* rc == NGX_AGAIN || rc == NGX_OK */
if (rc == NGX_AGAIN) {
excess = 0;
}
/* 计算好延迟时间 */
delay = ngx_http_limit_req_account(limits, n, &excess, &limit);
if (!delay) {
return NGX_DECLINED;
}
ngx_log_error(lrcf->delay_log_level, r->connection->log, 0,
"delaying request, excess: %ui.%03ui, by zone \"%V\"",
excess / 1000, excess % 1000, &limit->shm_zone->shm.name);
if (ngx_handle_read_event(r->connection->read, 0) != NGX_OK) {
/*
* 这里处理下这条连接的读事件,是为了如果在这段延迟的时间内,客户端
* 主动关闭了连接,Nginx 也可以通过事件调度器感知到,从而及时断开连接
*/
return NGX_HTTP_INTERNAL_SERVER_ERROR;
}
r->read_event_handler = ngx_http_test_reading;
r->write_event_handler = ngx_http_limit_req_delay;
/* 添加到定时器红黑树上,等到过期时调用 ngx_http_limit_req_delay */
ngx_add_timer(r->connection->write, delay);
/*
* 这里返回 NGX_AGAIN,让这个模块有机会再介入这个请求,
* 其实也很好理解,毕竟 delay 之后,不能保证那个时刻这个请求涉及到的“域”
* 就一定没有超过该“域” 的请求设置限制了,所以还需要再次计算
*/
return NGX_AGAIN;
}
ngx_http_limit_req_lookup
功能:这个函数是核心,在某个“域”的红黑树上找到对应 hash 值的节点,根据漏桶算法,以固定速率处理请求,但又不仅仅是漏桶算法,这里还包含了令牌桶算法的突发门限,具体表现在只要不超过突发门限值,就不会返回 NGX_BUSY,这样就可以处理一定量的突发请求了。
返回值意义:
- NGX_BUSY 超过了突发门限
- NGX_OK 没有超过限制的请求频率
- NGX_AGAIN 超过限制的请求频率,但是没有到达突发门限
static ngx_int_t
ngx_http_limit_req_lookup(ngx_http_limit_req_limit_t *limit, ngx_uint_t hash,
ngx_str_t *key, ngx_uint_t *ep, ngx_uint_t account)
{
size_t size;
ngx_int_t rc, excess;
ngx_msec_t now;
ngx_msec_int_t ms;
ngx_rbtree_node_t *node, *sentinel;
ngx_http_limit_req_ctx_t *ctx;
ngx_http_limit_req_node_t *lr;
now = ngx_current_msec;
ctx = limit->shm_zone->data;
node = ctx->sh->rbtree.root;
sentinel = ctx->sh->rbtree.sentinel;
while (node != sentinel) {
if (hash < node->key) {
node = node->left;
continue;
}1
if (hash > node->key) {
node = node->right;
continue;
}
/* hash == node->key */
lr = (ngx_http_limit_req_node_t *) &node->color;
rc = ngx_memn2cmp(key->data, lr->data, key->len, (size_t) lr->len);
/* hash 值相同,且 key 相同,才算是找到 */
if (rc == 0) {
/* 这个节点最近才访问,放到队列首部,最不容易被淘汰(LRU 思想)*/
ngx_queue_remove(&lr->queue);
ngx_queue_insert_head(&ctx->sh->queue, &lr->queue);
/*
* 漏桶算法:以固定速率接受请求,每秒接受 rate 个请求,
* ms 是距离上次处理这个 key 到现在的时间,单位 ms
* lr->excess 是上次还遗留着被延迟的请求数(*1000)
* excess = lr->excess - ctx->rate * ngx_abs(ms) / 1000 + 1000;
* 本次还会遗留的请求数就是上次遗留的减去这段时间可以处理掉的加上这个请求本身(之前 burst 和 rate 都放大了 1000 倍)
*/
ms = (ngx_msec_int_t) (now - lr->last);
excess = lr->excess - ctx->rate * ngx_abs(ms) / 1000 + 1000;
if (excess < 0) {
/* 全部处理完了 */
excess = 0;
}
*ep = excess;
if ((ngx_uint_t) excess > limit->burst) {
/* 这段时间处理之后,遗留的请求数超出了突发请求限制 */
return NGX_BUSY;
}
if (account) {
/* 这个请求到了最后一个“域”的限制
* 更新上次遗留请求数和上次访问时间
* 返回 NGX_OK 表示没有达到请求限制的频率
*/
lr->excess = excess;
lr->last = now;
return NGX_OK;
}
/*
* count++;
* 把这个“域”的 ctx->node 指针指向这个节点
* 这个在 ngx_http_limit_req_handler 里用到
*/
lr->count++;
/* 这一步是为了在 ngx_http_limit_req_account 里更新这些访问过的节点的信息 */
ctx->node = lr;
/* 返回 NGX_AGAIN,会进行下一个“域”的检查 */
return NGX_AGAIN;
}
node = (rc < 0) ? node->left : node->right;
}
/* 没有在红黑树上找到节点 */
*ep = 0;
/*
* 新建一个节点,需要的内存大小,包括了红黑树节点大小
* ngx_http_limit_req_node_t 还有 key 的长度
*/
size = offsetof(ngx_rbtree_node_t, color)
+ offsetof(ngx_http_limit_req_node_t, data)
+ key->len;
/* 先进行 LRU 淘汰,传入 n=1,则最多淘汰 2 个节点 */
ngx_http_limit_req_expire(ctx, 1);
/* 由于调用 ngx_http_limit_req_lookup 之前已经上过锁,这里不用再上 */
node = ngx_slab_alloc_locked(ctx->shpool, size);
if (node == NULL) {
/* 分配失败考虑再进行一次 LRU 淘汰,及时释放共享内存空间,这里 n = 0,最多淘汰 3 个节点 */
ngx_http_limit_req_expire(ctx, 0);
node = ngx_slab_alloc_locked(ctx->shpool, size);
if (node == NULL) {
ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, 0,
"could not allocate node%s", ctx->shpool->log_ctx);
return NGX_ERROR;
}
}
/* 设置相关的信息 */
node->key = hash;
lr = (ngx_http_limit_req_node_t *) &node->color;
lr->len = (u_short) key->len;
lr->excess = 0;
ngx_memcpy(lr->data, key->data, key->len);
ngx_rbtree_insert(&ctx->sh->rbtree, node);
ngx_queue_insert_head(&ctx->sh->queue, &lr->queue);
if (account) {
/* 同样地,如果这是最后一个“域”的检查,就更新 last 和 count,返回 NGX_OK */
lr->last = now;
lr->count = 0;
return NGX_OK;
}
/* 否则就令 count = 1,把节点放到 ctx 上 */
lr->last = 0;
lr->count = 1;
ctx->node = lr;
return NGX_AGAIN;
}
ngx_http_limit_req_expire
功能:从队列(ngx_http_limit_req_shctx_t->queue
)尾部遍历,将过期的红黑树节点删除,及时释放共享内存空间
static void
ngx_http_limit_req_expire(ngx_http_limit_req_ctx_t *ctx, ngx_uint_t n)
{
ngx_int_t excess;
ngx_msec_t now;
ngx_queue_t *q;
ngx_msec_int_t ms;
ngx_rbtree_node_t *node;
ngx_http_limit_req_node_t *lr;
now = ngx_current_msec;
/*
* n == 1 deletes one or two zero rate entries
* n == 0 deletes oldest entry by force
* and one or two zero rate entries
*/
/* 从这里可以看到,最多只会删除 2 - n + 1 个节点 */
while (n < 3) {
if (ngx_queue_empty(&ctx->sh->queue)) {
return;
}
/* 队列尾部的节点最近最久没有访问,最有可能被淘汰 */
q = ngx_queue_last(&ctx->sh->queue);
/* 取出对应节点 */
lr = ngx_queue_data(q, ngx_http_limit_req_node_t, queue);
/*
* 从这里可以看到,如果 count 大于 0,则不会被淘汰
* 所以看到 ngx_http_limit_req_handler 里如果这个 key 在某个“域”超过请求限制频率时,就把那个节点的 count++,避免不小心把节点删除
*
*/
if (lr->count) {
/*
* There is not much sense in looking further,
* because we bump nodes on the lookup stage.
*/
return;
}
if (n++ != 0) {
ms = (ngx_msec_int_t) (now - lr->last);
ms = ngx_abs(ms);
if (ms < 60000) {
return;
}
excess = lr->excess - ctx->rate * ms / 1000;
if (excess > 0) {
return;
}
}
ngx_queue_remove(q);
node = (ngx_rbtree_node_t *)
((u_char *) lr - offsetof(ngx_rbtree_node_t, color));
ngx_rbtree_delete(&ctx->sh->rbtree, node);
ngx_slab_free_locked(ctx->shpool, node);
}
}
ngx_http_limit_req_account
功能:这个函数负责对目前的这个请求计算一个延时时间,计算规则是
遍历每个之前在 ngx_http_limit_req_lookup 里访问过的“域”
如果这个“域”设置了 nodelay,跳到下一个
否则根据漏桶算法,和 ngx_http_limit_req_lookup 一样的做法,计算出从上次访问到现在,应该剩下的请求数,除以 rate,得到了这些请求数应该延迟的时间
取最大值
其实这些值都可以在 ngx_http_limit_req_lookup 里计算出来,不过为了让一个函数做一件事,这样设计条理更加清晰吧。
static ngx_msec_t
ngx_http_limit_req_account(ngx_http_limit_req_limit_t *limits, ngx_uint_t n,
ngx_uint_t *ep, ngx_http_limit_req_limit_t **limit)
{
ngx_int_t excess;
ngx_msec_t now, delay, max_delay;
ngx_msec_int_t ms;
ngx_http_limit_req_ctx_t *ctx;
ngx_http_limit_req_node_t *lr;
/*
* excess 是之前在 ngx_http_limit_req_lookup
* 里遍历到的最后一个“域”针对这个请求经过漏桶计算后
* 应该剩下的请求数; limit 则是最后一个“域”的配置
*/
excess = *ep;
if (excess == 0 || (*limit)->nodelay) {
/* 配置项里设置了 nodelay 或者 excess = 0 */
max_delay = 0;
} else {
ctx = (*limit)->shm_zone->data;
/*
* 剩下了 excess 个请求,加请求的速率是 rate,那么延迟
* 就是 excess * 1000 / ctx->rate,这里乘以 1000 是因为 rate 的单位是 ms
*/
max_delay = excess * 1000 / ctx->rate;
}
while (n--) {
/* 反向遍历之前遍历过的“域” */
ctx = limits[n].shm_zone->data;
/* 为了更新信息,所以才需要在 ctx 里放一个 node */
lr = ctx->node;
if (lr == NULL) {
continue;
}
ngx_shmtx_lock(&ctx->shpool->mutex);
now = ngx_current_msec;
ms = (ngx_msec_int_t) (now - lr->last);
excess = lr->excess - ctx->rate * ngx_abs(ms) / 1000 + 1000;
if (excess < 0) {
excess = 0;
}
/* 更新节点上的信息 */
lr->last = now;
lr->excess = excess;
lr->count--;
ngx_shmtx_unlock(&ctx->shpool->mutex);
ctx->node = NULL;
if (limits[n].nodelay) {
continue;
}
delay = excess * 1000 / ctx->rate;
if (delay > max_delay) {
max_delay = delay;
*ep = excess;
*limit = &limits[n];
}
}
/* 这里就计算出了一个最大延迟值 */
return max_delay;
}
ngx_http_limit_req_rbtree_insert_value
功能:该模块自定义的红黑树节点插入方法,key 就是根据用户配置的 limit_req_zone
指令里的 key
字段,hash 方法是 ngx_crc32_short
static void
ngx_http_limit_req_rbtree_insert_value(ngx_rbtree_node_t *temp,
ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel)
{
ngx_rbtree_node_t **p;
ngx_http_limit_req_node_t *lrn, *lrnt;
for ( ;; ) {
if (node->key < temp->key) {
p = &temp->left;
} else if (node->key > temp->key) {
p = &temp->right;
} else { /* node->key == temp->key */
/*
* 值相等不见得 key 一定相同,存在 hash 冲突的
* 前面说过,ngx_http_limit_req_node_t 和 ngx_rbtree_node_t
* 复用了 color 和 data 这两个字段,ngx_http_limit_req_node_t 的地址
* 就是 ngx_rbtree_node_t 里的 color 字段的地址
*/
lrn = (ngx_http_limit_req_node_t *) &node->color;
lrnt = (ngx_http_limit_req_node_t *) &temp->color;
p = (ngx_memn2cmp(lrn->data, lrnt->data, lrn->len, lrnt->len) < 0)
? &temp->left : &temp->right;
}
if (*p == sentinel) {
break;
}
temp = *p;
}
*p = node;
node->parent = temp;
node->left = sentinel;
node->right = sentinel;
/* 新加入节点需要涂成红色 */
ngx_rbt_red(node);
}
ngx_http_limit_req_delay
功能:作为写事件回调,再次运行 ngx_http_core_run_phases
,执行 HTTP 的 11 个阶段处理。
static void
ngx_http_limit_req_delay(ngx_http_request_t *r)
{
ngx_event_t *wev;
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
"limit_req delay");
wev = r->connection->write;
if (!wev->timedout) {
if (ngx_handle_write_event(wev, 0) != NGX_OK) {
ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
}
return;
}
wev->timedout = 0;
if (ngx_handle_read_event(r->connection->read, 0) != NGX_OK) {
ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
return;
}
r->read_event_handler = ngx_http_block_reading;
r->write_event_handler = ngx_http_core_run_phases;
ngx_http_core_run_phases(r);
}