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限流模块的基本实现,其他限制请求数的模块类似。