目录
- 1. 概述
- 2. 动态变量的分类
- 2.1 按照变量名的确定性来分类
- 2.2 按照变量声明的来源分类
- 2.3 按照是否可以变更分类
- 2.4 按照是否可以缓存分类
- 2.5 按照变量的索引方式分类
- 3. 变量的使用
- 3.1 声明一个变量
- 3.1.1 支撑变量声明的nginx关键结构体
- 3.1.2 在配置文件中声明
- 3.1.3 在http的核心模块中声明
- 3.1.4 在模块中声明
- 3.1.5 是否还有第四种声明变量的方式:
- 3.2 索引一个变量
- 3.3 获取变量值
1. 概述
nginx提供了非常强大的动态变量的机制,通过动态变量,我们可以控制nginx的行为(如限流、acl验证、if判断、url rewrite等),可以输出nginx的请求信息和运行状态(如nginx的log中使用的动态变量。
nginx中的动态变量都是以$开头后面紧跟变量名的方式来使用的,可以在nginx配置文件中使用,也可以在openresty的lua脚本中使用,更可以在自研nginx模块中使用其他模块声明的变量。
nginx本身声明了非常多的内置变量,可以供我们使用,当然,nginx的内核框架也不妨碍我们定义自己的动态变量,开放给其他模块(譬如日志模块)使用。
本文将从源码层面来学习和了解nginx动态变量的使用方法和运行机制,以便对nginx的动态变量机制有一个比较深入的理解。在继续下文之前,本文对分析的范围故且做一个限定,本文只讨论nginx http模块下的动态变量机制,对于stream模块,mail模块等不在本文讨论范围之内。
2. 动态变量的分类
2.1 按照变量名的确定性来分类
按照声明变量的时候变量名是否确定来分类,可以分为:
- 普通变量 : 这种变量变量名提前可知,如:$remote_addr $body_bytes_sent等。
- 前缀匹配型变量 :这种变量变量名提前不可知,声明的时候只能确定变量名的前缀,如:$http_host, $http_referer等。总共包括http_、 sent_http_ 、 upstream_http_、 cookie_或者
arg_共5种类型的前缀变量。
2.2 按照变量声明的来源分类
按照声明变量的来源来分类,可以分为:
- 核心变量:由ngx_http_core_module和ngx_http_upstream_module定义的变量,如:$connect_host、 $connect_port、$upstream_addr等。
- 模块变量:由扩展模块声明的内置变量,如:ngx_http_slice_module声明的$slice_range,ngx_http_proxy_module声明的$proxy_host等,还包括其他第三方模块声明的变量。
- 配置文件变量:由nginx配置文件通过set指令声明的变量。
2.3 按照是否可以变更分类
按照变量是否可以被非声明它的模块变更来分类,可以分为:
- 可变更变量:不能由其他模块修改,如 $remote_addr $body_bytes_sent等。
- 不可变更变量:可以由其他模块修改,如通过set指令声明的变量。
nginx本身提供的内置变量决定部分是不可变更的。
2.4 按照是否可以缓存分类
按照声明的变量是可以缓存,可以分为:
- 不可缓存变量
- 可以缓存变量
这里再解释一下,而变量值缓存的地方就是在ngx_http_request_t中,也就是说每个变量值从nginx的框架上来说是属于某个ngx_http_request_t实例的,这个可以通过动态变量的获取函数的定义得到明证,如下:
ngx_http_variable_value_t *
ngx_http_get_indexed_variable(ngx_http_request_t *r, ngx_uint_t index);ngx_http_variable_value_t *
ngx_http_get_flushed_variable(ngx_http_request_t *r, ngx_uint_t index);ngx_http_variable_value_t *
ngx_http_get_variable(ngx_http_request_t *r, ngx_str_t *name, ngx_uint_t key);
以上三个版本的动态变量获取函数都是需要将ngx_http_request_t的指针作为上下文参数传入的。当变量被声明为可以缓存,那么在某个http request在求值完成后将缓存到ngx_http_request_t,下次在该请求的某个处理环节还需要这个变量的值的时候就直接从缓存中获取了而不需要再进行求值操作,从而可以优化nginx的处理性能。
2.5 按照变量的索引方式分类
按照变量的索引方式分类, 可以分为:
- hash 变量: 通过哈希表用名字进行查找的变量类型
- 不可hash 变量:不把这个变盘 has h 到散列表中
因为nginx非常注重性能,对内存的使用也极其“抠门”的应用。如果某个模块设计了 一个可选变量提供给其他模块使用,并且要求如果有其他模块使用该变量
就必须通过数组索引的方式再使用(即不能调用 ngx_http_get_variable方法来获取变
量值),这样,这个变量就不用浪费散列表的存储空间了。
3. 变量的使用
和强类型编程语言一样,nginx对变量的使用是分为两个阶段的,即变量声明阶段和变量读写阶段。
变量的声明阶段定义了变量的一些属性,包括名字、是否可缓存标记、是否可修改标记、是否可哈希标记,以及遍历读写的回调函数等信息。
变量的读写阶段才会真正给变量分配对应的存储空间用来存储变量值存储。
区别于正常的编程语言,nginx变量只能支持字符串类型。
为了加速变量的读写操作,nginx也会将需要用到的变量放到数组里面,通过数组的下标可以直接对变量的值进行读写操作,避免每次都需要通过hash表进行名字查找。
3.1 声明一个变量
3.1.1 支撑变量声明的nginx关键结构体
在详细阐述如何进行变量的声明前,有必要对支撑nginx变量机制中的相关结构体进行说明。先来看一下ngx_http_variable_s结构体的定义,它表示了一个nginx动态变量的声明:
struct ngx_http_variable_s {/* 声明的变量名称,不包含第一个$字符 */ngx_str_t name; /* must be first to build the hash *//* 设置变量值的回调函数 */ngx_http_set_variable_pt set_handler;/* 获取变量值的回调函数 */ngx_http_get_variable_pt get_handler;/* 变量声明模块自定义的上下文信息 */uintptr_t data;/* 变量的属性 包括: NGX_HTTP_VAR_CHANGEABLE NGX_HTTP_VAR_NOCACHEABLE NGX_HTTP_VAR_INDEXED NGX_HTTP_VAR_NOHASH NGX_HTTP_VAR_WEAK NGX_HTTP_VAR_PREFIX 等标记*/ngx_uint_t flags;/* 如果变量被索引到数组中了,它在数组中的序号 */ngx_uint_t index;
};
在ngx_http_core_main_conf_t也有对变量声明相关的类型定义,源码如下:
typedef struct {ngx_array_t servers; /* ngx_http_core_srv_conf_t */ngx_http_phase_engine_t phase_engine;ngx_hash_t headers_in_hash;ngx_hash_t variables_hash;ngx_array_t variables; /* ngx_http_variable_t */ngx_array_t prefix_variables; /* ngx_http_variable_t */ngx_uint_t ncaptures;ngx_uint_t server_names_hash_max_size;ngx_uint_t server_names_hash_bucket_size;ngx_uint_t variables_hash_max_size;ngx_uint_t variables_hash_bucket_size;ngx_hash_keys_arrays_t *variables_keys;ngx_array_t *ports;ngx_http_phase_t phases[NGX_HTTP_LOG_PHASE + 1];
} ngx_http_core_main_conf_t;
在ngx_http_core_main_conf_t的定义中,首先需要关注的是variables_keys,它会把nginx初始化期间声明的所有变量都注册在里面,在配置文件解析完成并且完成postconfiguration后,就会调用ngx_http_variables_init_vars,将nginx运行过程中将来会使用到的动态变量索引到variables数组中,同时将可哈希的变量添加到variables_hash哈希表中。
3.1.2 在配置文件中声明
在配置文件的http块及其各子块中,可以通过set指令来声明并为一个动态变量设置值,这个是大家日常用得最多的自定义变量的方式。我们可以看看ngx_http_rewrite_module的ngx_http_rewrite_set函数的实现,它负责解析set指令。函数源码如下:
static char *
ngx_http_rewrite_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{ngx_http_rewrite_loc_conf_t *lcf = conf;ngx_int_t index;ngx_str_t *value;ngx_http_variable_t *v;ngx_http_script_var_code_t *vcode;ngx_http_script_var_handler_code_t *vhcode;value = cf->args->elts;/* 判断传入的第一个参数的第一个字符是否为$,如果不是说明不是有效的变量名,则报错 */if (value[1].data[0] != '$') {ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,"invalid variable name \"%V\"", &value[1]);return NGX_CONF_ERROR;}value[1].len--;value[1].data++;/* 如果变量已经存在,并且变量属性声明为可以缓存,则返回原来已经声明的变量,否则则创建一个新的变量。*/v = ngx_http_add_variable(cf, &value[1],NGX_HTTP_VAR_CHANGEABLE|NGX_HTTP_VAR_WEAK);if (v == NULL) {return NGX_CONF_ERROR;}/* 获取上面添加的变量的数组索引下标 */index = ngx_http_get_variable_index(cf, &value[1]);if (index == NGX_ERROR) {return NGX_CONF_ERROR;}/* 如果没有设置对应的get_handler,那么对它设置默认值ngx_http_rewrite_var */if (v->get_handler == NULL) {v->get_handler = ngx_http_rewrite_var;v->data = index;}if (ngx_http_rewrite_value(cf, lcf, &value[2]) != NGX_CONF_OK) {return NGX_CONF_ERROR;}/* 如果v->set_handler不为空,说明是其他模块声明的变量并且可以允许修改,向动态script引擎中添加对应的动态脚本执行指令代码,该代码用于在rewrite阶段对上面的变量进行赋值。*/if (v->set_handler) {vhcode = ngx_http_script_start_code(cf->pool, &lcf->codes,sizeof(ngx_http_script_var_handler_code_t));if (vhcode == NULL) {return NGX_CONF_ERROR;}vhcode->code = ngx_http_script_var_set_handler_code;vhcode->handler = v->set_handler;vhcode->data = v->data;return NGX_CONF_OK;}/* 如果v->set_handler为空,意味着是set指令声明的变量,向动态script引擎中添加对应的动态脚本执行指令代码,该代码用于在rewrite阶段对上面的变量进行赋值。*/vcode = ngx_http_script_start_code(cf->pool, &lcf->codes,sizeof(ngx_http_script_var_code_t));if (vcode == NULL) {return NGX_CONF_ERROR;}vcode->code = ngx_http_script_set_var_code;vcode->index = (uintptr_t) index;return NGX_CONF_OK;
}
3.1.3 在http的核心模块中声明
nginx的http内核默认内置了大量的动态变量,在ngx_http_core_module的preconfiguration阶段,会声明这些内置动态变量,对应调用的函数是ngx_http_core_preconfiguration,而ngx_http_core_preconfiguration又转而调用ngx_http_variables_add_core_vars来声明动态变量,声明的动态变量将都放在cmcf->variables_keys中,实现代码如下:
ngx_int_t
ngx_http_variables_add_core_vars(ngx_conf_t *cf)
{ngx_http_variable_t *cv, *v;ngx_http_core_main_conf_t *cmcf;cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);/* 初始化variables_keys,variables_keys用来存放所有声明的动态变量列表 */cmcf->variables_keys = ngx_pcalloc(cf->temp_pool,sizeof(ngx_hash_keys_arrays_t));if (cmcf->variables_keys == NULL) {return NGX_ERROR;}cmcf->variables_keys->pool = cf->pool;cmcf->variables_keys->temp_pool = cf->pool;if (ngx_hash_keys_array_init(cmcf->variables_keys, NGX_HASH_SMALL)!= NGX_OK){return NGX_ERROR;}if (ngx_array_init(&cmcf->prefix_variables, cf->pool, 8,sizeof(ngx_http_variable_t))!= NGX_OK){return NGX_ERROR;}/*ngx_http_core_variables定义了内置动态变量的列表, 通过遍历该列表,向nginx变量管理框架添加所有这些变量。*/for (cv = ngx_http_core_variables; cv->name.len; cv++) {v = ngx_http_add_variable(cf, &cv->name, cv->flags);if (v == NULL) {return NGX_ERROR;}*v = *cv;}return NGX_OK;
}
在添加完变量后, 这个函数和ngx_http_rewrite_set有一点不一样的地方,ngx_http_rewrite_set是有ngx_http_get_variable_index调用的,而ngx_http_variables_add_core_vars却没有,为什么?因为ngx_http_variables_add_core_vars只是声明变量,至于这个变量在运行过程中用或是不用还不能确定,要等到其他模块或者nginx配置文件有地方引用这个变量的时候才能知道,但是此时因为还没有解析配置文件是不知道的;而rewrite模块中的ngx_http_rewrite_set函数对应的set指令,它不光是声明一个变量,同时也是是对变量进行赋值操作,所以在这里对变量进行了索引操作,变量的索引的具体解析放在3.2节中。
3.1.4 在模块中声明
在nginx的扩展模块或者自研模块中进行声明其实和http核心模块差不多,我们以ngx_http_slice_module模块为例,关于ngx_http_slice_module模块的解析可以查看[[nginx slice模块的源码分析]],其中slice_range动态变量声明的源码如下:
static ngx_int_t
ngx_http_slice_add_variables(ngx_conf_t *cf)
{ngx_http_variable_t *var;var = ngx_http_add_variable(cf, &ngx_http_slice_range_name, 0);if (var == NULL) {return NGX_ERROR;}var->get_handler = ngx_http_slice_range_variable;return NGX_OK;
}
这里声明了一个名字为slice_range的只读变量,当然如果需要声明一个可读写变量,只要在调用ngx_http_add_variable的时候传递NGX_HTTP_VAR_CHANGEABLE即可,并且给ngx_http_variable_t设置set_handler回调函数,代码举例如下:
static ngx_int_t
ngx_http_writable_add_variable(ngx_conf_t *cf)
{ngx_http_variable_t *v;ngx_str_t name = ngx_string( "writable_variable" );v = ngx_http_add_variable( cf, &name, NGX_HTTP_VAR_NOCACHEABLE|NGX_HTTP_VAR_CHANGEABLE );if( v == NULL ){return NGX_ERROR;}v->get_handler = ngx_http_writable_get_variable;v->set_handler = ngx_http_writable_set_variable;v->data = 0;return NGX_OK;
}
3.1.5 是否还有第四种声明变量的方式:
上面把nginx添加动态变量声明的三种方式进行了详细的说明,那么是不是还有第四中方式呢?我想到的是好像可以通过openresty lua脚本来声明。那openresty是不是可以在openresty中用lua代码来声明变量呢?
正确的答案是不可以的,openresty只能对已经通过上述三种方式声明过的变量进行读写访问,不能自己声明变量,说白了openresty lua模块自己也是一个nginx中的特殊的模块,因为动态变量的声明只能是在nginx配置文件解析前,而openresty lua模块的执行至少是在配置文件解析之后,而且一般是在http request发生的时候,所以openresty是无法声明nginx动态变量的。
至于是不是还有其他方式,大家有知道的,也欢迎大家提供一下。
3.2 索引一个变量
3.3 获取变量值
深入理解nginx的动态变量机制(完整版)