掘金 后端 ( ) • 2024-05-17 10:12

变量的类型

变量有两种类型,分别是普通类型和复杂类型(complex variable)

普通类型变量

普通的变量还有细化的类型,通过不同的宏组合实现不同的功能。

ngx_http_add_variable 和 ngx_http_get_variable可以认为是变量的添加和变量的value获取的主入口。

宏 作用 NGX_HTTP_VAR_CHANGEABLE 此变量能重复定义,也可以使用set的指令修改 NGX_HTTP_VAR_NOCACHEABLE 每次都得使用get_handler获取值,不能使用缓存 NGX_HTTP_VAR_PREFIX 变量是一个前缀,比如$arg_这样的变量(用户获取请求的query参数) NGX_HTTP_VAR_WEAK 通过set修改或者新建的变量,都是属于WEAK类型的变量。 NGX_HTTP_VAR_NOHASH 只能通过索引访问,不能通过变量的名字访问 NGX_HTTP_VAR_INDEXED 变量可以通过索引快速从数组中获取(存在于cmcf->variables中)

cmcf->variables 和 cmcf->variables_hash 整个配置中,维护变量的两个数据结构。其中cmcf->variables是一个数组,里面记录了每个变量的get_handler、set_handler和flags。假设一个场景,我们需要根据变量的名字快速的从这个数组中获取变量的get_handler,在没有cmcf->variables_hash的时候,我们需要遍历这个cmcf->variables数组,这样效率会很低。因此需要由一个hash表格,记录每个变量(key)对应在cmcf->variables 的索引。

image-20240515162313875.png

如果变量是前缀类型的,那么在cmcf->variables_keys中不存在的,而是在cmcf->prefix_variables中维护(并且没有快速的hash索引)

NGX_HTTP_VAR_INDEXED

在http block解析的时候,ngx_http_variables_init_vars函数会给cmcf->variables_hash中的av设置一个索引。

av->flags |= NGX_HTTP_VAR_INDEXED;

前缀变量肯定不是NGX_HTTP_VAR_INDEXED类型的,因为前缀变量cmcf->prefix_variables中维护的。

NGX_HTTP_VAR_PREFIX

前缀类型的变量,ngx_http_add_variable添加变量的时候会调用ngx_http_add_prefix_variable把前缀变量放到cmcf->prefix_variables。

NGX_HTTP_VAR_NOCACHEABLE

所谓的Cache其实是一个优化的手段。如果变量设置了NGX_HTTP_VAR_NOCACHEABLE,那么每次获取变量的值都需要调用get_handler获取。默认的变量(没有强制设置NGX_HTTP_VAR_NOCACHEABLE)都在Cache中。那么Cache是什么呢?Cache其实是请求r生命周期中的variables数组里面专门针对Indexed类型变量优化的字段,如果r->variables中存在,那么就不需要再调用get_handler了。

ngx_http_variable_value_t *
ngx_http_get_flushed_variable(ngx_http_request_t *r, ngx_uint_t index)
{
    ngx_http_variable_value_t  *v;
​
    v = &r->variables[index];
​
    if (v->valid || v->not_found) {
        if (!v->no_cacheable) { // cache的变量,直接返回value
            return v;
        }
​
        v->valid = 0;
        v->not_found = 0;
    }
​
    return ngx_http_get_indexed_variable(r, index);
}

NGX_HTTP_VAR_CHANGEABLE

如果变量没有这个标志,那么变量不能在配置文件中使用set或者其他模块中重复定义。比如$upstream_response_length这个变量,就不能使用set 设置,否则就会报错

nginx: [emerg] the duplicate "upstream_response_length" variable in xxxxx

NGX_HTTP_VAR_WEAK

目前Nginx中,只有set指令覆盖或者新建的变量,才会设置为WEAK。有很多的文章说NGX_HTTP_VAR_WEAK是不会使用get_handler获取值。其实这里只说对了一半,原因是因为set的机制本来就会覆盖原始的获取变量的方法,即使有get_handler,也不会使用get_handler获取了(因为set指令的求值会优先于get_handler,具体看Nginx的set指令)

NGX_HTTP_VAR_NOHASH

字面的意思就是,这个变量只能通过索引获取,不能通过变量的名字获取(不能通过cmcf->variables_hash查询到key的index)。也就是说,这个变量只能遍历数组,一个个比较后才能获取到。

image-20240516180939215.png

NGX_HTTP_VAR_NOHASH 类型的变量是不会被添加到哈希表中的。这种类型的变量通常是动态生成的,或者是不需要快速查找的。例如,一些只在特定上下文中使用的变量,或者一些只在配置解析阶段使用的变量,可能就会被标记为 NGX_HTTP_VAR_NOHASH

NGX_HTTP_VAR_NOHASH类型的变量,在openresty中,通过ngx.var.xxx(xxx是变量的名字)是获取不到的,因为openresty的这个接口仅仅支持通过变量的名字获取到变量的值。但是在日志模块中,log_format中是能够获取到NOHASH变量的,因此日志模块在compile的时候,就获取index,日志在真实输出的时候就可以拿到了。

image-20240516200632301.png

以HTTP的upstream模块为例

upstream模块执行preconfiguration阶段的时候,就会调用ngx_http_upstream_add_variables为cf(cf可以理解为整个Nginx的配置的指针)添加upstream模块所有的变量。最终还是会调用ngx_http_add_variable添加变量,ngx_http_upstream_add_variables添加变量的模式,是所有HTTP模块的统一方案,若有第三方模块需要添加变量参考这个方式即可。

static ngx_int_t
ngx_http_upstream_add_variables(ngx_conf_t *cf)
{
    ngx_http_variable_t  *var, *v;
​
    for (v = ngx_http_upstream_vars; v->name.len; v++) {
        var = ngx_http_add_variable(cf, &v->name, v->flags);
        if (var == NULL) {
            return NGX_ERROR;
        }
​
        var->get_handler = v->get_handler;
        var->data = v->data;
    }
​
    return NGX_OK;
}

复杂(complex)类型变量

复杂类型的官方文档是这样定义的

A complex value, despite its name, provides an easy way to evaluate expressions which can contain text, variables, and their combination

也就是说,复杂类型可以是变量和text的集合,比如${status}_example${upstream_reponse_time}

特性如下:

  • 复杂值通常是通过配置指令如 set, map, if 等构建的,它们可以包含普通变量、文本和特殊字符的组合。
  • 它们可以在运行时根据需要进行计算和构建。
  • 访问或计算复杂值的开销通常比普通变量大,因为它们可能需要在每次请求时动态生成。
  • 复杂值可以用来创建新的变量,或者在 Nginx 的 rewrite 指令等操作中使用。

但是其使用方式很简单(不考虑实现细节),只需要两个接口就可以。一是编译complex variable类型(ngx_http_compile_complex_value),二是complex variable求值(ngx_http_complex_value)。

以access_log指令为例子,access_log可以配置if=xxxx来实现,根据某个条件被满足才输出access log的日志。比如access_log /var/log/nginx/error.log combined if=$loggable_user_agent;中,loggable_user_agent会被编译为复杂类型。

complex variable编译

if (ngx_strncmp(value[i].data, "if=", 3) == 0) {
    s.len = value[i].len - 3;
    s.data = value[i].data + 3;
​
    ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t));
​
    ccv.cf = cf;
    ccv.value = &s;
    ccv.complex_value = ngx_palloc(cf->pool,
                                   sizeof(ngx_http_complex_value_t));
    if (ccv.complex_value == NULL) {
        return NGX_CONF_ERROR;
    }
​
    if (ngx_http_compile_complex_value(&ccv) != NGX_OK) {
        return NGX_CONF_ERROR;
    }
​
    log->filter = ccv.complex_value;
​
    continue;
}

complex variable 求值

if (log[l].filter) {
    if (ngx_http_complex_value(r, log[l].filter, &val) != NGX_OK) {
        return NGX_ERROR;
    }
​
    if (val.len == 0 || (val.len == 1 && val.data[0] == '0')) {
        continue;
    }
}

求值后,就可以使用val去执行其他的逻辑了。可以看出access_log的逻辑,只有当if后面的变量的值为0的时候,日志才不打印,其他的时候都是要打印日志的。

Nginx 的set指令

set指令可以重新定义一个变量,或者修改一个变量。比如

set $arg_foo "hhhh";

那么变量$arg_foo的值,会在请求的rewrite阶段进行求值,具体的调用路径为

image-20240516115540140.png

最终是在ngx_http_script_set_var_code完成对变量$arg_foo的赋值。那么就有疑问了,赋值的来源是什么呢?是在set指令解析的时候,就已经确定了。在ngx_http_rewrite_set里面,有一个流程,就是给变量分配一个index

image-20240516154437299.png

其中vcode就是ngx_http_script_set_var_code执行时候的code。这里就可以解释为什么r->variables数组里面就已经可以获取到$arg_foo

image-20240516154529053.png

最后,假设我们没有使用set的指令配置$arg_foo的变量。$arg_还是一个特殊的前缀变量,也有自己的get_handler去获取,但是因为我们使用了set指令配置了,所以就不会调用get_handler去获取了。