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不支持转义),如下:
1 | geo $dollar { |
Variable Declaration
另外一个问题:变量的声明创建都是在什么时候?
在Nginx中,变量唯一能够被合法声明创建的时期就是在导入配置文件的配置期(configure time);因此,如果在变量未声明之前就进行使用,Nginx是会直接报错的;
configure time带来的一个特性是:只要一个变量被声明了,那么它在所有block中都是可见的。
但是请注意:不同的block中,变量名可能相同,但相互之间绝对不会影响,可以视为局部变量
1 | server { |
1 | ### 访问bar服务时,虽然foo变量可用,但只是一个局部变量 |
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_urirequest除去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的值缓存下来了。如下所示的例子,
1 | map $args $my_var{ |
1 | $ curl "http://localhost/foo" |
- 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是不涉及服务器的网络通信的,所以会很高效。
subrequest的高效性也意味着,高消耗的网络IO或者磁盘IO都应该尽量委托给subrequest来执行。
Shared Variable Container
前面提到过“局部变量”这个概念,意思是不同location中,即使变量名相同,但其也是不同的变量,不会相互影响。但在Nginx中,也存在一些module,它们使用的是共享机制,即,main request和subrequest之间,所用变量都会共享,相互影响。比较常见的就是ngx_auth_request模块,当main request使用该模块调用subrequest的时候,subrequest完全可以改变main request中变量的值。
shared variable container一定程度上减少了空间开销,但也因为location之间可以相互影响,所以会带来一些潜在的,不可预料的危险,因此大部分module采用的还是局部变量的分离机制。
1 | # 关于shared variable container造成的困惑的一个经典的例子 |
1 | $ curl "http://localhost:8080/foo" |
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"