Nginx限流模块详解

1.相关指令

1
2
3
Syntax: limit_conn_zone key zone=name:size;
Default: —
Context: http

此指令设置限流所需共享内存的名称和size,key指定限流的标志,比如$binary_remote_addr。

1
2
3
4
5

Syntax: limit_conn zone number;
Default: —
Context: http, server, location

此指令指定共享内存名以及限制多少连接,比如limit_conn_zone指令key设置为$binary_remote_addr,zone设置为limit_zone,那么来自于$binary_remote_addr的连接超过number,nginx直接给客户端响应503。

2.源码解析

2.1 配置解析函数

  1. 首先来看下limit_conn_zone指令的解析函数ngx_http_limit_conn_zone,这个函数的功能主要是解析共享内存的name和size,然后使用这两个值作为参数调用ngx_shared_memory_add函数,将共享内存添加到nginx共享内存管理模块中,设置共享内存的data以及初始化函数。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    static char *
    ngx_http_limit_conn_zone(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
    {
    u_char *p;
    ssize_t size;
    ngx_str_t *value, name, s;
    ngx_uint_t i;
    ngx_shm_zone_t *shm_zone;
    ngx_http_limit_conn_ctx_t *ctx;
    ngx_http_compile_complex_value_t ccv;

    value = cf->args->elts;

    ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_limit_conn_ctx_t));
    if (ctx == NULL) {
    return NGX_CONF_ERROR;
    }

    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;
    }

    size = 0;
    name.len = 0;

    for (i = 2; i < cf->args->nelts; i++) {

    if (ngx_strncmp(value[i].data, "zone=", 5) == 0) {

    name.data = value[i].data + 5;

    p = (u_char *) ngx_strchr(name.data, ':');

    if (p == NULL) {
    ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
    "invalid zone size \"%V\"", &value[i]);
    return NGX_CONF_ERROR;
    }

    name.len = p - name.data;

    s.data = p + 1;
    s.len = value[i].data + value[i].len - s.data;

    size = ngx_parse_size(&s);

    if (size == NGX_ERROR) {
    ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
    "invalid zone size \"%V\"", &value[i]);
    return NGX_CONF_ERROR;
    }

    if (size < (ssize_t) (8 * ngx_pagesize)) {
    ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
    "zone \"%V\" is too small", &value[i]);
    return NGX_CONF_ERROR;
    }

    continue;
    }

    ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
    "invalid parameter \"%V\"", &value[i]);
    return NGX_CONF_ERROR;
    }

    if (name.len == 0) {
    ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
    "\"%V\" must have \"zone\" parameter",
    &cmd->name);
    return NGX_CONF_ERROR;
    }

    shm_zone = ngx_shared_memory_add(cf, &name, size,
    &ngx_http_limit_conn_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;
    }

    shm_zone->init = ngx_http_limit_conn_init_zone;
    shm_zone->data = ctx;

    return NGX_CONF_OK;
    }

  1. 接下来看下limit_conn指令解析函数ngx_http_limit_conn,这个函数主要是保存配置的共享内存名以及限制连接的个数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

static char *
ngx_http_limit_conn(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_shm_zone_t *shm_zone;
ngx_http_limit_conn_conf_t *lccf = conf;
ngx_http_limit_conn_limit_t *limit, *limits;

ngx_str_t *value;
ngx_int_t n;
ngx_uint_t i;

value = cf->args->elts;

shm_zone = ngx_shared_memory_add(cf, &value[1], 0,
&ngx_http_limit_conn_module);
if (shm_zone == NULL) {
return NGX_CONF_ERROR;
}

limits = lccf->limits.elts;

if (limits == NULL) {
if (ngx_array_init(&lccf->limits, cf->pool, 1,
sizeof(ngx_http_limit_conn_limit_t))
!= NGX_OK)
{
return NGX_CONF_ERROR;
}
}

for (i = 0; i < lccf->limits.nelts; i++) {
if (shm_zone == limits[i].shm_zone) {
return "is duplicate";
}
}

// 解析配置种被限制的连接数
n = ngx_atoi(value[2].data, value[2].len);
if (n <= 0) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid number of connections \"%V\"", &value[2]);
return NGX_CONF_ERROR;
}

if (n > 65535) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"connection limit must be less 65536");
return NGX_CONF_ERROR;
}

limit = ngx_array_push(&lccf->limits);
if (limit == NULL) {
return NGX_CONF_ERROR;
}

limit->conn = n;
limit->shm_zone = shm_zone;

return NGX_CONF_OK;
}


  1. 接下来就来看下nginx是如何实现限流的,这个功能由ngx_http_limit_conn_handler实现,主要功能就是统计各个进程与key对应的连接数,超过则返回503,在统计各个进程数据的时候使用了共享内存。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117

    static ngx_int_t
    ngx_http_limit_conn_handler(ngx_http_request_t *r)
    {
    size_t n;
    uint32_t hash;
    ngx_str_t key;
    ngx_uint_t i;
    ngx_slab_pool_t *shpool;
    ngx_rbtree_node_t *node;
    ngx_pool_cleanup_t *cln;
    ngx_http_limit_conn_ctx_t *ctx;
    ngx_http_limit_conn_node_t *lc;
    ngx_http_limit_conn_conf_t *lccf;
    ngx_http_limit_conn_limit_t *limits;
    ngx_http_limit_conn_cleanup_t *lccln;

    if (r->main->limit_conn_set) {
    return NGX_DECLINED;
    }

    lccf = ngx_http_get_module_loc_conf(r, ngx_http_limit_conn_module);
    limits = lccf->limits.elts;

    for (i = 0; i < lccf->limits.nelts; i++) {
    ctx = limits[i].shm_zone->data;

    //获取limit_conn_zone指令后key的值
    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 > 255) {
    ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
    "the value of the \"%V\" key "
    "is more than 255 bytes: \"%V\"",
    &ctx->key.value, &key);
    continue;
    }

    r->main->limit_conn_set = 1;
    //对key进行crc计算,用于在红黑树中查找结点
    hash = ngx_crc32_short(key.data, key.len);

    shpool = (ngx_slab_pool_t *) limits[i].shm_zone->shm.addr;

    ngx_shmtx_lock(&shpool->mutex);
    // 查找符合要求的结点
    node = ngx_http_limit_conn_lookup(ctx->rbtree, &key, hash);
    // 没有找到对应的结点,将此次请求插入到红黑树中
    if (node == NULL) {
    //计算结点所需内存
    n = offsetof(ngx_rbtree_node_t, color)
    + offsetof(ngx_http_limit_conn_node_t, data)
    + key.len;
    // 申请结点内存
    node = ngx_slab_alloc_locked(shpool, n);

    if (node == NULL) {
    ngx_shmtx_unlock(&shpool->mutex);
    ngx_http_limit_conn_cleanup_all(r->pool);
    return lccf->status_code;
    }

    lc = (ngx_http_limit_conn_node_t *) &node->color;
    // 赋值key,用于下次请求到来查询
    node->key = hash;
    lc->len = (u_char) key.len;
    lc->conn = 1;
    ngx_memcpy(lc->data, key.data, key.len);

    ngx_rbtree_insert(ctx->rbtree, node);

    } else {

    lc = (ngx_http_limit_conn_node_t *) &node->color;
    // 判断key标记的connections是否超过配置,如果超过则返回错误码
    if ((ngx_uint_t) lc->conn >= limits[i].conn) {

    ngx_shmtx_unlock(&shpool->mutex);

    ngx_log_error(lccf->log_level, r->connection->log, 0,
    "limiting connections by zone \"%V\"",
    &limits[i].shm_zone->shm.name);

    ngx_http_limit_conn_cleanup_all(r->pool);
    return lccf->status_code;
    }
    //连接数量+1
    lc->conn++;
    }

    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
    "limit conn: %08Xi %d", node->key, lc->conn);

    ngx_shmtx_unlock(&shpool->mutex);
    //注册内存池清理函数
    cln = ngx_pool_cleanup_add(r->pool,
    sizeof(ngx_http_limit_conn_cleanup_t));
    if (cln == NULL) {
    return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    // 绑定内存清理函数
    cln->handler = ngx_http_limit_conn_cleanup;
    lccln = cln->data;

    lccln->shm_zone = limits[i].shm_zone;
    lccln->node = node;
    }

    return NGX_DECLINED;
    }
  1. 结点插入函数,限流模块是将每个连接都存进一颗红黑树中,nginx提供一个默认的插入函数,限流模块自己实现了一个插入函数。

3. 总结

以上就是nginx限流模块的基本实现,其他限制请求数的模块类似。