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
匹配,并無“內部跳轉”發生。