NginxDirectiveExecOrderTutorialCn09

= Nginx 配置指令的执行顺序（九） =

紧接在  阶段后边的是   阶段. 这个阶段并不支持 Nginx 模块注册处理程序，而是由 Nginx 核心来完成当前请求与  配置块之间的配对工作. 换句话说，在此阶段之前，请求并没有与任何  配置块相关联. 因此，对于运行在  阶段之前的   和   阶段来说，只有   配置块以及更外层作用域中的配置指令才会起作用. 这就是为什么只有写在  配置块中的 ngx_rewrite 模块的指令才会运行在   阶段，这也是为什么前面所有例子中的 ngx_realip 模块的指令也都特意写在了   配置块中，以确保其注册在   阶段的处理程序能够生效.

当 Nginx 在  阶段成功匹配了一个   配置块后，会立即打印一条调试信息到错误日志文件中. 我们来看这样的一个例子：

location /hello { echo "hello world"; }

如果启用了 Nginx 的“调试日志”，那么当请求  接口时，便可以在   文件中过滤出下面这一行信息：

$ grep 'using config' logs/error.log [debug] 84579#0: *1 using configuration "/hello"

我们有意省略了信息行首的时间戳，以便放在这里.

运行在  阶段之后的便是我们的老朋友   阶段. 由于 Nginx 已经在  阶段完成了当前请求与   的配对，所以从   阶段开始，  配置块中的指令便可以产生作用. 前面已经介绍过，当 ngx_rewrite 模块的指令用于  块中时，便是运行在这个   阶段. 另外，ngx_set_misc 模块的指令也是如此，还有 ngx_lua 模块的 set_by_lua 指令和 rewrite_by_lua 指令也不例外.

阶段再往后便是所谓的  阶段. 这个阶段也像  阶段那样不接受 Nginx 模块注册处理程序，而是由 Nginx 核心完成   阶段所要求的“内部跳转”操作（如果   阶段有此要求的话）. 先前在 （二） 中已经介绍过了“内部跳转”的概念，同时演示了如何通过 echo_exec 指令或者 rewrite 指令来发起“内部跳转”. 由于 echo_exec 指令运行在  阶段，与这里讨论的   阶段无关，于是我们感兴趣的便只剩下运行在   阶段的 rewrite 指令. 回顾一下 （二） 中演示过的这个例子：

server { listen 8080;

location /foo { set $a hello; rewrite ^ /bar; }

location /bar { echo "a = [$a]"; }   }

这里在  中通过 rewrite 指令把当前请求的 URI 无条件地改写为  ，同时发起一个“内部跳转”，最终跳进了   中. 这里比较有趣的地方是“内部跳转”的工作原理. “内部跳转”本质上其实就是把当前的请求处理阶段强行倒退到  阶段，以便重新进行请求 URI 与   配置块的配对. 比如上例中，运行在  阶段的 rewrite 指令就让当前请求的处理阶段倒退回了   阶段. 由于此时当前请求的 URI 已经被 rewrite 指令修改为了 ，所以这一次换成了   与当前请求相关联，然后再接着从   阶段往下执行.

不过这里更有趣的地方是，倒退回  阶段的动作并不是发生在   阶段，而是发生在后面的   阶段. 上例中的 rewrite 指令只是简单地指示 Nginx 有必要在  阶段发起“内部跳转”. 这个设计对于 Nginx 初学者来说，或许显得有些古怪：“为什么不直接在 rewrite 指令执行时立即进行跳转呢？”答案其实很简单，那就是为了在最初匹配的  块中支持多次反复地改写 URI，例如：

location /foo { rewrite ^ /bar; rewrite ^ /baz;

echo foo; }

location /bar { echo bar; }

location /baz { echo baz; }

这里在  中连续把当前请求的 URI 改写了两遍：第一遍先无条件地改写为  ，第二遍再无条件地改写为. 而这两条 rewrite 语句只会最终导致  阶段发生一次“内部跳转”操作，从而不至于在第一次改写 URI 时就直接跳离了当前的   而导致后面的 rewrite 语句没有机会执行. 请求  接口的结果证实了这一点：

$ curl localhost:8080/foo baz

从输出结果可以看到，上例确实成功地从  一步跳到了   中. 如果启用 Nginx “调试日志”的话，还可以从  阶段生成的   块的匹配信息中进一步证实这一点：

$ grep 'using config' logs/error.log [debug] 89449#0: *1 using configuration "/foo" [debug] 89449#0: *1 using configuration "/baz"

我们看到，对于该次请求，Nginx 一共只匹配过  和   这两个  ，从而只发生过一次“内部跳转”.

当然，如果在  配置块中直接使用 rewrite 配置指令对请求 URI 进行改写，则不会涉及“内部跳转”，因为此时 URI 改写发生在   阶段，早于执行   配对的   阶段. 比如下面这个例子：

server { listen 8080;

rewrite ^/foo /bar;

location /foo { echo foo; }

location /bar { echo bar; }   }

这里，我们在  阶段就把那些以   起始的 URI 改写为  ，而此时请求并没有和任何   相关联，所以 Nginx 正常往下运行   阶段，完成最终的   匹配. 如果我们请求上例中的  接口，那么   根本就没有机会匹配，因为在第一次（也是唯一的一次）运行   阶段时，当前请求的 URI 已经被改写为  ，从而只会匹配. 实际请求的输出正是如此：

$ curl localhost:8080/foo bar

Nginx “调试日志”可以再一次佐证我们的结论：

$ grep 'using config' logs/error.log [debug] 92693#0: *1 using configuration "/bar"

可以看到，Nginx 总共只进行过一次  匹配，并无“内部跳转”发生.