Nginx 配置指令的執行順序(一)


大多數 Nginx 新手都會頻繁遇到這樣一個困惑,那就是當同一個 location 配置塊使用了多個 Nginx 模塊的配置指令時,這些指令的執行順序很可能會跟它們的書寫順序大相徑庭。於是許多人選擇了“試錯法”,然后他們的配置文件就時常被改得一片狼藉。這個系列的教程就旨在幫助讀者逐步地理解這些配置指令背后的執行時間和先后順序的奧秘。

 

    現在就來看這樣一個令人困惑的例子:

    ? location /test {
    ?     set $a 32;
    ?     echo $a;
    ?
    ?     set $a 56;
    ?     echo $a;
    ? }

從這個例子的本意來看,我們期望的輸出是一行 32 和一行 56,因為我們第一次用 echo 配置指令輸出了 $a變量的值以后,又緊接着使用 set 配置指令修改了 $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 模塊提供的配置指令一般只會注冊並運行在其中的某一個處理階段。比如上例中的 set 指令就是在 rewrite 階段運行的,而 echo 指令就只會在 content 階段運行。前面我們已經知道,在單個請求的處理過程中,rewrite 階段總是在 content 階段之前執行,因此屬於 rewrite 階段的配置指令也總是會無條件地在content 階段的配置指令之前執行。於是在同一個 location 配置塊中,set 指令總是會在 echo 指令之前執行,即使我們在配置文件中有意把 set 語句寫在 echo 語句的后面。

 

    回到剛才那個例子,

    set $a 32;
    echo $a;
 
    set $a 56;
    echo $a;

實際的執行順序應當是

    set $a 32;
    set $a 56;
    echo $a;
    echo $a;

即先在 rewrite 階段執行完這里的兩條 set 賦值語句,然后再在后面的 content 階段依次執行那兩條 echo語句。分屬兩個不同處理階段的配置指令之間是不能穿插着運行的。

 

    為了進一步驗證這一點,我們不妨借助 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

如果你使用的是我維護的 ngx_openresty 軟件包,則同樣可以向它的 ./configure 腳本傳遞 --with-debug 命令行選項。

 

    當我們啟用 --with-debug 選項重新構建好調試版的 Nginx 之后,還需要同時在配置文件中通過標准的error_log 配置指令為錯誤日志使用 debug 日志級別(這同時也是最低的日志級別):

    error_log logs/error.log debug;

這里重要的是 error_log 指令的第二個參數,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?"

這里需要稍微解釋一下這些調試信息的具體含義。set 配置指令在實際運行時會打印出兩行以 http script 起始的調試信息,其中第一行信息是 set 語句中被賦予的值,而第二行則是 set 語句中被賦值的 Nginx 變量名。於是上面首先過濾出來的

    [debug] 5363#0: *1 http script value: "32"
    [debug] 5363#0: *1 http script set $a

這兩行就對應我們例子中的配置語句

    set $a 32;

而接下來這兩行調試信息

    [debug] 5363#0: *1 http script value: "56"
    [debug] 5363#0: *1 http script set $a

則對應配置語句

    set $a 56;

此外,凡在 Nginx 中輸出響應體數據時,都會調用 Nginx 的所謂“輸出過濾器”(output filter),我們一直在使用的 echo 指令自然也不例外。而一旦調用 Nginx 的“輸出過濾器”,便會產生類似下面這樣的調試信息:

    [debug] 5363#0: *1 http output filter "/test?"

當然,這里的 "/test?" 部分對於其他接口可能會發生變化,因為它顯示的是當前請求的 URI. 這樣聯系起來看,就不難發現,上例中的那兩條 set 語句確實都是在那兩條 echo 語句之前執行的。

 

    細心的讀者可能會問,為什么這個例子明明只使用了兩條 echo 語句進行輸出,但卻有三行 http output filter 調試信息呢?其實,前兩行 http output filter 信息確實分別對應那兩條 echo 語句,而最后那一行信息則是對應 ngx_echo 模塊輸出指示響應體末尾的結束標記。正是為了輸出這個特殊的結束標記,才會多出一次對 Nginx “輸出過濾器”的調用。包括 ngx_proxy 在內的許多模塊在輸出響應體數據流時都具有此種行為。

 

    現在我們就不會再為前面那個例子輸出兩行一模一樣的 56 而感到驚訝了。我們根本沒有機會在第二條 set語句之前用 echo 輸出。幸運的是,仍然可以借助一些小技巧來達到最初的目的:

    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 的初始值。而對於多條 set 指令而言,它們之間的執行順序是由 ngx_rewrite 模塊來保證與書寫順序相一致的。同理,ngx_echo 模塊自身也會保證它的多條 echo 指令之間的執行順序。

 

    細心的讀者應當發現,我們在 Nginx 變量漫談系列 的示例中已經廣泛使用了這種技巧,來繞過因處理階段而引起的指令執行順序上的限制。

 

    看到這里,有的讀者可能會問:“那么我在使用一條陌生的配置指令之前,如何知道它究竟運行在哪一個處理階段呢?”答案是:查看該指令的文檔(當然,高級開發人員也可以直接查看模塊的 C 源碼)。在許多模塊的文檔中,都會專門標記其配置指令所運行的具體階段。例如 echo 指令的文檔中有這么一行:

    phase: content

這一行便是說,當前配置指令運行在 content 階段。如果你使用的 Nginx 模塊碰巧沒有指示運行階段的文檔,可以直接聯系該模塊的作者請求補充。不過,值得一提的是,並非所有的配置指令都與某個處理階段相關聯,例如我們先前在 Nginx 變量漫談(一) 中提到過的 geo 指令以及在 Nginx 變量漫談(四) 中介紹過的 map 指令。這些不與處理階段相關聯的配置指令基本上都是“聲明性的”(declarative),即不直接產生某種動作或者過程。Nginx 的作者 Igor Sysoev 在公開場合曾不止一次地強調,Nginx 配置文件所使用的語言本質上是“聲明性的”,而非“過程性的”(procedural)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM