在使用nginx lua 时经常会使用ngx.var.varname 来获取变量,随着业务的越复杂,变量越来越多,发现服务的速度明显变慢了
本文分析了整个变量的获取和保存逻辑
- ngx_lua 获取变量
首先ngx_lua 获取变量调用的是 ngx_http_lua_var_get(lua_State *L)
46-static int
47ngx_http_lua_var_get(lua_State *L)
48{
49 ngx_http_request_t *r;
50 u_char *p, *lowcase;
51 size_t len;
52 ngx_uint_t hash;
53 ngx_str_t name;
54 ngx_http_variable_value_t *vv;
55
56#if (NGX_PCRE)
57 u_char *val;
58 ngx_uint_t n;
59 LUA_NUMBER index;
60 int *cap;
61#endif
62
63 r = ngx_http_lua_get_req(L);
64 if (r == NULL) {
65 return luaL_error(L, "no request object found");
66 }
...
111 p = (u_char *) lua_tolstring(L, -1, &len);
112
113 lowcase = lua_newuserdata(L, len);
114
115 hash = ngx_hash_strlow(lowcase, p, len);
116
117 name.len = len;
118 name.data = lowcase;
119 dd("variable name: %.*s", (int) len, lowcase);
120 vv = ngx_http_get_variable(r, &name, hash);
可见就是调用nginx的变量获取接口, 可以看到前面将变量名全部变为小写然后计算hash,ngx_lua是不区分大小写的
- nginx 变量的获取
546-ngx_http_variable_value_t *
547ngx_http_get_variable(ngx_http_request_t *r, ngx_str_t *name, ngx_uint_t key)
548{
549 ngx_http_variable_t *v;
550 ngx_http_variable_value_t *vv;
551 ngx_http_core_main_conf_t *cmcf;
552
553 cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);
554
555 v = ngx_hash_find(&cmcf->variables_hash, key, name->data, name->len);
556
- nginx 变量的保存结构
前面已经猜到nginx 的变量是保存在hash表中的, 主要的结构保存在ngx_hash.c 中
这里主要分析两个函数ngx_hash_find 和 ngx_hash_init
ngx_hash_find 就是典型的拉链式hash表的处理方式,先定位bucket, 然后挨个对比,那么效率就在于hash bucket的构建上了
12-void *
13ngx_hash_find(ngx_hash_t *hash, ngx_uint_t key, u_char *name, size_t len)
14{
15 ngx_uint_t i;
16 ngx_hash_elt_t *elt;
17
18#if 0
19 ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, 0, "hf:\"%*s\"", len, name);
20#endif
21
22 elt = hash->buckets[key % hash->size];
23
24 if (elt == NULL) {
25 return NULL;
26 }
27
28 while (elt->value) {
29
30 if (len != (size_t) elt->len) {
31 goto next;
32 }
33
34 for (i = 0; i < len; i++) {
35 if (name[i] != elt->name[i]) {
36 goto next;
37 }
38 }
39
40 return elt->value;
41
42 next:
43
44 elt = (ngx_hash_elt_t *) ngx_align_ptr(&elt->name[0] + elt->len,
45 sizeof(void *));
46 continue;
47 }
48
49 return NULL;
50}
下面我们来看一下 hash bucket的构建
// 用于计算存储一个变量所需要的空间
249#define NGX_HASH_ELT_SIZE(name) \
250 (sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *)))
251
252-ngx_int_t
253ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts)
254{
NGX_HASH_ELT_SIZE 是计算每个key 所占用的空间
每一个bucket 中key的保存如下
ngx_hash_elt_t
value | len | name |
---|---|---|
point to ngx_http_core_srv_conf_t | key.len(short) | key.data |
NULL(队尾) | key.len(short) | key.data |
因为key.len 是short 类型的所以需要在+2 然后对齐
nginx 对于变量的保存是存在hash 表中, 在配置中有两个参数
server_names_hash_bucket_size 每个bucket 的大小,决定着每个bucket 最多能放入多少个对象
server_names_hash_max_size 最大的bucket 数量
nginx 用拿现有的变量列表从一个hash size开始尝试, 及 尝试把所有符合条件 hash(key)%size 的key放到一个bucket 中,不过不能则增大size 直到 max_size,让然不行就会报错。
这中间就会有两个问题
1 这种尝试会使nginx 启动变慢
2 如果bucket size 过大会使太多的单个bucket 中存放了太多的对象导致,在运行中获取变量时的开销变大,相应的bucket 太小又会使size 很大消耗更多的内存
nginx 在这方面是选择了在参数内尽可能的减少内存的使用,但是在实际环境中增大内存来加快变量的访问时非常可取的也是。
252-ngx_int_t
253ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts)
{
...
// 1 首先check 一下当前bucekt size 是不是能放下所有的对象
270 for (n = 0; n < nelts; n++) {
271 ngx_uint_t var_size = NGX_HASH_ELT_SIZE(&names[n]) + sizeof(void *);
272 if (hinit->bucket_size < NGX_HASH_ELT_SIZE(&names[n]) + sizeof(void *))
273 {
274 ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0,
275 "could not build %s, you should "
276 "increase %s_bucket_size: %i",
277 hinit->name, hinit->name, hinit->bucket_size);
278 return NGX_ERROR;
279 }
280
283 if (NULL == names[n].key.data){
284 ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0, "ngx_hash_init error var %i", n);
285 continue;
286 }
}
...
// 接下来开始, 顺序尝试,看能否确定一个 size 后, 看能不能把符合hash%size的key 放入同一个桶中, 如果不能则继续增大size 直至max_size
311 if (hinit->max_size > 10000 && nelts && hinit->max_size / nelts < 100) {
312 start = hinit->max_size - 1000;
313 }
314 ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0,
315 "ngx_hash_init start confirm size... "
316 "name %s start(%i) max_size(%i)",
317 hinit->name, start, hinit->max_size);
318 if (hinit->max_size < 20*nelts)
319 hinit->max_size = 20*nelts;
320 for (size = start; size <= hinit->max_size; size++) {
321
322 ngx_memzero(test, size * sizeof(u_short));
323
324 for (n = 0; n < nelts; n++) {
325 if (names[n].key.data == NULL) {
326 continue;
327 }
328
329 key = names[n].key_hash % size;
330 test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
331
332#if 0
333 ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
334 "%ui: %ui %ui \"%V\"",
335 size, key, test[key], &names[n].key);
336#endif
337
338 if (test[key] > (u_short) bucket_size) {
339 goto next;
340 }
341 }
342 ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0,
343 "ngx_hash_init confirm size "
344 "name %s bucket_cnt(%i) var_cnt(%i) bucket_size(%i) max_size(%i)",
345 hinit->name, size, nelts, hinit->bucket_size, hinit->max_size);
346 goto found;
347
348 next:
349
350 continue;
351 }
352
353 size = hinit->max_size;
354
355 ngx_log_error(NGX_LOG_WARN, hinit->pool->log, 0,
356 "could not build optimal %s, you should increase "
357 "either %s_max_size: %i or %s_bucket_size: %i; "
358 "ignoring %s_bucket_size",
359 hinit->name, hinit->name, hinit->max_size,
360 hinit->name, hinit->bucket_size, hinit->name);
361
362found:
364 for (i = 0; i < size; i++) { // 每一个bucket 都至少有一个元素,没有value
365 test[i] = sizeof(void *);
366 }
...
// 确定了size 后面就开始根据元素来分配空间,创建hash表, 填充空间了
}
nginx 的hash 是静态表不能运行是添加或删除, 一种优化的方案是在构造hash表时根据所需要存的变量计算bucket size, 例如是平均值的几倍,这样避免业务变化后需要重新调整这参数,也避免了参数不匹配造成的性能问题。另一个就是在尝试size时 初始尝试一个较大值, 毕竟size 过小也会使单个bucket 中的元素过多影响访问性能,同事也能尽快的找到合适的size 加速度nginx 启动。
后续的填充逻辑有机会再详细分析。