WHY Nginx Variable
其实完全可以把Nginx configuration file视为一种微型的编程语言,其语言风格深受shell和perl的影响。而编程语言的核心之一就是变量。
Nginx的变量类型只能是string(除开一些扩展module)。变量在nginx.conf中的声明和定义很简单:set $my_variable "value"
(当然,一些其他命令也能声明定义变量),之后通过$
符号转义使用即可。
- module:Nginx的世界其实就是由module组成,所有的命令都来自于不同的module(包括上面的
set
,其来自于标准modulengx_rewrite
)。module可以分为标准模块和第三方模块;
那么如何打印$
符号呢?很遗憾,在nginx中,没有直接可用于转义$
符号的方法。比较折中的方法是使用geo
命令将一个变量的值设置为$
,随后再对其进行引用(因为geo
不支持转义),如下:
geo $dollar {
default "$";
}
Variable Declaration
另外一个问题:变量的声明创建都是在什么时候?
在Nginx中,变量唯一能够被合法声明创建的时期就是在导入配置文件的配置期(configure time);因此,如果在变量未声明之前就进行使用,Nginx是会直接报错的;
configure time带来的一个特性是:只要一个变量被声明了,那么它在所有block中都是可见的。
但是请注意:不同的block中,变量名可能相同,但相互之间绝对不会影响,可以视为局部变量
server {
listen 8080;
location /bar {
echo "XXX $foo";
}
location /foo {
set $foo "hello";
echo "XXX $foo";
}
}
### 访问bar服务时,虽然foo变量可用,但只是一个局部变量
$ curl "http://localhost:8080/bar"
$ XXX
$
$ curl "http://localhost:8080/foo"
$ XXX hello
$
$ curl "http://localhost:8080/bar" #即使在/foo中变量已经被初始化了,但在/bar依旧是未初始化状态
$ XXX
Variable Lifetime
configure time已经进行了阐述,接下来就是关于变量生命期的一些需要注意的问题。
一言以蔽之:nginx变量的生命期只和发来的request的处理期一致,而和location block毫无关系。
因为nginx中存在可以调用其他location 的命令(比如echo_exec
),因此,如果nginx变量和location block相关的话,则当执行其他的location的时候,之前的变量就不存在了,这显然是错误的。
built-in Variable
上面小节中,用set
等类似的指令创建的变量称为用户自定义变量(user-defined variable);
其实Nginx也为用户创建了一些原生的,built-in的变量,比较常见的如下:
-
$uri
:request的对应服务(不包含query string);比如curl "http://localhost:8080/bar?a=1%20&b=2"
,则url
的值是/bar
; -
$request_uri
request除去IP-port的全部(包含query string);还是上面的例子,则request_uri
的值是/bar?a=1%20&b=2
; -
$arg_XXX
:request的query string中XXX对应的值;还是上面的例子,则arg_b
的值是2
;(注意,这是一个infinite variables,所以只能由nginx core创建)
注意:大部分built-in variable都是只读的,所以最好不要自己对这些变量赋值。
但也存在一些可写的built-in variable,常见的如下:
-
$args
:request的所有query_string;很有意思的是,只要修改了$args
,那么诸如$arg_XXX
等需要从args
中进行读取的变量,它们的值都会改变;
Container VS. Get/Set handler
所谓容器(Container),其意思就是,这里有一片空间,专门用来存储对应的变量。一个变量如果存储于某个容器中,则称它是被索引的(indexed);
但对于大多数built-in variable以及其他一些变量,它们是未被索引的,因此,对它们进行读写则需要执行特制的get handler和set handler函数;
这两者的区别在哪里?核心就在于:set handler和get handler都是实时的,不经存储的。比如,当读取$arg_XXX
变量时,系统会实时的扫描request的url,寻找XXX对应的值,而不是提前扫描之后将其存储起来;
Container for Cache
很有意思的是,对于一些module来说,它们会提前将变量的值保存起来,从而达到缓存的作用,这样可以避免上一节中多次调用get handler函数带来的额外开销。
但这也同时造成了一些困惑,比较经典的就是ngx_map模块的map函数。map $args $my_var
的作用是将args映射到my_var,但这个函数的特点是它只执行一次get handler,就把my_var的值缓存下来了。如下所示的例子,
map $args $my_var{
default "0";
special "1";
}
server {
listen 8080;
location /foo{
echo "original var : $my_var";
set $args "special";
echo "new var : $my_var";
}
location /bar{
echo "hello there";
}
}
$ curl "http://localhost/foo"
$ original var : 0
new var : 0 ### 因为my_var在request刚发送过来的时候就已经缓存好了,所以后续即使改变了$args,也不会对my_var造成影响
$
$ curl "http://localhost/foo?special"
$ original var : 1
new var : 1 ### 因为my_var在request刚发送过来的时候就已经缓存好了,所以后续即使改变了$args,也不会对my_var造成影响
- Context:上面的例子中,map规则是写在server的外部的,也就是在http block中,这是因为每个函数都有它自己的context规则。所以如果有不清楚的地方,应该及时查阅文档。
-
惰性计算:因为map直接定义在http block的原因,所以有人可能会认为所有的location都会对map内部执行一次计算,即使它们根本没有使用对应的变量,从而加大了计算开销。但其实不是这样的,对于ngx_map模块来说,它使用的是惰性计算策略,即,只有当变量真正被读写时,才会进行计算,所以在上面的例子中,
/bar
是不会对map进行计算的。
Main Request VS. Subrequest
在Nginx的世界中,其实存在着两类request,即main request和subrequest。
main request指的就是从HTTP客户端发来的一个完整的request。而subrequest的定义则相对抽象一些,它存在的意义只是为了将main request进行解耦,从而转化为多个子请求以更方便的执行。对应的conf file中,就是某个location中调用多个其他的location。(比较常见的是通过ngx_echo的echo_location进行subrequest调用)
但需要注意的是,main request和subrequest对应的location必须位于同一个server中(也就是配置文件所定义的同一个虚拟服务器中)。这样的限制,或者说特性,背后的原理就是:main request调用subrequest,其实就是调用另一个C函数罢了。因此main request调用subrequest是不涉及服务器的网络通信的,所以会很高效。
Shared Variable Container
前面提到过“局部变量”这个概念,意思是不同location中,即使变量名相同,但其也是不同的变量,不会相互影响。但在Nginx中,也存在一些module,它们使用的是共享机制,即,main request和subrequest之间,所用变量都会共享,相互影响。比较常见的就是ngx_auth_request模块,当main request使用该模块调用subrequest的时候,subrequest完全可以改变main request中变量的值。
shared variable container一定程度上减少了空间开销,但也因为location之间可以相互影响,所以会带来一些潜在的,不可预料的危险,因此大部分module采用的还是局部变量的分离机制。
# 关于shared variable container造成的困惑的一个经典的例子
map $args $my_var{
default "0"
/foo "1"
/bar "2"
}
server{
listen 8080;
location /foo{
auto_request /bar
echo "my_var in foo : $my_var"
}
location /bar{
echo "my_var in bar : $my_var"
}
}
$ curl "http://localhost:8080/foo"
$ my_var in foo : 2
# 1. auth_request会忽略subrequest的response body,只关注它的返回码,所以echo "my_var in bar : $my_var"没有显示
# 2. 最先读取my_var是在/bar中,此时my_var被映射为2,并且被缓存了!所以后续在/foo中读取时,my_var还是2
Confusion of Built-in Variable
built-in variable比较容易造成困惑的一点在于,不同的built-in variables在main request和subrequest中的表现可能是完全不一样的,所谓的表现,主要分为两类:对subrequest context是否敏感:
敏感:也就是说,当该变量在sub request进行读取时,其展现的是sub request中的上下文,包括$uri
, $args
等;
不敏感:也就是说,当该变量在sub request进行读取时,其展现的是main request中的上下文,包括$request_uri
, $request_mothod
等;以$request_uri
为例,其意思是未经加工的,原始的request中的query string,因此,不敏感是更符合其语义的,因为subrequest url毕竟已经进行了加工。
- 到底如何区别一个built-in variable是否敏感,一是观察其语义,二是...查文档吧
Empty Arg VS. Invalid Arg
对于request中未曾出现的arg,其在nginx中并非对应一个空字符串,相反,其实际上是一个特制的字串"invalid"或者"not found"。后续,当读取时,get handler再将这些特制的字串转换为空。
-
invalid:对于使用
set
等命令进行了声明,但并没有初始化的变量,其值就是"invalid"
; -
not found:对于query string中不存在的key,引用
$arg_XXX
等变量时,其值就是"not found"
网友评论