Nginx 配置指令的执行顺序(九)
紧接在 server-rewrite
阶段后边的是 find-config
阶段。这个阶段并不支持 Nginx 模块注册处理程序,而是由 Nginx 核心来完成当前请求与 location
配置块之间的配对工作。换句话说,在此阶段之前,请求并没有与任何 location
配置块相关联。因此,对于运行在 find-config
阶段之前的 post-read
和 server-rewrite
阶段来说,只有 server
配置块以及更外层作用域中的配置指令才会起作用。这就是为什么只有写在server
配置块中的 ngx_rewrite 模块的指令才会运行在 server-rewrite
阶段,这也是为什么前面所有例子中的 ngx_realip 模块的指令也都特意写在了 server
配置块中,以确保其注册在 post-read
阶段的处理程序能够生效。
当 Nginx 在 find-config
阶段成功匹配了一个 location
配置块后,会立即打印一条调试信息到错误日志文件中。我们来看这样的一个例子:
location /hello {
echo "hello world";
}
如果启用了 Nginx 的“调试日志”,那么当请求 /hello
接口时,便可以在 error.log
文件中过滤出下面这一行信息:
$ grep 'using config' logs/error.log
[debug] 84579#0: *1 using configuration "/hello"
我们有意省略了信息行首的时间戳,以便放在这里。
运行在 find-config
阶段之后的便是我们的老朋友 rewrite
阶段。由于 Nginx 已经在 find-config
阶段完成了当前请求与 location
的配对,所以从 rewrite
阶段开始,location
配置块中的指令便可以产生作用。前面已经介绍过,当 ngx_rewrite 模块的指令用于 location
块中时,便是运行在这个 rewrite
阶段。另外,ngx_set_misc 模块的指令也是如此,还有 ngx_lua 模块的 set_by_lua 指令和 rewrite_by_lua 指令也不例外。
rewrite
阶段再往后便是所谓的 post-rewrite
阶段。这个阶段也像 find-config
阶段那样不接受 Nginx 模块注册处理程序,而是由 Nginx 核心完成 rewrite
阶段所要求的“内部跳转”操作(如果 rewrite
阶段有此要求的话)。先前在 (二) 中已经介绍过了“内部跳转”的概念,同时演示了如何通过 echo_exec 指令或者 rewrite 指令来发起“内部跳转”。由于 echo_exec 指令运行在 content
阶段,与这里讨论的 post-rewrite
阶段无关,于是我们感兴趣的便只剩下运行在 rewrite
阶段的 rewrite 指令。回顾一下 (二) 中演示过的这个例子:
server {
listen 8080;
location /foo {
set $a hello;
rewrite ^ /bar;
}
location /bar {
echo "a = [$a]";
}
}
这里在 location /foo
中通过 rewrite 指令把当前请求的 URI 无条件地改写为 /bar
,同时发起一个“内部跳转”,最终跳进了 location /bar
中。这里比较有趣的地方是“内部跳转”的工作原理。“内部跳转”本质上其实就是把当前的请求处理阶段强行倒退到 find-config
阶段,以便重新进行请求 URI 与 location
配置块的配对。比如上例中,运行在 rewrite
阶段的 rewrite 指令就让当前请求的处理阶段倒退回了 find-config
阶段。由于此时当前请求的 URI 已经被 rewrite 指令修改为了 /bar
,所以这一次换成了 location /bar
与当前请求相关联,然后再接着从 rewrite
阶段往下执行。
不过这里更有趣的地方是,倒退回 find-config
阶段的动作并不是发生在 rewrite
阶段,而是发生在后面的 post-rewrite
阶段。上例中的 rewrite 指令只是简单地指示 Nginx 有必要在 post-rewrite
阶段发起“内部跳转”。这个设计对于 Nginx 初学者来说,或许显得有些古怪:“为什么不直接在 rewrite 指令执行时立即进行跳转呢?”答案其实很简单,那就是为了在最初匹配的 location
块中支持多次反复地改写 URI,例如:
location /foo {
rewrite ^ /bar;
rewrite ^ /baz;
echo foo;
}
location /bar {
echo bar;
}
location /baz {
echo baz;
}
这里在 location /foo
中连续把当前请求的 URI 改写了两遍:第一遍先无条件地改写为 /bar
,第二遍再无条件地改写为 /baz
. 而这两条 rewrite 语句只会最终导致 post-rewrite
阶段发生一次“内部跳转”操作,从而不至于在第一次改写 URI 时就直接跳离了当前的 location
而导致后面的 rewrite 语句没有机会执行。请求/foo
接口的结果证实了这一点:
$ curl localhost:8080/foo
baz
从输出结果可以看到,上例确实成功地从 /foo
一步跳到了 /baz
中。如果启用 Nginx “调试日志”的话,还可以从 find-config
阶段生成的 locatin
块的匹配信息中进一步证实这一点:
$ grep 'using config' logs/error.log
[debug] 89449#0: *1 using configuration "/foo"
[debug] 89449#0: *1 using configuration "/baz"
我们看到,对于该次请求,Nginx 一共只匹配过 /foo
和 /baz
这两个 location
,从而只发生过一次“内部跳转”。
当然,如果在 server
配置块中直接使用 rewrite 配置指令对请求 URI 进行改写,则不会涉及“内部跳转”,因为此时 URI 改写发生在 server-rewrite
阶段,早于执行 location
配对的 find-config
阶段。比如下面这个例子:
server {
listen 8080;
rewrite ^/foo /bar;
location /foo {
echo foo;
}
location /bar {
echo bar;
}
}
这里,我们在 server-rewrite
阶段就把那些以 /foo
起始的 URI 改写为 /bar
,而此时请求并没有和任何location
相关联,所以 Nginx 正常往下运行 find-config
阶段,完成最终的 location
匹配。如果我们请求上例中的 /foo
接口,那么 location /foo
根本就没有机会匹配,因为在第一次(也是唯一的一次)运行find-config
阶段时,当前请求的 URI 已经被改写为 /bar
,从而只会匹配 location /bar
. 实际请求的输出正是如此:
$ curl localhost:8080/foo
bar
Nginx “调试日志”可以再一次佐证我们的结论:
$ grep 'using config' logs/error.log
[debug] 92693#0: *1 using configuration "/bar"
可以看到,Nginx 总共只进行过一次 location
匹配,并无“内部跳转”发生。