转自:http://blog.sina.com.cn/s/blog_6d579ff40100xm7t.html
大多数 Nginx 新手都会频繁遇到这样一个困惑,那就是当同一个 location 配置块使用了多个 Nginx 模块的配置指令时,这些指令的执行顺序很可能会跟它们的书写顺序大相径庭。于是许多人选择了“试错法”,然后他们的配置文件就时常被改得一片狼藉。这个系列的教程就旨在帮助读者逐步地理解这些配置指令背后的执行时间和先后顺序的奥秘。
现在就来看这样一个令人困惑的例子:
-
location /test {
-
set $a 32;
-
echo $a;
-
set $a 56;
-
echo $a;
-
}
从这个例子的本意来看,我们期望的输出是一行 32 和一行 56,因为我们第一次用 配置指令输出了 $a变量的值以后,又紧接着使用 配置指令修改了 $a. 然而不幸的是,事实并非如此:
-
curl 'http://localhost:8080/test
-
56
-
56
我们看到,语句 set $a 56 似乎在第一条 echo $a 语句之前就执行过了。这究竟是为什么呢?难道我们遇到了 Nginx 中的一个 bug?
显然,这里并没有 Nginx 的 bug;要理解这里发生的事情,就首先需要知道 Nginx 处理每一个用户请求时,都是按照若干个不同阶(phase)依次处理的。Nginx 的请求处理阶段共有 11 个之多,我们先介绍其中 3 个比较常见的。按照它们执行时的先后顺序,依次是 rewrite 阶段、access 阶段以及 content 阶段(后面我们还有机会见到其他更多的处理阶段)。
所有 Nginx 模块提供的配置指令一般只会注册并运行在其中的某一个处理阶段。比如上例中的 指令就是在 rewrite 阶段运行的,而 指令就只会在 content 阶段运行。前面我们已经知道,在单个请求的处理过程中,rewrite 阶段总是在 content 阶段之前执行,因此属于 rewrite 阶段的配置指令也总是会无条件地在 content 阶段的配置指令之前执行。于是在同一个 location 配置块中, 指令总是会在 指令之前执行,即使我们在配置文件中有意把 语句写在 语句的后面。
回到刚才那个例子,
-
set $a 32;
-
echo $a;
-
set $a 56;
-
echo $a;
实际的执行顺序应当是
-
set $a 32;
-
set $a 56;
-
echo $a;
-
echo $a;
即先在 rewrite 阶段执行完这里的两条 赋值语句,然后再在后面的 content 阶段依次执行那两条 语句。分属两个不同处理阶段的配置指令之间是不能穿插着运行的。为了进一步验证这一点,我们不妨借助 Nginx 的“调试日志”来一窥 Nginx 的实际执行过程。
因为这是我们第一次提及 Nginx 的“调试日志”,所以有必要先简单介绍一下它的启用方法。调试日志默认是禁用的,因为它会引入比较大的运行时开销,让 Nginx 服务器显著变慢。一般我们需要重新编译和构造 Nginx 可执行文件,并且在调用 Nginx 源码包提供的 ./configure 脚本时传入 --with-debug 命令行选项。例如我们下载完 Nginx 源码包后在 Linux 或者 Mac OS X 等系统上构建时,典型的步骤是这样的:
-
tar xvf nginx-1.0.10.tar.gz
-
cd nginx-1.0.10/
-
./configure --with-debug
-
make
-
sudu make install
当我们启用 --with-debug 选项重新构建好调试版的 Nginx 之后,还需要同时在配置文件中通过标准的 配置指令为错误日志使用 debug 日志级别(这同时也是最低的日志级别):
-
error_log logs/error.log debug
这里重要的是 指令的第二个参数,debug,而前面第一个参数是错误日志文件的路径,logs/error.log. 当然,你也可以指定其他路径,但后面我们会检查这个文件的内容,所以请特别留意一下这里实际配置的文件路径。
现在我们重新启动 Nginx(注意,如果 Nginx 可执行文件也被更新过,仅仅让 Nginx 重新加载配置是不够的,需要关闭再启动 Nginx 主服务进程),然后再请求一下我们刚才那个示例接口:
点击(此处)折叠或打开
-
$ curl 'http://localhost:8080/test'
-
56
-
56
现在可以检查一下前面配置的 Nginx 错误日志文件中的输出。因为文件中的输出比较多(在我的机器上有 700 多行),所以不妨用 grep 命令在终端上过滤出我们感兴趣的部分:
-
grep -E 'http (output filter|script (set|value))' logs/error.log
在我机器上的输出是这个样子的(为了方便呈现,这里对 grep 命令的实际输出作了一些简单的编辑,略去了每一行的行首时间戳):
-
[debug] 5363#0: *1 http script value: "32"
-
[debug] 5363#0: *1 http script set $a
-
[debug] 5363#0: *1 http script value: "56"
-
[debug] 5363#0: *1 http script set $a
-
[debug] 5363#0: *1 http output filter "/test?"
-
[debug] 5363#0: *1 http output filter "/test?"
-
[debug] 5363#0: *1 http output filter "/test?"
这里需要稍微解释一下这些调试信息的具体含义。 配置指令在实际运行时会打印出两行以 http script 起始的调试信息,其中第一行信息是 语句中被赋予的值,而第二行则是 语句中被赋值的 Nginx 变量名。于是上面首先过滤出来的
-
[debug] 5363#0: *1 http script value: "32"
-
[debug] 5363#0: *1 http script set $a
这两行就对应我们例子中的配置语句
而接下来这两行调试信息
-
[debug] 5363#0: *1 http script value: "56"
-
[debug] 5363#0: *1 http script set $a
则对应配置语句
此外,凡在 Nginx 中输出响应体数据时,都会调用 Nginx 的所谓“输出过滤器”(output filter),我们一直在使用的 指令自然也不例外。而一旦调用 Nginx 的“输出过滤器”,便会产生类似下面这样的调试信息:
-
[debug] 5363#0: *1 http output filter "/test?"
当然,这里的 "/test?" 部分对于其他接口可能会发生变化,因为它显示的是当前请求的 URI. 这样联系起来看,就不难发现,上例中的那两条 语句确实都是在那两条 语句之前执行的。
细心的读者可能会问,为什么这个例子明明只使用了两条 语句进行输出,但却有三行 http output filter 调试信息呢?其实,前两行 http output filter 信息确实分别对应那两条 语句,而最后那一行信息则是对应 模块输出指示响应体末尾的结束标记。正是为了输出这个特殊结束标记,才会多出一次对 Nginx “输出过滤器”的调用。包括 在内的许多模块在输出响应体数据流时都具有此种行为。
现在我们就不会再为前面那个例子输出两行一模一样的 56 而感到惊讶了。我们根本没有机会在第二条 语句之前用 输出。幸运的是,仍然可以借助一些小技巧来达到最初的目的:
点击(此处)折叠或打开
-
location /test {
-
set $a 32;
-
set $saved_a $a;
-
set $a 56;
-
-
echo $saved_a;
-
echo $a;
-
}
此时的输出便符合那个问题示例的初衷了:
-
$ curl 'http://localhost:8080/test'
-
32
-
56
这里通过引入新的用户变量 $saved_a,在改写 $a 之前及时保存了 $a 的初始值。而对于多条 指令而言,它们之间的执行顺序是由 模块来保证与书写顺序相一致的。同理, 模块自身也会保证它的多条 指令之间的执行顺序。
细心的读者应当发现,我们在 Nginx 变量的示例中已经广泛使用了这种技巧,来绕过因处理阶段而引起的指令执行顺序上的限制。
看到这里,有的读者可能会问:“那么我在使用一条陌生的配置指令之前,如何知道它究竟运行在哪一个处理阶段呢?”答案是:查看该指令的文档(当然,高级开发人员也可以直接查看模块的 C 源码)。在许多模块的文档中,都会专门标记其配置指令所运行的具体阶段。例如 指令的文档中有这么一行:
这一行便是说,当前配置指令运行在 content 阶段。如果你使用的 Nginx 模块碰巧没有指示运行阶段的文档,可以直接联系该模块的作者请求补充。不过,值得一提的是,并非所有的配置指令都与某个处理阶段相关联,例如我们先前在 Nginx 变量 中提到过的 指令及 指令。这些不与处理阶段相关联的配置指令基本上都是“声明性的”(declarative),即不直接产生某种动作或者过程。Nginx 的作者 Igor Sysoev 在公开场合曾不止一次地强调,Nginx 配置文件所使用的语言本质上是“声明性的”,而非“过程性的”(procedural)。
我们前面已经知道,当 set 指令用在 location 配置块中时,都是在当前请求的 rewrite 阶段运行的。事实上,在此上下文中,ngx_rewrite 模块中的几乎全部指令,都运行在 rewrite 阶段,包括 以前介绍过的 rewrite 指令。不过,值得一提的是,当这些指令使用在 server 配置块中时,则会运行在一个我们尚未提及的更早的处理阶段,server-rewrite 阶段。
以前介绍过的 ngx_set_misc 模块的 set_unescape_uri 指令同样也运行在 rewrite 阶段。特别地,ngx_set_misc 模块的指令还可以和 ngx_rewrite 的指令混合在一起依次执行。我们来看这样的一个例子:
点击(此处)折叠或打开
-
location /test {
-
set $a "hello%20world";
-
set_unescape_uri $b $a;
-
set $c "$b!";
-
-
echo $c;
-
}
访问这个接口可以得到:
点击(此处)折叠或打开
我们看到,set_unescape_uri 语句前后的 set 语句都按书写时的顺序一前一后地执行了。
为了进一步确认这一点,我们不妨再检查一下 Nginx 的“调试日志”:
点击(此处)折叠或打开
-
grep -E 'http script (value|copy|set)' t/servroot/logs/error.log
过滤出来的调试日志信息如下所示:
点击(此处)折叠或打开
-
[debug] 11167#0: *1 http script value: "hello%20world"
-
[debug] 11167#0: *1 http script set $a
-
[debug] 11167#0: *1 http script value (post filter): "hello world"
-
[debug] 11167#0: *1 http script set $b
-
[debug] 11167#0: *1 http script copy: "!"
-
[debug] 11167#0: *1 http script set $c
开头的两行信息:
点击(此处)折叠或打开
-
[debug] 11167#0: *1 http script value: "hello%20world"
-
[debug] 11167#0: *1 http script set $a
对应下面的配置语句:
点击(此处)折叠或打开
再下面的两行:
点击(此处)折叠或打开
-
[debug] 11167#0: *1 http script value (post filter): "hello world"
-
[debug] 11167#0: *1 http script set $b
对应配置语句:
点击(此处)折叠或打开
我们看到第一行信息与 set 指令略有区别,多了 "(post filter)" 这个标记,而且最后显示出 URI 解码操作确实如我们期望的那样工作了,即 "hello%20world" 在这里被成功解码为 "hello world". 而最后两行调试信息:
点击(此处)折叠或打开
-
[debug] 11167#0: *1 http script copy: "!"
-
[debug] 11167#0: *1 http script set $c
则对应最后一条set语句:
点击(此处)折叠或打开
注意,因为这条指令在为 $c 变量赋值时使用了“变量插值”功能,所以第一行调试信息是以 http script copy 起始的,后面则是拼接到最终取值的字符串常量 "!". 把这些调试信息联系起来看,我们不难发现,这些配置指令的实际执行顺序是:
点击(此处)折叠或打开
-
set $a "hello%20world";
-
set_unescape_uri $b $a;
-
set $c "$b!"
这与它们在配置文件中的书写顺序完全一致。
我们在前面文章初识了第三方模块 ngx_lua,它提供的 set_by_lua 配置指令也和 ngx_set_misc 模块的指令一样,可以和 ngx_rewrite 模块的指令混合使用。set_by_lua 指令支持通过一小段用户 Lua 代码来计算出一个结果,然后赋给指定的 Nginx 变量。和 set 指令相似,set_by_lua 指令也有自动创建不存在的 Nginx 变量的功能。下面我们就来看一个 set_by_lua 指令与 set 指令混合使用的例子:
点击(此处)折叠或打开
-
location /test {
-
set $a 32;
-
set $b 56;
-
set_by_lua $c "return ngx.var.a + ngx.var.b";
-
set $equation "$a + $b = $c";
-
-
echo $equation;
-
}
这里我们先将 $a 和 $b 变量分别初始化为 32 和 56,然后利用 set_by_lua 指令内联一行我们自己指定的 Lua 代码,计算出 Nginx 变量 $a 和 $b 的“代数和”(sum),并赋给变量 $c,接着利用“变量插值”功能,把变量 $a、 $b 和 $c 的值拼接成一个字符串形式的等式,赋予变量 $equation,最后再用 echo 指令输出 $equation 的值。这个例子值得注意的地方是:首先,我们在 Lua 代码中是通过 ngx.var.VARIABLE 接口来读取 Nginx 变量 $VARIABLE 的;其次,因为 Nginx 变量的值只有字符串这一种类型,所以在 Lua 代码里读取 ngx.var.a 和 ngx.var.b 时得到的其实都是 Lua 字符串类型的值 "32" 和 "56";接着,我们对两个字符串作加法运算会触发 Lua 对加数进行自动类型转换(Lua 会把两个加数先转换为数值类型再求和);然后,我们在 Lua 代码中把最终结果通过 return 语句返回给外面的 Nginx 变量 $c;最后,ngx_lua 模块在给 $c 实际赋值之前,也会把 return 语句返回的数值类型的结果,也就是 Lua 加法计算得出的“和”,自动转换为字符串(这同样是因为 Nginx 变量的值只能是字符串)。这个例子的实际运行结果符合我们的期望:
点击(此处)折叠或打开
于是这验证了 set_by_lua 指令确实也可以和 set 这样的 ngx_rewrite 模块提供的指令混合在一起工作。
还有不少第三方模块,例如 介绍过的 ngx_array_var ,以及后面即将接触到的用于加解密用户会话(session)的 ngx_encrypted_session,也都可以和 ngx_rewrite 模块的指令无缝混合工作。标准 ngx_rewrite 模块的应用是如此广泛,所以能够和它的配置指令混合使用的第三方模块是幸运的。事实上,上面提到的这些第三方模块都采用了特殊的技术,将它们自己的配置指令“注入”到了 ngx_rewrite 模块的指令序列中(它们都借助了 Marcus Clyne 编写的第三方模块 ngx_devel_kit)。换句话说,更多常规的在 Nginx 的 rewrite 阶段注册和运行指令的第三方模块就没那么幸运了。这些“常规模块”的指令虽然也运行在 rewrite 阶段,但其配置指令和 ngx_rewrite 模块(以及同一阶段内的其他模块)都是分开独立执行的。在运行时,不同模块的配置指令集之间的先后顺序一般是不确定的(严格来说,一般是由模块的加载顺序决定的,但也有例外的情况)。比如 A 和 B 两个模块都在 rewrite 阶段运行指令,于是要么是 A 模块的所有指令全部执行完再执行 B 模块的那些指令,要么就是反过来,把 B 的指令全部执行完,再去运行 A 的指令。除非模块的文档中有明确的交待,否则用户一般不应编写依赖于此种不确定顺序的配置。
如前文所述,除非像 ngx_set_misc 模块那样使用特殊技术,其他模块的配置指令即使是在 rewrite 阶段运行,也不能和 ngx_rewrite 模块的指令混合使用。不妨来看几个这样的例子。
第三方模块 ngx_headers_more 提供了一系列配置指令,用于操纵当前请求的请求头和响应头。其中有一条名叫 more_set_input_headers 的指令可以在 rewrite 阶段改写指定的请求头(或者在请求头不存在时自动创建)。这条指令总是运行在 rewrite 阶段的末尾,该指令的文档中有这么一行标记:
phase: rewrite tail
其中的 rewrite tail 的意思就是 rewrite 阶段的末尾。
既然运行在 rewrite 阶段的末尾,那么也就总是会运行在 ngx_rewrite 模块的指令之后,即使我们在配置文件中把它写在前面。
阅读(3980) | 评论(0) | 转发(0) |