Just For Coding

Keep learning, keep living …

Ngx_http_limit_req_module分析

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
2
3
4
5
6
7
8
9
10
11
12
13
static ngx_http_module_t  ngx_http_limit_req_module_ctx = {
    NULL,                                  /* preconfiguration */
    ngx_http_limit_req_init,               /* postconfiguration */

    NULL,                                  /* create main configuration */
    NULL,                                  /* init main configuration */

    NULL,                                  /* create server configuration */
    NULL,                                  /* merge server configuration */

    ngx_http_limit_req_create_conf,        /* create location configuration */
    ngx_http_limit_req_merge_conf          /* merge location configuration */
};

limit_req模块只使用location级别的配置,只注册了location级别配置的create和merge函数。

ngx_http_limit_req_create_conf()函数简单地从配置内存池中分配一个ngx_http_limit_req_conf_t结构。

1
2
3
4
5
6
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;

该结构用于保存一个location{}配置块中的limit_req模块配置。location{}配置块下允许使用多个limit_req指令。如:

1
2
3
4
5
location /dummy {
    limit_req zone=one burst=5 nodelay;
    limit_req zone=two burst=10;
    ...
}

limits数组的每个元素为ngx_http_limit_req_limit_t结构,每条limit_req指令配置都保存到该结构中。

1
2
3
4
5
6
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_zone和limit_req指令的解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
{ ngx_string("limit_req_zone"),
  NGX_HTTP_MAIN_CONF|NGX_CONF_TAKE3,
  ngx_http_limit_req_zone,
  0,
  0,
  NULL },

{ ngx_string("limit_req"),
  NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE123,
  ngx_http_limit_req,
  NGX_HTTP_LOC_CONF_OFFSET,
  0,
  NULL },

limit_req_zone指令处理函数为ngx_http_limit_req_zone函数。

首先,函数从配置内存池分配一个ngx_http_limit_req_ctx_t结构。

1
2
3
4
ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_limit_req_ctx_t));
if (ctx == NULL) {
    return NGX_CONF_ERROR;
}

每个ngx_http_limit_req_ctx_t保存limit_req_zone指令的配置内容:

1
2
3
4
5
6
7
8
typedef struct {
    ngx_http_limit_req_shctx_t  *sh;
    ngx_slab_pool_t             *shpool;
    /* integer value, 1 corresponds to 0.001 r/s */
    ngx_uint_t                   rate;
    ngx_http_complex_value_t     key;
    ngx_http_limit_req_node_t   *node;
} ngx_http_limit_req_ctx_t;

接着,预编译该ZONE所指定的KEY,将编译结果保存key成员变量中,在请求处理阶段模块根据key成员变量得到KEY的值。

1
2
3
4
5
6
7
8
9
ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t));

ccv.cf = cf;
ccv.value = &value[1];
ccv.complex_value = &ctx->key;

if (ngx_http_compile_complex_value(&ccv) != NGX_OK) {
    return NGX_CONF_ERROR;
}

接下来,解析各参数的值,如共享内存区的名称和大小,限制的访问频率。为了避免计算时使用浮点数而提高处理效率,代码中将1r/s对应的rate值设为1000。

1
ctx->rate = rate * 1000 / scale;

再接着,添加共享内存信息:

1
2
3
4
5
shm_zone = ngx_shared_memory_add(cf, &name, size,
                                 &ngx_http_limit_req_module);
...
shm_zone->init = ngx_http_limit_req_init_zone;
shm_zone->data = ctx;

NGINX此时并不会分配共享内存,只是将信息保存下来,在所有配置解析完后再统一进行分配。NGINX分配完共享内存后,调用设置的ngx_http_limit_req_init_zone来初始化该共享内存区域。

1
2
3
4
5
6
7
8
9
10
11
12
13
ctx->shpool = (ngx_slab_pool_t *) shm_zone->shm.addr;
...
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;

ngx_rbtree_init(&ctx->sh->rbtree, &ctx->sh->sentinel,
                ngx_http_limit_req_rbtree_insert_value);

ngx_queue_init(&ctx->sh->queue);

ngx_http_limit_req_init_zone函数在共享内存区创建了一个RBTREE和一个QUEUE结构,RBTREE NODE用于保存检查命中各KEY的请求是否应限速所需的信息,QUEUE结构用于回收长期没有访问的节点内存。

NGINX解析完配置后,调用注册在postconfiguration阶段的ngx_http_limit_req_init函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static ngx_int_t
ngx_http_limit_req_init(ngx_conf_t *cf)
{
    ngx_http_handler_pt        *h;
    ngx_http_core_main_conf_t  *cmcf;

    cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);

    h = ngx_array_push(&cmcf->phases[NGX_HTTP_PREACCESS_PHASE].handlers);
    if (h == NULL) {
        return NGX_ERROR;
    }

    *h = ngx_http_limit_req_handler;

    return NGX_OK;
}

该函数在NGINX请求处理的PREACCESS阶段注册了处理函数ngx_http_limit_req_handler。

下面来看请求阶段的处理。

当请求到达时,NGINX按阶段执行到ngx_http_limit_req_handler函数。

handler函数首先判断是否已经进行过limit_req检查。若已进行过,则直接跳到下一个阶段,保证每个请求只由limit_req模块处理一次。

1
2
3
if (r->main->limit_req_set) {
    return NGX_DECLINED;
}

接着依据当前请求所属location{}配置中的所有limit_req策略依次调用ngx_http_limit_req_lookup进行检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;
    }

    ...

    hash = ngx_crc32_short(key.data, key.len);
    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;
    }
}

ngx_http_limit_req_lookup()首先从RBTREE中查找当前KEY所属节点。 若查找到节点,则把当前节点移到队列首端,保证不会在近期被回收内存。

1
2
ngx_queue_remove(&lr->queue);
ngx_queue_insert_head(&ctx->sh->queue, &lr->queue);

然后,计算该请求是否超出了限制频率:

1
2
ms = (ngx_msec_int_t) (now - lr->last);
excess = lr->excess - ctx->rate * ngx_abs(ms) / 1000 + 1000;

这里用的是”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
2
3
4
5
6
7
8
9
10
11
12
13
14
tp = ngx_timeofday();

now = (ngx_msec_t) (tp->sec * 1000 + tp->msec);
ms = (ngx_msec_int_t) (now - lr->last);

excess = lr->excess - ctx->rate * ngx_abs(ms) / 1000 + 1000;

if (excess < 0) {
    excess = 0;
}

...

delay = excess * 1000 / ctx->rate;

若需要delay请求,则添加一个NGINX TIMER来延迟请求处理。

1
2
3
r->read_event_handler = ngx_http_test_reading;
r->write_event_handler = ngx_http_limit_req_delay;
ngx_add_timer(r->connection->write, delay);

至此,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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct ngx_rbtree_node_s {
    ngx_rbtree_key_t       key;
    ngx_rbtree_node_t     *left;
    ngx_rbtree_node_t     *right;
    ngx_rbtree_node_t     *parent;
    u_char                 color;
    u_char                 data;
};

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;

NODE大小这样计算:

1
2
3
size = offsetof(ngx_rbtree_node_t, color)
       + offsetof(ngx_http_limit_req_node_t, data)
       + key->len;

这样理解起来不够直观。可以简单的定义结构为:

1
2
3
4
5
6
7
8
9
10
typedef struct {
    ngx_rbtree_node_t            node;
    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;

这样计算NODE大小则为:

1
2
size = offsetof(ngx_http_limit_req_node_t, data)
       + key->len;

另外对于我们特定的需求,limit_req_zone限定的访问频率有两个不方便之处:

  • 不能针对不同的KEY,限定不同的访问频率
  • 不能实时动态更改, 只能通过修改配置RELOAD NGINX来生效

针对上述两个不方便之处,可以给limit_req_zone添加一个RATE变量表示需要对该请求的限制频率,在处理请求时动态获取到该值。而该变量可以在另外的模块中根据更情况来设置,比如,可以由NGX_LUA模块从REDIS等动态存储中获取相关信息而设置。

但这种方式有两点需注意:

  • 针对同一个KEY的请求要保证频率值不变,否则失去了频率的意义
  • RATE变量的设置要在PREACCESS阶段前,比如在SERVER REWRITE阶段

文中代码为nginx-1.8.0。