大多數(shù) Nginx 新手都會頻繁遇到這樣一個困惑,那就是當(dāng)同一個location配置塊使用了多個 Nginx 模塊的配置指令時泳秀,這些指令的執(zhí)行順序很可能會跟它們的書寫順序大相徑庭。于是許多人選擇了“試錯法”,然后他們的配置文件就時常被改得一片狼藉雁仲。這個系列的教程就旨在幫助讀者逐步地理解這些配置指令背后的執(zhí)行時間和先后順序的奧秘。
????現(xiàn)在就來看這樣一個令人困惑的例子:
?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語句之前就執(zhí)行過了诱建。這究竟是為什么呢蝴蜓?難道我們遇到了 Nginx 中的一個 bug?
????顯然,這里并沒有 Nginx 的 bug茎匠;要理解這里發(fā)生的事情格仲,就首先需要知道 Nginx 處理每一個用戶請求時,都是按照若干個不同階段(phase)依次處理的诵冒。
Nginx 的請求處理階段共有 11 個之多凯肋,我們先介紹其中 3 個比較常見的。按照它們執(zhí)行時的先后順序汽馋,依次是rewrite階段否过、access階段以及content階段(后面我們還有機(jī)會見到其他更多的處理階段)。
所有 Nginx 模塊提供的配置指令一般只會注冊并運行在其中的某一個處理階段惭蟋。比如上例中的set指令就是在rewrite階段運行的,而echo指令就只會在content階段運行药磺。前面我們已經(jīng)知道告组,在單個請求的處理過程中,rewrite階段總是在content階段之前執(zhí)行癌佩,因此屬于rewrite階段的配置指令也總是會無條件地在content階段的配置指令之前執(zhí)行木缝。于是在同一個location配置塊中,set指令總是會在echo指令之前執(zhí)行围辙,即使我們在配置文件中有意把set語句寫在echo語句的后面我碟。
????回到剛才那個例子,
set$a?32;
echo?$a;
set$a?56;
echo?$a;
實際的執(zhí)行順序應(yīng)當(dāng)是
set$a?32;
set$a?56;
echo?$a;
echo?$a;
即先在rewrite階段執(zhí)行完這里的兩條set賦值語句姚建,然后再在后面的content階段依次執(zhí)行那兩條echo語句矫俺。分屬兩個不同處理階段的配置指令之間是不能穿插著運行的。
????為了進(jìn)一步驗證這一點掸冤,我們不妨借助 Nginx 的“調(diào)試日志”來一窺 Nginx 的實際執(zhí)行過程厘托。
因為這是我們第一次提及 Nginx 的“調(diào)試日志”,所以有必要先簡單介紹一下它的啟用方法稿湿。調(diào)試日志默認(rèn)是禁用的铅匹,因為它會引入比較大的運行時開銷,讓 Nginx 服務(wù)器顯著變慢饺藤。一般我們需要重新編譯和構(gòu)造 Nginx 可執(zhí)行文件包斑,并且在調(diào)用 Nginx 源碼包提供的./configure腳本時傳入--with-debug命令行選項。例如我們下載完 Nginx 源碼包后在 Linux 或者 Mac OS X 等系統(tǒng)上構(gòu)建時涕俗,典型的步驟是這樣的:
tar?xvf?nginx-1.0.10.tar.gz
cd?nginx-1.0.10/
./configure?--with-debug
make
sudu?make?install
如果你使用的是我維護(hù)的ngx_openresty軟件包罗丰,則同樣可以向它的./configure腳本傳遞--with-debug命令行選項。
當(dāng)我們啟用--with-debug選項重新構(gòu)建好調(diào)試版的 Nginx 之后再姑,還需要同時在配置文件中通過標(biāo)準(zhǔn)的error_log配置指令為錯誤日志使用debug日志級別(這同時也是最低的日志級別):
error_loglogs/error.log?debug;
這里重要的是error_log指令的第二個參數(shù)丸卷,debug,而前面第一個參數(shù)是錯誤日志文件的路徑询刹,logs/error.log. 當(dāng)然谜嫉,你也可以指定其他路徑萎坷,但后面我們會檢查這個文件的內(nèi)容,所以請?zhí)貏e留意一下這里實際配置的文件路徑沐兰。
????現(xiàn)在我們重新啟動 Nginx(注意哆档,如果 Nginx 可執(zhí)行文件也被更新過,僅僅讓 Nginx 重新加載配置是不夠的住闯,需要關(guān)閉再啟動 Nginx 主服務(wù)進(jìn)程)瓜浸,然后再請求一下我們剛才那個示例接口:
$?curl?'http://localhost:8080/test'
56
56
現(xiàn)在可以檢查一下前面配置的 Nginx 錯誤日志文件中的輸出。因為文件中的輸出比較多(在我的機(jī)器上有 700 多行)比原,所以不妨用grep命令在終端上過濾出我們感興趣的部分:
????grep?-E?'http?(output?filter|script?(set|value))'?logs/error.log
在我機(jī)器上的輸出是這個樣子的(為了方便呈現(xiàn)插佛,這里對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?"
這里需要稍微解釋一下這些調(diào)試信息的具體含義量窘。set配置指令在實際運行時會打印出兩行以http script起始的調(diào)試信息雇寇,其中第一行信息是set語句中被賦予的值,而第二行則是set語句中被賦值的 Nginx 變量名蚌铜。于是上面首先過濾出來的
[debug]?5363#0:?*1?http?script?value:?"32"
[debug]?5363#0:?*1?http?script?set?$a
這兩行就對應(yīng)我們例子中的配置語句
set$a?32;
而接下來這兩行調(diào)試信息
[debug]?5363#0:?*1?http?script?value:?"56"
[debug]?5363#0:?*1?http?script?set?$a
則對應(yīng)配置語句
set$a?56;
此外锨侯,凡在 Nginx 中輸出響應(yīng)體數(shù)據(jù)時,都會調(diào)用 Nginx 的所謂“輸出過濾器”(output filter)冬殃,我們一直在使用的echo指令自然也不例外囚痴。而一旦調(diào)用 Nginx 的“輸出過濾器”,便會產(chǎn)生類似下面這樣的調(diào)試信息:
????[debug]?5363#0:?*1?http?output?filter?"/test?"
當(dāng)然审葬,這里的"/test?"部分對于其他接口可能會發(fā)生變化深滚,因為它顯示的是當(dāng)前請求的 URI. 這樣聯(lián)系起來看,就不難發(fā)現(xiàn)涣觉,上例中的那兩條set語句確實都是在那兩條echo語句之前執(zhí)行的成箫。
細(xì)心的讀者可能會問,為什么這個例子明明只使用了兩條echo語句進(jìn)行輸出旨枯,但卻有三行http output filter調(diào)試信息呢蹬昌?其實,前兩行http output filter信息確實分別對應(yīng)那兩條echo語句攀隔,而最后那一行信息則是對應(yīng)ngx_echo模塊輸出指示響應(yīng)體末尾的結(jié)束標(biāo)記皂贩。正是為了輸出這個特殊的結(jié)束標(biāo)記,才會多出一次對 Nginx “輸出過濾器”的調(diào)用昆汹。包括ngx_proxy在內(nèi)的許多模塊在輸出響應(yīng)體數(shù)據(jù)流時都具有此種行為明刷。
現(xiàn)在我們就不會再為前面那個例子輸出兩行一模一樣的56而感到驚訝了。我們根本沒有機(jī)會在第二條set語句之前用echo輸出满粗。幸運的是辈末,仍然可以借助一些小技巧來達(dá)到最初的目的:
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指令而言挤聘,它們之間的執(zhí)行順序是由ngx_rewrite模塊來保證與書寫順序相一致的轰枝。同理,ngx_echo模塊自身也會保證它的多條echo指令之間的執(zhí)行順序组去。
細(xì)心的讀者應(yīng)當(dāng)發(fā)現(xiàn)鞍陨,我們在Nginx 變量漫談系列的示例中已經(jīng)廣泛使用了這種技巧,來繞過因處理階段而引起的指令執(zhí)行順序上的限制从隆。
看到這里诚撵,有的讀者可能會問:“那么我在使用一條陌生的配置指令之前,如何知道它究竟運行在哪一個處理階段呢键闺?”答案是:查看該指令的文檔(當(dāng)然寿烟,高級開發(fā)人員也可以直接查看模塊的 C 源碼)。在許多模塊的文檔中辛燥,都會專門標(biāo)記其配置指令所運行的具體階段筛武。例如echo指令的文檔中有這么一行:
????phase:?content
這一行便是說,當(dāng)前配置指令運行在content階段购桑。如果你使用的 Nginx 模塊碰巧沒有指示運行階段的文檔,可以直接聯(lián)系該模塊的作者請求補(bǔ)充氏淑。不過勃蜘,值得一提的是,并非所有的配置指令都與某個處理階段相關(guān)聯(lián)假残,例如我們先前在Nginx 變量漫談(一)中提到過的geo指令以及在Nginx 變量漫談(四)中介紹過的map指令缭贡。這些不與處理階段相關(guān)聯(lián)的配置指令基本上都是“聲明性的”(declarative),即不直接產(chǎn)生某種動作或者過程辉懒。Nginx 的作者 Igor Sysoev 在公開場合曾不止一次地強(qiáng)調(diào)阳惹,Nginx 配置文件所使用的語言本質(zhì)上是“聲明性的”,而非“過程性的”(procedural)眶俩。
我們前面已經(jīng)知道莹汤,當(dāng)set指令用在location配置塊中時,都是在當(dāng)前請求的rewrite階段運行的颠印。事實上纲岭,在此上下文中,ngx_rewrite模塊中的幾乎全部指令线罕,都運行在rewrite階段止潮,包括Nginx 變量漫談(二)中介紹過的rewrite指令。不過钞楼,值得一提的是喇闸,當(dāng)這些指令使用在server配置塊中時,則會運行在一個我們尚未提及的更早的處理階段,server-rewrite階段燃乍。
Nginx 變量漫談(二)中介紹過的ngx_set_misc模塊的set_unescape_uri指令同樣也運行在rewrite階段唆樊。特別地,ngx_set_misc模塊的指令還可以和ngx_rewrite的指令混合在一起依次執(zhí)行橘沥。我們來看這樣的一個例子:
location/test?{
set$a?"hello%20world";
set_unescape_uri?$b?$a;
set$c?"$b!";
echo?$c;
}
訪問這個接口可以得到:
$?curl?'http://localhost:8080/test'
hello?world!
我們看到窗轩,set_unescape_uri語句前后的set語句都按書寫時的順序一前一后地執(zhí)行了。
為了進(jìn)一步確認(rèn)這一點座咆,我們不妨再檢查一下 Nginx 的“調(diào)試日志”(如果你還不清楚如何開啟“調(diào)試日志”的話痢艺,可以參考(一)中的步驟):
????grep?-E?'http?script?(value|copy|set)'?t/servroot/logs/error.log
過濾出來的調(diào)試日志信息如下所示:
[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
就對應(yīng)我們的配置語句
set$a?"hello%20world";
而接下來的兩行
[debug]?11167#0:?*1?http?script?value?(post?filter):?"hello?world"
[debug]?11167#0:?*1?http?script?set?$b
則對應(yīng)配置語句
????set_unescape_uri?$b?$a;
我們看到第一行信息與set指令略有區(qū)別,多了"(post?filter)"這個標(biāo)記介陶,而且最后顯示出 URI 解碼操作確實如我們期望的那樣工作了堤舒,即"hello%20world"在這里被成功解碼為"hello?world".
????而最后兩行調(diào)試信息
[debug]?11167#0:?*1?http?script?copy:?"!"
[debug]?11167#0:?*1?http?script?set?$c
則對應(yīng)最后一條set語句:
set$c?"$b!";
注意,因為這條指令在為$c變量賦值時使用了“變量插值”功能哺呜,所以第一行調(diào)試信息是以http?script?copy起始的舌缤,后面則是拼接到最終取值的字符串常量"!".
????把這些調(diào)試信息聯(lián)系起來看,我們不難發(fā)現(xiàn)某残,這些配置指令的實際執(zhí)行順序是:
set$a?"hello%20world";
set_unescape_uri?$b?$a;
set$c?"$b!";
這與它們在配置文件中的書寫順序完全一致国撵。
我們在Nginx 變量漫談(七)中初識了第三方模塊ngx_lua,它提供的set_by_lua配置指令也和ngx_set_misc模塊的指令一樣玻墅,可以和ngx_rewrite模塊的指令混合使用介牙。set_by_lua指令支持通過一小段用戶 Lua 代碼來計算出一個結(jié)果,然后賦給指定的 Nginx 變量澳厢。和set指令相似环础,set_by_lua指令也有自動創(chuàng)建不存在的 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指令內(nèi)聯(lián)一行我們自己指定的 Lua 代碼线得,計算出 Nginx 變量$a和$b的“代數(shù)和”(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"檩咱;接著揭措,我們對兩個字符串作加法運算會觸發(fā) Lua 對加數(shù)進(jìn)行自動類型轉(zhuǎn)換(Lua 會把兩個加數(shù)先轉(zhuǎn)換為數(shù)值類型再求和);然后刻蚯,我們在 Lua 代碼中把最終結(jié)果通過return語句返回給外面的 Nginx 變量$c绊含;最后,ngx_lua模塊在給$c實際賦值之前炊汹,也會把return語句返回的數(shù)值類型的結(jié)果躬充,也就是 Lua 加法計算得出的“和”,自動轉(zhuǎn)換為字符串(這同樣是因為 Nginx 變量的值只能是字符串)讨便。
????這個例子的實際運行結(jié)果符合我們的期望:
$?curl?'http://localhost:8080/test'
32?+?56?=?88
于是這驗證了set_by_lua指令確實也可以和set這樣的ngx_rewrite模塊提供的指令混合在一起工作充甚。
還有不少第三方模塊,例如Nginx 變量漫談(八)中介紹過的ngx_array_var以及后面即將接觸到的用于加解密用戶會話(session)的ngx_encrypted_session霸褒,也都可以和ngx_rewrite模塊的指令無縫混合工作伴找。
標(biāo)準(zhǔn)ngx_rewrite模塊的應(yīng)用是如此廣泛,所以能夠和它的配置指令混合使用的第三方模塊是幸運的废菱。事實上技矮,上面提到的這些第三方模塊都采用了特殊的技術(shù),將它們自己的配置指令“注入”到了ngx_rewrite模塊的指令序列中(它們都借助了 Marcus Clyne 編寫的第三方模塊ngx_devel_kit)殊轴。換句話說衰倦,更多常規(guī)的在 Nginx 的rewrite階段注冊和運行指令的第三方模塊就沒那么幸運了。這些“常規(guī)模塊”的指令雖然也運行在rewrite階段旁理,但其配置指令和ngx_rewrite模塊(以及同一階段內(nèi)的其他模塊)都是分開獨立執(zhí)行的樊零。在運行時,不同模塊的配置指令集之間的先后順序一般是不確定的(嚴(yán)格來說韧拒,一般是由模塊的加載順序決定的淹接,但也有例外的情況)十性。比如A和B兩個模塊都在rewrite階段運行指令叛溢,于是要么是A模塊的所有指令全部執(zhí)行完再執(zhí)行B模塊的那些指令,要么就是反過來劲适,把B的指令全部執(zhí)行完楷掉,再去運行A的指令。除非模塊的文檔中有明確的交待霞势,否則用戶一般不應(yīng)編寫依賴于此種不確定順序的配置烹植。
如前文所述,除非像ngx_set_misc模塊那樣使用特殊技術(shù)愕贡,其他模塊的配置指令即使是在rewrite階段運行草雕,也不能和ngx_rewrite模塊的指令混合使用。不妨來看幾個這樣的例子固以。
第三方模塊ngx_headers_more提供了一系列配置指令墩虹,用于操縱當(dāng)前請求的請求頭和響應(yīng)頭嘱巾。其中有一條名叫more_set_input_headers的指令可以在rewrite階段改寫指定的請求頭(或者在請求頭不存在時自動創(chuàng)建)。這條指令總是運行在rewrite階段的末尾赴蝇,該指令的文檔中有這么一行標(biāo)記:
????phase:?rewrite?tail
其中的rewrite?tail的意思就是rewrite階段的末尾伐憾。
既然運行在rewrite階段的末尾旧噪,那么也就總是會運行在ngx_rewrite模塊的指令之后,即使我們在配置文件中把它寫在前面问拘,例如:
?location/test?{
?set$value?dog;
??????more_set_input_headers?"X-Species:?$value";
?set$value?cat;
?
??????echo?"X-Species:?$http_x_species";
??}
這個例子用到的$http_XXX內(nèi)建變量在讀取時會返回當(dāng)前請求中名為XXX的請求頭,我們在Nginx 變量漫談(二)中曾經(jīng)簡單提過它惧所。需要注意的是骤坐,$http_XXX變量在匹配請求頭時會自動對請求頭的名字進(jìn)行歸一化,即將名字的大寫字母轉(zhuǎn)換為小寫字母纯路,同時把間隔符(-)替換為下劃線(_)或油,所以變量名$http_x_species才得以成功匹配more_set_input_headers語句中設(shè)置的請求頭X-Species.
此例書寫的指令順序會誤導(dǎo)我們認(rèn)為/test接口輸出的X-Species頭的值是dog,然而實際的結(jié)果卻并非如此:
$?curl?'http://localhost:8080/test'
X-Species:?cat
顯然驰唬,寫在more_set_input_headers指令之后的set?$value?cat語句卻先執(zhí)行了顶岸。
上面這個例子證明了即使運行在同一個請求處理階段,分屬不同模塊的配置指令也可能會分開獨立運行(除非像ngx_set_misc等模塊那樣針對ngx_rewrite模塊提供特殊支持)叫编。換句話說辖佣,在單個請求處理階段內(nèi)部,一般也會以 Nginx 模塊為單位進(jìn)一步地劃分出內(nèi)部子階段搓逾。
第三方模塊ngx_lua提供的rewrite_by_lua配置指令也和more_set_input_headers一樣運行在rewrite階段的末尾卷谈。我們來驗證一下:
?location/test?{
?set$a?1;
??????rewrite_by_lua?"ngx.var.a?=?ngx.var.a?+?1";
?set$a?56;
?
??????echo?$a;
??}
這里我們在rewrite_by_lua語句內(nèi)聯(lián)的 Lua 代碼中對 Nginx 變量$a進(jìn)行了自增計算。從該例的指令書寫順序上看霞篡,我們或許會期望輸出是56世蔗,可是因為rewrite_by_lua會在所有的set語句之后執(zhí)行,所以結(jié)果是57:
$?curl?'http://localhost:8080/test'
57
顯然朗兵,rewrite_by_lua指令的行為不同于我們前面在(二)中介紹過的set_by_lua指令污淋。
有的讀者可能要問,既然more_set_input_headers和rewrite_by_lua指令都運行在rewrite階段的末尾余掖,那么它們之間的先后順序又是怎樣的呢寸爆?答案是:不一定。我們應(yīng)當(dāng)避免寫出依賴它們二者間順序的配置盐欺。
Nginx 的rewrite階段是一個比較早的請求處理階段赁豆,這個階段的配置指令一般用來對當(dāng)前請求進(jìn)行各種修改(比如對 URI 和 URL 參數(shù)進(jìn)行改寫),或者創(chuàng)建并初始化一系列后續(xù)處理階段可能需要的 Nginx 變量冗美。當(dāng)然魔种,也不能阻止一些用戶在rewrite階段做一系列更復(fù)雜的事情,比如讀取請求體粉洼,或者訪問數(shù)據(jù)庫等遠(yuǎn)方服務(wù)节预,畢竟有rewrite_by_lua這樣的指令可以嵌入任意復(fù)雜的 Lua 代碼甲抖。
在rewrite階段之后,有一個名叫access的請求處理階段心铃。Nginx 變量漫談(五)中介紹過的第三方模塊ngx_auth_request的指令就運行在access階段准谚。在access階段運行的配置指令多是執(zhí)行訪問控制性質(zhì)的任務(wù),比如檢查用戶的訪問權(quán)限去扣,檢查用戶的來源 IP 地址是否合法柱衔,諸如此類。
例如愉棱,標(biāo)準(zhǔn)模塊ngx_access提供的allow和deny配置指令可用于控制哪些 IP 地址可以訪問唆铐,哪些不可以:
location/hello?{
allow127.0.0.1;
denyall;
echo?"hello?world";
}
這個/test接口被配置為只允許從本機(jī)(IP 地址為保留的127.0.0.1)訪問,而從其他 IP 地址訪問都會被拒(返回403錯誤頁)奔滑。ngx_access模塊自己的多條配置指令之間是按順序執(zhí)行的艾岂,直到遇到第一條滿足條件的指令就不再執(zhí)行后續(xù)的allow和deny指令。如果首先匹配的指令是allow朋其,則會繼續(xù)執(zhí)行后續(xù)其他模塊的指令或者跳到后續(xù)的處理階段王浴;而如果首先滿足的是deny則會立即中止當(dāng)前整個請求的處理,并立即返回403錯誤頁梅猿。所以看上面這個例子氓辣,如果是從本地訪問的,則首先匹配allow?127.0.0.1這一條語句袱蚓,于是 Nginx 就繼續(xù)往下執(zhí)行其他模塊的指令以及后續(xù)的處理階段钞啸;而如果是從其他機(jī)器訪問,則首先匹配的則是deny?all這一條語句喇潘,即拒絕所有地址体斩,它會導(dǎo)致403錯誤頁立即返回給客戶端。
????我們來實測一下颖低。從本機(jī)訪問這個接口可以得到
$?curl?'http://localhost:8080/hello'
hello?world
而從另一臺機(jī)器訪問這臺機(jī)器(假設(shè)運行 Nginx 的機(jī)器地址是192.168.1.101)提供的接口時則得到
$?curl?'http://192.168.1.101:8080/hello'
403?Forbidden
403?Forbidden
nginx
值得一提的是絮吵,ngx_access模塊還支持所謂的“CIDR 記法”來表示一個網(wǎng)段,例如169.200.179.4/24則表示路由前綴是169.200.179.0(或者說子網(wǎng)掩碼是255.255.255.0)的網(wǎng)段枫甲。
因為ngx_access模塊的指令運行在access階段源武,而access階段又處于rewrite階段之后扼褪,所以前面我們見到的所有那些在rewrite階段運行的配置指令想幻,都總是在allow和deny之前執(zhí)行,而無論它們在配置文件中的書寫順序是怎樣的话浇。所以脏毯,為了避免閱讀配置時的混亂,我們應(yīng)該總是讓指令的書寫順序和它們的實際執(zhí)行順序保持一致幔崖。
ngx_lua模塊提供了配置指令access_by_lua食店,用于在access請求處理階段插入用戶 Lua 代碼渣淤。這條指令運行于access階段的末尾,因此總是在allow和deny這樣的指令之后運行吉嫩,雖然它們同屬access階段价认。一般我們通過access_by_lua在ngx_access這樣的模塊檢查過客戶端 IP 地址之后,再通過 Lua 代碼執(zhí)行一系列更為復(fù)雜的請求驗證操作自娩,比如實時查詢數(shù)據(jù)庫或者其他后端服務(wù)用踩,以驗證當(dāng)前用戶的身份或權(quán)限。
我們來看一個簡單的例子忙迁,利用access_by_lua來實現(xiàn)ngx_access模塊的 IP 地址過濾功能:
location/hello?{
access_by_lua?'
if?ngx.var.remote_addr?==?"127.0.0.1"?then
return
end
ngx.exit(403)
';
echo?"hello?world";
}
這里在 Lua 代碼中通過引用 Nginx 標(biāo)準(zhǔn)的內(nèi)建變量$remote_addr來獲取字符串形式的客戶端 IP 地址脐彩,然后用 Lua 的if語句判斷是否為本機(jī)地址,即是否等于127.0.0.1. 如果是本機(jī)地址姊扔,則直接利用 Lua 的return語句返回惠奸,讓 Nginx 繼續(xù)執(zhí)行后續(xù)的請求處理階段(包括echo指令所處的content階段);而如果不是本機(jī)地址恰梢,則通過ngx_lua模塊提供的 Lua 函數(shù)ngx.exit中斷當(dāng)前的整個請求處理流程佛南,直接返回403錯誤頁給客戶端。
這個例子在功能上完全等價于先前在(三)中介紹過的那個使用ngx_access模塊的例子:
location/hello?{
allow127.0.0.1;
denyall;
echo?"hello?world";
}
雖然這兩個例子在功能上完全相同嵌言,但在性能上還是有區(qū)別的共虑,畢竟ngx_access是用純 C 實現(xiàn)的專門化的 Nginx 模塊。
下面我們不妨來實際測量一下這兩個例子的性能差別呀页。因為我們使用 Nginx 就是為了追求性能妈拌,而量化的性能比較,在工程上具有很大的現(xiàn)實意義蓬蝶,所以我們順便介紹一下重要的測量技術(shù)尘分。由于無論是ngx_access還是ngx_lua在進(jìn)行 IP 地址驗證方面的性能都非常之高,所以為了減少測量誤差丸氛,我們希望能對access階段的用時進(jìn)行直接測量培愁。為了做到這一點,傳統(tǒng)的做法一般會涉及到修改 Nginx 源碼缓窜,自己插入專門的計時代碼和統(tǒng)計輸出代碼定续,抑或是重新編譯 Nginx 以啟用像GNU?gprof這樣專門的性能監(jiān)測工具。
幸運的是禾锤,在新一點的 Solaris, Mac OS X, 以及 FreeBSD 等系統(tǒng)上存在一個叫做dtrace的工具私股,可以對任意的用戶程序進(jìn)行微觀性能分析(以及行為分析),而無須對用戶程序的源碼進(jìn)行修改或者對用戶程序進(jìn)行重新編譯恩掷。因為 Mac OS X 10.5 以后就自帶了dtrace倡鲸,所以為方便起見,下面在我的 MacBook Air 筆記本上演示一下這里的測量過程黄娘。
首先峭状,在 Mac OS X 系統(tǒng)中打開一個命令行終端克滴,在某一個文件目錄下面創(chuàng)建一個名為nginx-access-time.d的文件,并編輯內(nèi)容如下:
#!/usr/bin/env?dtrace?-s
pid$1::ngx_http_handler:entry
{
elapsed?=?0;
}
pid$1::ngx_http_core_access_phase:entry
{
begin?=?timestamp;
}
pid$1::ngx_http_core_access_phase:return
/begin?>?0/
{
elapsed?+=?timestamp?-?begin;
begin?=?0;
}
pid$1::ngx_http_finalize_request:return
/elapsed?>?0/
{
@elapsed?=?avg(elapsed);
elapsed?=?0;
}
保存好此文件后优床,再賦予它可執(zhí)行權(quán)限:
????$?chmod?+x?./nginx-access-time.d
這個.d文件中的代碼是用dtrace工具自己提供的D語言來編寫的(注意劝赔,這里的D語言并不同于 Walter Bright 作為另一種“更好的 C++”而設(shè)計的D語言)。由于本系列教程并不打算介紹如何編寫dtrace的D腳本胆敞,同時理解這個腳本需要不少有關(guān) Nginx 內(nèi)部源碼實現(xiàn)的細(xì)節(jié)望忆,所以這里我們不展開介紹。大家只需要知道這個腳本的功能是:統(tǒng)計指定的 Nginx worker 進(jìn)程在處理每個請求時竿秆,平均花費在access階段上的時間启摄。
現(xiàn)在來演示一下這個D腳本的運行方法。這個腳本接受一個命令行參數(shù)用于指定監(jiān)視的 Nginx worker 進(jìn)程的進(jìn)程號(pid)幽钢。由于 Nginx 支持多 worker 進(jìn)程歉备,所以我們測試時發(fā)起的 HTTP 請求可能由其中任意一個 worker 進(jìn)程服務(wù)。為了確保所有測試請求都為固定的 worker 進(jìn)程處理匪燕,不妨在nginx.conf配置文件中指定只啟用一個 worker 進(jìn)程:
重啟 Nginx 服務(wù)器之后蕾羊,可以利用ps命令得到當(dāng)前 worker 進(jìn)程的進(jìn)程號:
????$?ps?ax|grep?nginx|grep?worker|grep?-v?grep
在我機(jī)器上的一次典型輸出是
????10975???????S??????0:34.28?nginx:?worker?process
其中第一列的數(shù)值便是我的 nginx worker 進(jìn)程的進(jìn)程號,10975帽驯。如果你得到的輸出不止一行龟再,則通常意味著你的系統(tǒng)中同時運行著多個 Nginx 服務(wù)器實例,或者當(dāng)前 Nginx 實例啟用了多個 worker 進(jìn)程尼变。
接下來使用剛剛得到的 worker 進(jìn)程號以及 root 身份來運行nginx-access-time.d腳本:
????$?sudo?./nginx-access-time.d?10975
如果一切正常利凑,則會看到這樣一行輸出:
????dtrace:?script?'./nginx-access-time.d'?matched?4?probes
這行輸出是說,我們的D腳本已成功向目標(biāo)進(jìn)程動態(tài)植入了 4 個dtrace“探針”(probe)嫌术。緊接著這個腳本就掛起了哀澈,表明dtrace工具正在對進(jìn)程10975進(jìn)行持續(xù)監(jiān)視。
然后我們再打開一個新終端度气,在那里使用curl這樣的工具多次請求我們正在監(jiān)視的接口
$?curl?'http://localhost:8080/hello'
hello?world
$?curl?'http://localhost:8080/hello'
hello?world
最后我們回到原先那個一直在運行D腳本的終端割按,按下Ctrl-C組合鍵中止dtrace的運行。而該腳本在退出時會向終端打印出最終統(tǒng)計結(jié)果磷籍。例如我的終端此時是這個樣子的:
$?sudo?./nginx-access-time.d?10975
dtrace:?script?'./nginx-access-time.d'?matched?4?probes
^C
19219
最后一行輸出19219便是那幾次curl請求在access階段的平均用時(以納秒适荣,即 10 的負(fù) 9 次方秒為單位)。
通過上面介紹的步驟院领,可以通過nginx-access-time.d腳本分別統(tǒng)計出各種不同的 Nginx 配置下access階段的平均用時弛矛。針對我們感興趣的三種情況可以進(jìn)行三組平行試驗,即使用ngx_access過濾 IP 地址的情況栅盲,使用access_by_lua過濾 IP 地址的情況汪诉,以及不在access階段使用任何配置指令的情況废恋。最后一種情況屬于“空白對照組”谈秫,用于校正測試過程中因dtrace探針等其他因素而引入的“系統(tǒng)誤差”扒寄。另外,為了最小化各種不可控的“隨機(jī)誤差”拟烫,可以用ab這樣的批量測試工具來取代curl發(fā)起連續(xù)十萬次以上的請求该编,例如
????$?ab?-k?-c1?-n100000?'http://127.0.0.1:8080/hello'
這樣我們的D腳本統(tǒng)計出來的平均值將更加接近“真實值”。
????在我的蘋果系統(tǒng)上硕淑,一次典型的測試結(jié)果如下:
ngx_access?組???????????????18146
access_by_lua?組????????????35011
空白對照組???????????????????15887
把前兩組的結(jié)果分別減去“空白對照組”的結(jié)果可以得到
ngx_access?組???????????????2259
access_by_lua?組???????????19124
可以看到课竣,ngx_access組比access_by_lua組快了大約一個數(shù)量級,這正是我們所預(yù)期的置媳。不過其絕對時間差是極小的于樟,對于我的Intel?Core2Duo?1.86?GHz的 CPU 而言,也只有區(qū)區(qū)十幾微秒拇囊,或者說是在十萬分之一秒的量級迂曲。
當(dāng)然,上面使用access_by_lua的例子還可以通過換用$binary_remote_addr內(nèi)建變量進(jìn)行優(yōu)化寥袭,因為$binary_remote_addr讀出的是二進(jìn)制形式的 IP 地址路捧,而$remote_addr則返回更長一些的字符串形式的地址。更短的地址意味著用 Lua 進(jìn)行字符串比較時通炒疲可以更快杰扫。
值得注意的是,如果按(一)中介紹的方法為 Nginx 開啟了“調(diào)試日志”的話膘掰,上面統(tǒng)計出來的時間會顯著增加章姓,因為“調(diào)試日志”自身的開銷是很大的。
Nginx 的content階段是所有請求處理階段中最為重要的一個识埋,因為運行在這個階段的配置指令一般都肩負(fù)著生成“內(nèi)容”(content)并輸出 HTTP 響應(yīng)的使命啤覆。正因為其重要性,這個階段的配置指令也異常豐富惭聂,例如前面我們一直在示例中廣泛使用的echo指令窗声,在Nginx 變量漫談(二)中接觸到的echo_exec指令,Nginx 變量漫談(三)中接觸到的proxy_pass指令辜纲,Nginx 變量漫談(五)中介紹過的echo_location指令笨觅,以及Nginx 變量漫談(七)中介紹過的content_by_lua指令,都運行在這個階段耕腾。
content階段屬于一個比較靠后的處理階段见剩,運行在先前介紹過的rewrite和access這兩個階段之后。當(dāng)和rewrite扫俺、access階段的指令一起使用時苍苞,這個階段的指令總是最后運行,例如:
location/test?{
#?rewrite?phase
set$age?1;
rewrite_by_lua?"ngx.var.age?=?ngx.var.age?+?1";
#?access?phase
deny10.32.168.49;
access_by_lua?"ngx.var.age?=?ngx.var.age?*?3";
#?content?phase
echo?"age?=?$age";
}
這個例子中各個配置指令的執(zhí)行順序便是它們的書寫順序。測試結(jié)果完全符合預(yù)期:
$?curl?'http://localhost:8080/test'
age?=?6
即使改變它們的書寫順序羹呵,也不會影響到執(zhí)行順序骂际。其中,set指令來自ngx_rewrite模塊冈欢,運行于rewrite階段歉铝;而rewrite_by_lua指令來自ngx_lua模塊,運行于rewrite階段的末尾凑耻;接下來太示,deny指令來自ngx_access模塊,運行于access階段香浩;再下來类缤,access_by_lua指令同樣來自ngx_lua模塊,運行于access階段的末尾邻吭;最后呀非,我們的老朋友echo指令則來自ngx_echo模塊,運行在content階段镜盯。
????這個例子展示了通過同時使用多個處理階段的配置指令來實現(xiàn)多個模塊協(xié)同工作的效果岸裙。在這個過程中,Nginx 變量則經(jīng)常扮演著在指令間乃至模塊間傳遞(小份)數(shù)據(jù)的角色速缆。這些配置指令的執(zhí)行順序降允,也強(qiáng)烈地受到請求處理階段的影響。
進(jìn)一步地艺糜,在rewrite和access這兩個階段剧董,多個模塊的配置指令可以同時使用,譬如上例中的set指令和rewrite_by_lua指令同處rewrite階段破停,而deny指令和access_by_lua指令則同處access階段翅楼。但不幸的是,這通常不適用于content階段真慢。
絕大多數(shù) Nginx 模塊在向content階段注冊配置指令時毅臊,本質(zhì)上是在當(dāng)前的location配置塊中注冊所謂的“內(nèi)容處理程序”(content handler)。每一個location只能有一個“內(nèi)容處理程序”黑界,因此管嬉,當(dāng)在location中同時使用多個模塊的content階段指令時,只有其中一個模塊能成功注冊“內(nèi)容處理程序”朗鸠◎橇茫考慮下面這個有問題的例子:
?location/test?{
??????echo?hello;
??????content_by_lua?'ngx.say("world")';
??}
這里,ngx_echo模塊的echo指令和ngx_lua模塊的content_by_lua指令同處content階段烛占,于是只有其中一個模塊能注冊和運行這個location的“內(nèi)容處理程序”:
$?curl?'http://localhost:8080/test'
world
實際運行結(jié)果表明胎挎,寫在后面的content_by_lua指令反而勝出了,而echo指令則完全沒有運行。具體哪一個模塊的指令會勝出是不確定的德迹,例如把上例中的echo語句和content_by_lua語句交換順序,則輸出就會變成hello旦装,即ngx_echo模塊勝出眨八。所以我們應(yīng)當(dāng)避免在同一個location中使用多個模塊的content階段指令篓足。
將上例中的content_by_lua指令替換為echo指令就可以如愿了:
location/test?{
echo?hello;
echo?world;
}
測試結(jié)果證明了這一點:
$?curl?'http://localhost:8080/test'
hello
world
這里使用多條echo指令是沒問題的,因為它們同屬ngx_echo模塊染簇,而且ngx_echo模塊規(guī)定和實現(xiàn)了它們之間的執(zhí)行順序暴心。值得一提的是蚌成,并非所有模塊的指令都支持在同一個location中被使用多次,例如content_by_lua就只能使用一次,所以下面這個例子是錯誤的:
?location/test?{
??????content_by_lua?'ngx.say("hello")';
??????content_by_lua?'ngx.say("world")';
??}
這個配置在 Nginx 啟動時就會報錯:
????[emerg]?"content_by_lua"?directive?is?duplicate?...
正確的寫法應(yīng)當(dāng)是:
location/test?{
content_by_lua?'ngx.say("hello")?ngx.say("world")';
}
即在content_by_lua內(nèi)聯(lián)的 Lua 代碼中調(diào)用兩次ngx.say函數(shù)买决,而不是在當(dāng)前l(fā)ocation中使用兩次content_by_lua指令。
類似地督赤,ngx_proxy模塊的proxy_pass指令和echo指令也不能同時用在一個location中,因為它們也同屬content階段办悟。不少 Nginx 新手都會犯類似下面這樣的錯誤:
?location/test?{
??????echo?"before...";
?proxy_passhttp://127.0.0.1:8080/foo;
??????echo?"after...";
??}
?
?location/foo?{
??????echo?"contents?to?be?proxied";
??}
這個例子表面上是想在ngx_proxy模塊返回的內(nèi)容前后病蛉,通過ngx_echo模塊的echo指令分別輸出字符串"before..."和"after...",但其實只有其中一個模塊能在content階段運行瑰煎。測試結(jié)果表明铺然,在這個例子中是ngx_proxy模塊勝出,而ngx_echo模塊的echo指令根本沒有運行:
$?curl?'http://localhost:8080/test'
contents?to?be?proxied
要實現(xiàn)這個例子希望達(dá)到的效果酒甸,需要改用ngx_echo模塊提供的echo_before_body和echo_after_body這兩條配置指令:
location/test?{
echo_before_body?"before...";
proxy_passhttp://127.0.0.1:8080/foo;
echo_after_body?"after...";
}
location/foo?{
echo?"contents?to?be?proxied";
}
測試結(jié)果表明這一次我們成功了:
$?curl?'http://localhost:8080/test'
before...
contents?to?be?proxied
after...
配置指令echo_before_body和echo_after_body之所以可以和其他模塊運行在content階段的指令一起工作魄健,是因為它們運行在 Nginx 的“輸出過濾器”中。前面我們在(一)中分析echo指令產(chǎn)生的“調(diào)試日志”時已經(jīng)知道插勤,Nginx 在輸出響應(yīng)體數(shù)據(jù)時都會調(diào)用“輸出過濾器”沽瘦,所以ngx_echo模塊才有機(jī)會在“輸出過濾器”中對ngx_proxy模塊產(chǎn)生的響應(yīng)體輸出進(jìn)行修改(即在首尾添加新的內(nèi)容)革骨。值得一提的是,“輸出過濾器”并不屬于(一)中提到的那 11 個請求處理階段(畢竟許多階段都可以通過輸出響應(yīng)體數(shù)據(jù)來調(diào)用“輸出過濾器”)析恋,但這并不妨礙echo_before_body和echo_after_body指令在文檔中標(biāo)記下面這一行:
????phase:?output?filter
這一行的意思是良哲,當(dāng)前配置指令運行在“輸出過濾器”這個特殊的階段。
前面我們在(五)中提到助隧,在一個location中使用content階段指令時筑凫,通常情況下就是對應(yīng)的 Nginx 模塊注冊該location中的“內(nèi)容處理程序”。那么當(dāng)一個location中未使用任何content階段的指令并村,即沒有模塊注冊“內(nèi)容處理程序”時巍实,content階段會發(fā)生什么事情呢?誰又來擔(dān)負(fù)起生成內(nèi)容和輸出響應(yīng)的重?fù)?dān)呢橘霎?答案就是那些把當(dāng)前請求的 URI 映射到文件系統(tǒng)的靜態(tài)資源服務(wù)模塊蔫浆。當(dāng)存在“內(nèi)容處理程序”時殖属,這些靜態(tài)資源服務(wù)模塊并不會起作用姐叁;反之,請求的處理權(quán)就會自動落到這些模塊上洗显。
Nginx 一般會在content階段安排三個這樣的靜態(tài)資源服務(wù)模塊(除非你的 Nginx 在構(gòu)造時顯式禁用了這三個模塊中的一個或者多個外潜,又或者啟用了這種類型的其他模塊)。按照它們在content階段的運行順序挠唆,依次是ngx_index模塊处窥,ngx_autoindex模塊,以及ngx_static模塊玄组。下面就來逐一介紹一下這三個模塊滔驾。
ngx_index和ngx_autoindex模塊都只會作用于那些 URI 以/結(jié)尾的請求,例如請求GET?/cats/俄讹,而對于不以/結(jié)尾的請求則會直接忽略哆致,同時把處理權(quán)移交給content階段的下一個模塊。而ngx_static模塊則剛好相反患膛,直接忽略那些 URI 以/結(jié)尾的請求摊阀。
ngx_index模塊主要用于在文件系統(tǒng)目錄中自動查找指定的首頁文件,類似index.html和index.htm這樣的踪蹬,例如:
location/?{
root/var/www/;
}
這樣胞此,當(dāng)用戶請求/地址時,Nginx 就會自動在root配置指令指定的文件系統(tǒng)目錄下依次尋找index.htm和index.html這兩個文件跃捣。如果index.htm文件存在漱牵,則直接發(fā)起“內(nèi)部跳轉(zhuǎn)”到/index.htm這個新的地址;而如果index.htm文件不存在疚漆,則繼續(xù)檢查index.html是否存在酣胀。如果存在蚊惯,同樣發(fā)起“內(nèi)部跳轉(zhuǎn)”到/index.html;如果index.html文件仍然不存在灵临,則放棄處理權(quán)給content階段的下一個模塊截型。
我們前面已經(jīng)在Nginx 變量漫談(二)中提到,echo_exec指令和rewrite指令可以發(fā)起“內(nèi)部跳轉(zhuǎn)”儒溉。這種跳轉(zhuǎn)會自動修改當(dāng)前請求的 URI宦焦,并且重新匹配與之對應(yīng)的location配置塊,再重新執(zhí)行rewrite顿涣、access波闹、content等處理階段。因為是“內(nèi)部跳轉(zhuǎn)”涛碑,所以有別于 HTTP 協(xié)議中定義的基于 302 和 301 響應(yīng)的“外部跳轉(zhuǎn)”精堕,最終用戶的瀏覽器的地址欄也不會發(fā)生變化,依然是原來的 URI 位置蒲障。而ngx_index模塊一旦找到了index指令中列舉的文件之后歹篓,就會發(fā)起這樣的“內(nèi)部跳轉(zhuǎn)”,仿佛用戶是直接請求的這個文件所對應(yīng)的 URI 一樣揉阎。
為了進(jìn)一步確認(rèn)ngx_index模塊在找到文件時的“內(nèi)部跳轉(zhuǎn)”行為庄撮,我們不妨設(shè)計下面這個小例子:
location/?{
root/var/www/;
}
set$a?32;
echo?"a?=?$a";
}
此時我們在本機(jī)的/var/www/目錄下創(chuàng)建一個空白的index.html文件,并確保該文件的權(quán)限設(shè)置對于運行 Nginx worker 進(jìn)程的帳戶可讀毙籽。然后我們來請求一下根位置(/):
$?curl?'http://localhost:8080/'
a?=?32
這里發(fā)生了什么洞斯?為什么輸出不是index.html文件的內(nèi)容(即空白)?首先對于用戶的原始請求GET?/坑赡,Nginx 匹配出location?/來處理它烙如,然后content階段的ngx_index模塊在/var/www/下找到了index.html,于是立即發(fā)起一個到/index.html位置的“內(nèi)部跳轉(zhuǎn)”毅否。
到這里亚铁,相信大家都不會有問題。接下來有趣的事情發(fā)生了搀突!在重新為/index.html這個新位置匹配location配置塊時刀闷,location?/index.html的優(yōu)先級要高于location?/,因為location塊按照 URI 前綴來匹配時遵循所謂的“最長子串匹配語義”仰迁。這樣甸昏,在進(jìn)入location?/index.html配置塊之后,又重新開始執(zhí)行rewrite徐许、access施蜜、以及content等階段。最終輸出a?=?32自然也就在情理之中了雌隅。
我們接著研究上面這個例子翻默。如果此時把/var/www/index.html文件刪除缸沃,再訪問/又會發(fā)生什么事情呢?答案是返回403?Forbidden出錯頁修械。為什么呢趾牧?因為ngx_index模塊找不到index指令指定的文件(在這里就是index.html),接著把處理權(quán)轉(zhuǎn)給content階段的后續(xù)模塊肯污,而后續(xù)的模塊也都無法處理這個請求翘单,于是 Nginx 只好放棄,輸出了錯誤頁蹦渣,并且在 Nginx 錯誤日志中留下了類似這一行信息:
????[error]?28789#0:?*1?directory?index?of?"/var/www/"?is?forbidden
所謂directory?index便是生成“目錄索引”的意思哄芜,典型的方式就是生成一個網(wǎng)頁,上面列舉出/var/www/目錄下的所有文件和子目錄柬唯。而運行在ngx_index模塊之后的ngx_autoindex模塊就可以用于自動生成這樣的“目錄索引”網(wǎng)頁认臊。我們來把上例修改一下:
location/?{
root/var/www/;
autoindexon;
}
此時仍然保持文件系統(tǒng)中的/var/www/index.html文件不存在。我們再訪問/位置時锄奢,就會得到一張漂亮的網(wǎng)頁:
$?curl?'http://localhost:8080/'
Index?of?/
Index?of?/
../
cgi-bin/??08-Mar-2010?19:36???-
error/??????08-Mar-2010?19:36???-
htdocs/????05-Apr-2010?03:55???-
icons/??????08-Mar-2010?19:36???-
生成的 HTML 源碼顯示失晴,我本機(jī)的/var/www/目錄下還有cgi-bin/,error/,htdocs/, 以及icons/這幾個子目錄。在你的系統(tǒng)中嘗試上面的例子斟薇,輸出很可能會不太一樣师坎。
值得一提的是恕酸,當(dāng)你的文件系統(tǒng)中存在/var/www/index.html時堪滨,優(yōu)先運行的ngx_index模塊就會發(fā)起“內(nèi)部跳轉(zhuǎn)”,根本輪不到ngx_autoindex執(zhí)行蕊温。感興趣的讀者可以自己測試一下袱箱。
在content階段默認(rèn)“墊底”的最后一個模塊便是極為常用的ngx_static模塊。這個模塊主要實現(xiàn)服務(wù)靜態(tài)文件的功能义矛。比方說发笔,一個網(wǎng)站的靜態(tài)資源,包括靜態(tài).html文件凉翻、靜態(tài).css文件了讨、靜態(tài).js文件、以及靜態(tài)圖片文件等等制轰,全部可以通過這個模塊對外服務(wù)前计。前面介紹的ngx_index模塊雖然可以在指定的首頁文件存在時發(fā)起“內(nèi)部跳轉(zhuǎn)”,但真正把相應(yīng)的首頁文件服務(wù)出去(即把該文件的內(nèi)容作為響應(yīng)體數(shù)據(jù)輸出垃杖,并設(shè)置相應(yīng)的響應(yīng)頭)男杈,還是得靠這個ngx_static模塊來完成。
來看一個ngx_static模塊服務(wù)磁盤文件的例子调俘。我們使用下面這個配置片段:
location/?{
root/var/www/;
}
同時在本機(jī)的/var/www/目錄下創(chuàng)建兩個文件伶棒,一個文件叫做index.html旺垒,內(nèi)容是一行文本this?is?my?home;另一個文件叫做hello.html肤无,內(nèi)容是一行文本hello?world. 同時注意這兩個文件的權(quán)限設(shè)置先蒋,確保它們都對運行 Nginx worker 進(jìn)程的系統(tǒng)帳戶可讀。
????現(xiàn)在來通過 HTTP 協(xié)議請求一下這兩個文件所對應(yīng)的 URI:
$?curl?'http://localhost:8080/index.html'
this?is?my?home
$?curl?'http://localhost:8080/hello.html'
hello?world
我們看到宛渐,先前創(chuàng)建的那兩個磁盤文件的內(nèi)容被分別輸出了鞭达。
不妨來分析一下這里發(fā)生的事情:location?/中沒有使用運行在content階段的模塊指令,于是也就沒有模塊注冊這個location的“內(nèi)容處理程序”皇忿,處理權(quán)便自動落到了在content階段“墊底”的那 3 個靜態(tài)資源服務(wù)模塊畴蹭。首先運行的ngx_index和ngx_autoindex模塊先后看到當(dāng)前請求的 URI,/index.html和/hello.html鳍烁,并不以/結(jié)尾叨襟,于是直接棄權(quán),將處理權(quán)轉(zhuǎn)給了最后運行的ngx_static模塊幔荒。ngx_static模塊根據(jù)root指令指定的“文檔根目錄”(document root)糊闽,分別將請求 URI/index.html和/hello.html映射為文件系統(tǒng)路徑/var/www/index.html和/var/www/hello.html,在確認(rèn)這兩個文件存在后爹梁,將它們的內(nèi)容分別作為響應(yīng)體輸出右犹,并自動設(shè)置Content-Type、Content-Length以及Last-Modified等響應(yīng)頭姚垃。
為了確認(rèn)ngx_static模塊確實運行了念链,可以啟用(一)中介紹過的 Nginx “調(diào)試日志”,然后再次請求/index.html這個接口积糯。此時掂墓,在 Nginx 錯誤日志文件中可以看到類似下面這一行的調(diào)試信息:
????[debug]?3033#0:?*1?http?static?fd:?8
這一行信息便是ngx_static模塊生成的,其含義是“正在輸出的靜態(tài)文件的描述符是數(shù)字8”看成。當(dāng)然君编,具體的文件描述符編號會經(jīng)常發(fā)生變化,這里只是我機(jī)器的一次典型輸出川慌。值得一提的是吃嘿,能生成這一行調(diào)試信息的還有標(biāo)準(zhǔn)模塊ngx_gzip_static,但它默認(rèn)是不啟用的梦重,后面會專門介紹到這個模塊兑燥。
注意上面這個例子中使用的root配置指令只起到了聲明“文檔根目錄”的作用,并不是它開啟了ngx_static模塊忍饰。ngx_static模塊總是處于開啟狀態(tài)贪嫂,但是否輪得到它運行就要看content階段先于它運行的那些模塊是否“棄權(quán)”了。為了進(jìn)一步確認(rèn)這一點艾蓝,來看下面這個空白location的定義:
location/?{
}
因為沒有配置root指令力崇,所以在訪問這個接口時斗塘,Nginx 會自動計算出一個缺省的“文檔根目錄”。該缺省值是取所謂的“配置前綴”(configure prefix)路徑下的html/子目錄亮靴。舉一個例子馍盟,假設(shè)“配置前綴”是/foo/bah/,則缺省的“文檔根目錄”便是/foo/bar/html/.
那么“配置前綴”是由什么來決定的呢茧吊?默認(rèn)情況下贞岭,就是 Nginx 安裝時的根目錄(或者說 Nginx 構(gòu)造時傳遞給./configure腳本的--prefix選項的路徑值)。如果 Nginx 安裝到了/usr/local/nginx/下搓侄,則“配置前綴”便是/usr/local/nginx/瞄桨,同時默認(rèn)的“文檔根目錄”便是/usr/local/nginx/html/. 不過,我們也可以在啟動 Nginx 的時候讶踪,通過--prefix命令行選項臨時指定自己的“配置前綴”路徑芯侥。假設(shè)我們啟動 Nginx 時使用的命令是
????nginx?-p?/home/agentzh/test/
則對于該服務(wù)器實例,其“配置前綴”便是/home/agentzh/test/乳讥,而默認(rèn)的“文檔根目錄”便是/home/agentzh/test/html/. “配置前綴”不僅會決定默認(rèn)的“文檔根目錄”柱查,還決定著 Nginx 配置文件中許多相對路徑值如何解釋為絕對路徑,后面我們還會看到許多需要引用到“配置前綴”的例子云石。
????獲取當(dāng)前“文檔根目錄”的路徑有一個非常簡便的方法唉工,那就是請求一個肯定不存在的文件所對應(yīng)的資源名,例如:
$?curl?'http://localhost:8080/blah-blah.txt'
404?Not?Found
404?Not?Found
nginx
我們會很自然地得到404錯誤頁汹忠。此時再看 Nginx 錯誤日志文件淋硝,應(yīng)該會看到類似下面這一行錯誤消息:
????[error]?9364#0:?*1?open()?"/home/agentzh/test/html/blah-blah.txt"?failed?(2:?No?such?file?or?directory)
這條錯誤消息是ngx_static模塊打印出來的,因為它并不能在文件系統(tǒng)的對應(yīng)路徑上找到名為blah-blah.txt的文件错维。因為這條錯誤信息中包含有ngx_static試圖打開的文件的絕對路徑奖地,所以從這個路徑不難看出,當(dāng)前的“文檔根目錄”是/home/agentzh/test/html/.
很多初學(xué)者會想當(dāng)然地把404錯誤理解為某個location不存在赋焕,其實上面這個例子表明,即使location存在并成功匹配仰楚,也是可能返回404錯誤頁的隆判。因為決定著404錯誤頁的是抽象的“資源”是否存在,而非某個具體的location是否存在僧界。
初學(xué)者常犯的一個錯誤是忘記配置content階段的模塊指令侨嘀,而他們自己其實并不期望使用content階段缺省運行的靜態(tài)資源服務(wù),例如:
location/auth?{
access_by_lua?'
--?a?lot?of?Lua?code?omitted?here...
';
}
顯然捂襟,這個/auth接口只定義了access階段的配置指令咬腕,即access_by_lua,并未定義任何content階段的配置指令葬荷。于是當(dāng)我們請求/auth接口時涨共,在access階段的 Lua 代碼會如期執(zhí)行纽帖,然后content階段的那些靜態(tài)文件服務(wù)會緊接著自動發(fā)生作用,直至ngx_static模塊去文件系統(tǒng)上找名為auth的文件举反。而經(jīng)常地懊直,404錯誤頁會拋出,除非運氣太好火鼻,在對應(yīng)路徑上確實存在一個叫做auth的文件室囊。所以,一條經(jīng)驗是魁索,當(dāng)遇到意外的404錯誤并且又不涉及靜態(tài)文件服務(wù)時融撞,應(yīng)當(dāng)首先檢查是否在對應(yīng)的location配置塊中恰當(dāng)?shù)嘏渲昧薱ontent階段的模塊指令,例如content_by_lua粗蔚、echo以及proxy_pass之類懦铺。當(dāng)然,Nginx 的error.log文件一般總是會提供各種意外問題的答案支鸡,例如對于上面這個例子冬念,我的error.log中有下面這條錯誤信息:
????[error]?9364#0:?*1?open()?"/home/agentzh/test/html/auth"?failed?(2:?No?such?file?or?directory)
前面我們詳細(xì)討論了rewrite、access和content這三個最為常見的 Nginx 請求處理階段牧挣,在此過程中急前,也順便介紹了運行在這三個階段的眾多 Nginx 模塊及其配置指令。同時可以看到瀑构,請求處理階段的劃分直接影響到了配置指令的執(zhí)行順序裆针,熟悉這些階段對于正確配置不同的 Nginx 模塊并實現(xiàn)它們彼此之間的協(xié)同工作是非常必要的。所以接下來我們接著討論余下的那些階段寺晌。
前面在(一)中提到世吨,Nginx 處理請求的過程一共劃分為 11 個階段,按照執(zhí)行順序依次是post-read呻征、server-rewrite耘婚、find-config、rewrite陆赋、post-rewrite沐祷、preaccess、access攒岛、post-access赖临、try-files、content以及l(fā)og.
最先執(zhí)行的post-read階段在 Nginx 讀取并解析完請求頭(request headers)之后就立即開始運行灾锯。這個階段像前面介紹過的rewrite階段那樣支持 Nginx 模塊注冊處理程序兢榨。比如標(biāo)準(zhǔn)模塊ngx_realip就在post-read階段注冊了處理程序,它的功能是迫使 Nginx 認(rèn)為當(dāng)前請求的來源地址是指定的某一個請求頭的值。下面這個例子就使用了ngx_realip模塊提供的set_real_ip_from和real_ip_header這兩條配置指令:
listen8080;
set_real_ip_from127.0.0.1;
real_ip_headerX-My-IP;
location/test?{
set$addr?$remote_addr;
echo?"from:?$addr";
}
}
這里的配置是讓 Nginx 把那些來自127.0.0.1的所有請求的來源地址吵聪,都改寫為請求頭X-My-IP所指定的值凌那。同時該例使用了標(biāo)準(zhǔn)內(nèi)建變量$remote_addr來輸出當(dāng)前請求的來源地址暖璧,以確認(rèn)是否被成功改寫案怯。
首先在本地請求一下這個/test接口:
$?curl?-H?'X-My-IP:?1.2.3.4'?localhost:8080/test
from:?1.2.3.4
這里使用了 curl 工具的-H選項指定了額外的 HTTP 請求頭X-My-IP:?1.2.3.4. 從輸出可以看到局蚀,$remote_addr變量的值確實在rewrite階段就已經(jīng)成為了X-My-IP請求頭中指定的值料祠,即1.2.3.4. 那么 Nginx 究竟是在什么時候改寫了當(dāng)前請求的來源地址呢?答案是:在post-read階段图焰。由于rewrite階段的運行遠(yuǎn)在post-read階段之后颗味,所以當(dāng)在location配置塊中通過set配置指令讀取$remote_addr內(nèi)建變量時谨娜,讀出的來源地址已經(jīng)是經(jīng)過post-read階段篡改過的族淮。
如果在請求上例中的/test接口時沒有指定X-My-IP請求頭,或者提供的X-My-IP請求頭的值不是合法的 IP 地址焦履,那么 Nginx 就不會對來源地址進(jìn)行改寫,例如:
$?curl?localhost:8080/test
from:?127.0.0.1
$?curl?-H?'X-My-IP:?abc'?localhost:8080/test
from:?127.0.0.1
如果從另一臺機(jī)器訪問這個/test接口卫玖,那么即使指定了合法的X-My-IP請求頭剪芥,也不會觸發(fā) Nginx 對來源地址進(jìn)行改寫偏塞。這是因為上例已經(jīng)使用set_real_ip_from指令規(guī)定了來源地址的改寫操作只對那些來自127.0.0.1的請求生效滔以。這種過濾機(jī)制可以避免來自其他不受信任的地址的惡意欺騙敦迄。當(dāng)然尖滚,也可以通過set_real_ip_from指令指定一個 IP 網(wǎng)段(利用(三)中介紹過的“CIDR 記法”)。此外,同時配置多個set_real_ip_from語句也是允許的质蕉,這樣可以指定多個受信任的來源地址或地址段绷蹲。下面是一個例子:
set_real_ip_from10.32.10.5;
set_real_ip_from127.0.0.0/24;
有的讀者可能會問,ngx_realip模塊究竟有什么實際用途呢顾孽?為什么我們需要去改寫請求的來源地址呢祝钢?答案是:當(dāng) Nginx 處理的請求經(jīng)過了某個 HTTP 代理服務(wù)器的轉(zhuǎn)發(fā)時,這個模塊就變得特別有用若厚。當(dāng)原始的用戶請求經(jīng)過轉(zhuǎn)發(fā)之后拦英,Nginx 接收到的請求的來源地址無一例外地變成了該代理服務(wù)器的 IP 地址,于是 Nginx 以及 Nginx 背后的應(yīng)用就無法知道原始請求的真實來源测秸。所以疤估,一般我們會在 Nginx 之前的代理服務(wù)器中把請求的原始來源地址編碼進(jìn)某個特殊的 HTTP 請求頭中(例如上例中的X-My-IP請求頭)灾常,然后再在 Nginx 一側(cè)把這個請求頭中編碼的地址恢復(fù)出來。這樣 Nginx 中的后續(xù)處理階段(包括 Nginx 背后的各種后端應(yīng)用)就會認(rèn)為這些請求直接來自那些原始的地址铃拇,代理服務(wù)器就仿佛不存在一樣钞瀑。正是因為這個需求,所以ngx_realip模塊才需要在第一個處理階段慷荔,即post-read階段雕什,注冊處理程序,以便盡可能早地改寫請求的來源显晶。
post-read階段之后便是server-rewrite階段贷岸。我們曾在(二)中簡單提到,當(dāng)ngx_rewrite模塊的配置指令直接書寫在server配置塊中時磷雇,基本上都是運行在server-rewrite階段偿警。下面就來看這樣的一個例子:
listen8080;
location/test?{
set$b?"$a,?world";
echo?$b;
}
set$a?hello;
}
這里,配置語句set?$a?hello直接寫在了server配置塊中唯笙,因此它就運行在server-rewrite階段螟蒸。而server-rewrite階段要早于rewrite階段運行,因此寫在location配置塊中的語句set?$b?"$a,?world"便晚于外面的set?$a?hello語句運行睁本。該例的測試結(jié)果證明了這一點:
$?curl?localhost:8080/test
hello,?world
由于server-rewrite階段位于post-read階段之后尿庐,所以server配置塊中的set指令也就總是運行在ngx_realip模塊改寫請求的來源地址之后。來看下面這個例子:
listen8080;
set$addr?$remote_addr;
set_real_ip_from127.0.0.1;
real_ip_headerX-Real-IP;
location/test?{
echo?"from:?$addr";
}
}
請求/test接口的結(jié)果如下:
$?curl?-H?'X-Real-IP:?1.2.3.4'?localhost:8080/test
from:?1.2.3.4
在這個例子中呢堰,雖然set指令寫在了ngx_realip的配置指令之前抄瑟,但仍然晚于ngx_realip模塊執(zhí)行。所以$addr變量在server-rewrite階段被set指令賦值時枉疼,從$remote_addr變量讀出的來源地址已經(jīng)是經(jīng)過改寫過的了皮假。
緊接在server-rewrite階段后邊的是find-config階段。這個階段并不支持 Nginx 模塊注冊處理程序骂维,而是由 Nginx 核心來完成當(dāng)前請求與location配置塊之間的配對工作惹资。換句話說,在此階段之前航闺,請求并沒有與任何location配置塊相關(guān)聯(lián)褪测。因此,對于運行在find-config階段之前的post-read和server-rewrite階段來說潦刃,只有server配置塊以及更外層作用域中的配置指令才會起作用侮措。這就是為什么只有寫在server配置塊中的ngx_rewrite模塊的指令才會運行在server-rewrite階段,這也是為什么前面所有例子中的ngx_realip模塊的指令也都特意寫在了server配置塊中乖杠,以確保其注冊在post-read階段的處理程序能夠生效分扎。
當(dāng) Nginx 在find-config階段成功匹配了一個location配置塊后,會立即打印一條調(diào)試信息到錯誤日志文件中胧洒。我們來看這樣的一個例子:
location/hello?{
echo?"hello?world";
}
如果啟用了 Nginx 的“調(diào)試日志”畏吓,那么當(dāng)請求/hello接口時墨状,便可以在error.log文件中過濾出下面這一行信息:
$?grep?'using?config'?logs/error.log
[debug]?84579#0:?*1?using?configuration?"/hello"
我們有意省略了信息行首的時間戳,以便放在這里菲饼。
運行在find-config階段之后的便是我們的老朋友rewrite階段肾砂。由于 Nginx 已經(jīng)在find-config階段完成了當(dāng)前請求與location的配對,所以從rewrite階段開始巴粪,location配置塊中的指令便可以產(chǎn)生作用通今。前面已經(jīng)介紹過,當(dāng)ngx_rewrite模塊的指令用于location塊中時肛根,便是運行在這個rewrite階段。另外漏策,ngx_set_misc模塊的指令也是如此派哲,還有ngx_lua模塊的set_by_lua指令和rewrite_by_lua指令也不例外。
rewrite階段再往后便是所謂的post-rewrite階段掺喻。這個階段也像find-config階段那樣不接受 Nginx 模塊注冊處理程序芭届,而是由 Nginx 核心完成rewrite階段所要求的“內(nèi)部跳轉(zhuǎn)”操作(如果rewrite階段有此要求的話)。先前在(二)中已經(jīng)介紹過了“內(nèi)部跳轉(zhuǎn)”的概念感耙,同時演示了如何通過echo_exec指令或者rewrite指令來發(fā)起“內(nèi)部跳轉(zhuǎn)”褂乍。由于echo_exec指令運行在content階段,與這里討論的post-rewrite階段無關(guān)即硼,于是我們感興趣的便只剩下運行在rewrite階段的rewrite指令逃片。回顧一下(二)中演示過的這個例子:
listen8080;
location/foo?{
set$a?hello;
rewrite^?/bar;
}
location/bar?{
echo?"a?=?[$a]";
}
}
這里在location?/foo中通過rewrite指令把當(dāng)前請求的 URI 無條件地改寫為/bar只酥,同時發(fā)起一個“內(nèi)部跳轉(zhuǎn)”褥实,最終跳進(jìn)了location?/bar中。這里比較有趣的地方是“內(nèi)部跳轉(zhuǎn)”的工作原理裂允∷鹄耄“內(nèi)部跳轉(zhuǎn)”本質(zhì)上其實就是把當(dāng)前的請求處理階段強(qiáng)行倒退到find-config階段,以便重新進(jìn)行請求 URI 與location配置塊的配對绝编。比如上例中僻澎,運行在rewrite階段的rewrite指令就讓當(dāng)前請求的處理階段倒退回了find-config階段。由于此時當(dāng)前請求的 URI 已經(jīng)被rewrite指令修改為了/bar十饥,所以這一次換成了location?/bar與當(dāng)前請求相關(guān)聯(lián)窟勃,然后再接著從rewrite階段往下執(zhí)行。
不過這里更有趣的地方是绷跑,倒退回find-config階段的動作并不是發(fā)生在rewrite階段拳恋,而是發(fā)生在后面的post-rewrite階段。上例中的rewrite指令只是簡單地指示 Nginx 有必要在post-rewrite階段發(fā)起“內(nèi)部跳轉(zhuǎn)”砸捏。這個設(shè)計對于 Nginx 初學(xué)者來說谬运,或許顯得有些古怪:“為什么不直接在rewrite指令執(zhí)行時立即進(jìn)行跳轉(zhuǎn)呢隙赁?”答案其實很簡單,那就是為了在最初匹配的location塊中支持多次反復(fù)地改寫 URI梆暖,例如:
location/foo?{
rewrite^?/bar;
rewrite^?/baz;
echo?foo;
}
location/bar?{
echo?bar;
}
location/baz?{
echo?baz;
}
這里在location?/foo中連續(xù)把當(dāng)前請求的 URI 改寫了兩遍:第一遍先無條件地改寫為/bar伞访,第二遍再無條件地改寫為/baz. 而這兩條rewrite語句只會最終導(dǎo)致post-rewrite階段發(fā)生一次“內(nèi)部跳轉(zhuǎn)”操作,從而不至于在第一次改寫 URI 時就直接跳離了當(dāng)前的location而導(dǎo)致后面的rewrite語句沒有機(jī)會執(zhí)行轰驳。請求/foo接口的結(jié)果證實了這一點:
$?curl?localhost:8080/foo
baz
從輸出結(jié)果可以看到厚掷,上例確實成功地從/foo一步跳到了/baz中。如果啟用 Nginx “調(diào)試日志”的話级解,還可以從find-config階段生成的locatin塊的匹配信息中進(jìn)一步證實這一點:
$?grep?'using?config'?logs/error.log
[debug]?89449#0:?*1?using?configuration?"/foo"
[debug]?89449#0:?*1?using?configuration?"/baz"
我們看到冒黑,對于該次請求,Nginx 一共只匹配過/foo和/baz這兩個location勤哗,從而只發(fā)生過一次“內(nèi)部跳轉(zhuǎn)”抡爹。
當(dāng)然,如果在server配置塊中直接使用rewrite配置指令對請求 URI 進(jìn)行改寫芒划,則不會涉及“內(nèi)部跳轉(zhuǎn)”冬竟,因為此時 URI 改寫發(fā)生在server-rewrite階段,早于執(zhí)行l(wèi)ocation配對的find-config階段民逼。比如下面這個例子:
listen8080;
rewrite^/foo?/bar;
location/foo?{
echo?foo;
}
location/bar?{
echo?bar;
}
}
這里泵殴,我們在server-rewrite階段就把那些以/foo起始的 URI 改寫為/bar,而此時請求并沒有和任何location相關(guān)聯(lián)拼苍,所以 Nginx 正常往下運行find-config階段笑诅,完成最終的location匹配。如果我們請求上例中的/foo接口映屋,那么location?/foo根本就沒有機(jī)會匹配苟鸯,因為在第一次(也是唯一的一次)運行find-config階段時,當(dāng)前請求的 URI 已經(jīng)被改寫為/bar棚点,從而只會匹配location?/bar. 實際請求的輸出正是如此:
$?curl?localhost:8080/foo
bar
Nginx “調(diào)試日志”可以再一次佐證我們的結(jié)論:
$?grep?'using?config'?logs/error.log
[debug]?92693#0:?*1?using?configuration?"/bar"
可以看到早处,Nginx 總共只進(jìn)行過一次location匹配,并無“內(nèi)部跳轉(zhuǎn)”發(fā)生瘫析。
運行在post-rewrite階段之后的是所謂的preaccess階段砌梆。該階段在access階段之前執(zhí)行,故名preaccess.
標(biāo)準(zhǔn)模塊ngx_limit_req和ngx_limit_zone就運行在此階段贬循,前者可以控制請求的訪問頻度咸包,而后者可以限制訪問的并發(fā)度。這里我們僅僅和它們打個照面杖虾,后面還會有機(jī)會專門接觸到這兩個模塊烂瘫。
前面反復(fù)提到的標(biāo)準(zhǔn)模塊ngx_realip其實也在這個階段注冊了處理程序。有些讀者可能會問:“這是為什么呢?它不是已經(jīng)在post-read階段注冊處理程序了嗎坟比?”我們不妨通過下面這個例子來揭曉答案:
listen8080;
location/test?{
set_real_ip_from127.0.0.1;
real_ip_headerX-Real-IP;
echo?"from:?$remote_addr";
}
}
與先看前到的例子相比芦鳍,此例最重要的區(qū)別在于把ngx_realip的配置指令放在了location配置塊中。前面我們介紹過葛账,Nginx 匹配location的動作發(fā)生在find-config階段柠衅,而find-config階段遠(yuǎn)遠(yuǎn)晚于post-read階段執(zhí)行,所以在post-read階段籍琳,當(dāng)前請求還沒有和任何location相關(guān)聯(lián)菲宴。在這個例子中,因為ngx_realip的配置指令都寫在了location配置塊中趋急,所以在post-read階段喝峦,ngx_realip模塊的處理程序沒有看到任何可用的配置信息,便不會執(zhí)行來源地址的改寫工作了宣谈。
為了解決這個難題愈犹,ngx_realip模塊便又特意在preaccess階段注冊了處理程序,這樣它才有機(jī)會運行l(wèi)ocation塊中的配置指令闻丑。正是因為這個緣故,上面這個例子的運行結(jié)果才符合直覺預(yù)期:
$?curl?-H?'X-Real-IP:?1.2.3.4'?localhost:8080/test
from:?1.2.3.4
不幸的是勋颖,ngx_realip模塊的這個解決方案還是存在漏洞的嗦嗡,比如下面這個例子:
listen8080;
location/test?{
set_real_ip_from127.0.0.1;
real_ip_headerX-Real-IP;
set$addr?$remote_addr;
echo?"from:?$addr";
}
}
這里,我們在rewrite階段將$remote_addr的值保存到了用戶變量$addr中饭玲,然后再輸出侥祭。因為rewrite階段先于preaccess階段執(zhí)行,所以當(dāng)ngx_realip模塊尚未在preaccess階段改寫來源地址時茄厘,最初的來源地址就已經(jīng)在rewrite階段被讀取了矮冬。上例的實際請求結(jié)果證明了我們的結(jié)論:
$?curl?-H?'X-Real-IP:?1.2.3.4'?localhost:8080/test
from:?127.0.0.1
輸出的地址確實是未經(jīng)改寫過的。Nginx 的“調(diào)試日志”可以進(jìn)一步確認(rèn)這一點:
$?grep?-E?'http?script?(var|set)|realip'?logs/error.log
[debug]?32488#0:?*1?http?script?var:?"127.0.0.1"
[debug]?32488#0:?*1?http?script?set?$addr
[debug]?32488#0:?*1?realip:?"1.2.3.4"
[debug]?32488#0:?*1?realip:?0100007F?FFFFFFFF?0100007F
[debug]?32488#0:?*1?http?script?var:?"127.0.0.1"
其中第一行調(diào)試信息
????[debug]?32488#0:?*1?http?script?var:?"127.0.0.1"
是set語句讀取$remote_addr變量時產(chǎn)生的次哈。信息中的字符串"127.0.0.1"便是$remote_addr當(dāng)時讀出來的值胎署。
????而第二行調(diào)試信息
????[debug]?32488#0:?*1?http?script?set?$addr
則顯示我們對變量$addr進(jìn)行了賦值操作。
????后面兩行信息
[debug]?32488#0:?*1?realip:?"1.2.3.4"
[debug]?32488#0:?*1?realip:?0100007F?FFFFFFFF?0100007F
是ngx_realip模塊在preaccess階段改寫當(dāng)前請求的來源地址窑滞。我們看到琼牧,改寫后的新地址確實是期望的1.2.3.4. 但很明顯這個操作發(fā)生在$addr變量賦值之后,所以已經(jīng)太遲了哀卫。
????而最后一行信息
????[debug]?32488#0:?*1?http?script?var:?"127.0.0.1"
則是echo配置指令在輸出時讀取變量$addr時產(chǎn)生的巨坊,我們看到它的值是改寫前的來源地址。
看到這里此改,有的讀者可能會問:“如果ngx_realip模塊不在preaccess階段注冊處理程序趾撵,而在rewrite階段注冊,那么上例不就可以工作了共啃?”答案是:不一定占调。因為ngx_rewrite模塊的處理程序也同樣注冊在rewrite階段暂题,而前面我們在(二)中特別提到,在這種情況下妈候,不同模塊之間的執(zhí)行順序一般是不確定的敢靡,所以ngx_realip的處理程序可能仍然在set語句之后執(zhí)行。
一個建議是:盡量在server配置塊中配置ngx_realip這樣的模塊苦银,以避免上面介紹的這種棘手的例外情況啸胧。
運行在preaccess階段之后的則是我們的另一個老朋友,access階段幔虏。前面我們已經(jīng)知道了纺念,標(biāo)準(zhǔn)模塊ngx_access、第三方模塊ngx_auth_request以及第三方模塊ngx_lua的access_by_lua指令就運行在這個階段想括。
access階段之后便是post-access階段陷谱。從這個階段的名字,我們也能一眼看出它是緊跟在access階段后面執(zhí)行的瑟蜈。這個階段也和post-rewrite階段類似烟逊,并不支持 Nginx 模塊注冊處理程序,而是由 Nginx 核心自己完成一些處理工作铺根。post-access階段主要用于配合access階段實現(xiàn)標(biāo)準(zhǔn)ngx_http_core模塊提供的配置指令satisfy的功能宪躯。
對于多個 Nginx 模塊注冊在access階段的處理程序,satisfy配置指令可以用于控制它們彼此之間的協(xié)作方式位迂。比如模塊 A 和 B 都在access階段注冊了與訪問控制相關(guān)的處理程序访雪,那就有兩種協(xié)作方式,一是模塊 A 和模塊 B 都得通過驗證才算通過掂林,二是模塊 A 和模塊 B 只要其中任一個通過驗證就算通過臣缀。第一種協(xié)作方式稱為all方式(或者說“與關(guān)系”),第二種方式則被稱為any方式(或者說“或關(guān)系”)泻帮。默認(rèn)情況下精置,Nginx 使用的是all方式。下面是一個例子:
location/test?{
satisfy?all;
denyall;
access_by_lua?'ngx.exit(ngx.OK)';
echo?something?important;
}
這里刑顺,我們在/test接口中同時配置了ngx_access模塊和ngx_lua模塊氯窍,這樣access階段就由這兩個模塊一起來做檢驗工作。其中蹲堂,語句deny?all會讓ngx_access模塊的處理程序總是拒絕當(dāng)前請求狼讨,而語句access_by_lua?'ngx.exit(ngx.OK)'則總是允許訪問。當(dāng)我們通過satisfy指令配置了all方式時柒竞,就需要access階段的所有模塊都通過驗證政供,但不幸的是,這里ngx_access模塊總是會拒絕訪問,所以整個請求就會被拒:
$?curl?localhost:8080/test
403?Forbidden
403?Forbidden
nginx
細(xì)心的讀者會在 Nginx 錯誤日志文件中看到類似下面這一行的出錯信息:
????[error]?6549\#0:?*1?access?forbidden?by?rule
然而布隔,如果我們把上例中的satisfy?all語句更改為satisfy?any离陶,
location/test?{
satisfy?any;
denyall;
access_by_lua?'ngx.exit(ngx.OK)';
echo?something?important;
}
結(jié)果則會完全不同:
$?curl?localhost:8080/test
something?important
即請求反而最終通過了驗證。這是因為在any方式下衅檀,access階段只要有一個模塊通過了驗證招刨,就會認(rèn)為請求整體通過了驗證,而在上例中哀军,ngx_lua模塊的access_by_lua語句總是會通過驗證的沉眶。
在配置了satisfy?any的情況下,只有當(dāng)access階段的所有模塊的處理程序都拒絕訪問時杉适,整個請求才會被拒谎倔,例如:
location/test?{
satisfy?any;
denyall;
access_by_lua?'ngx.exit(ngx.HTTP_FORBIDDEN)';
echo?something?important;
}
此時訪問/test接口才會得到403?Forbidden錯誤頁。這里猿推,post-access階段參與了access階段各模塊處理程序的“或關(guān)系”的實現(xiàn)片习。
值得一提的是,上面這幾個的例子需要ngx_lua0.5.0rc19 或以上版本蹬叭;之前的版本是不能和satisfy?any配置語句一起工作的藕咏。
緊跟在post-access階段之后的是try-files階段。這個階段專門用于實現(xiàn)標(biāo)準(zhǔn)配置指令try_files的功能秽五,并不支持 Nginx 模塊注冊處理程序侈离。由于try_files指令在許多 FastCGI 應(yīng)用的配置中都有用到,所以我們不妨在這里簡單介紹一下筝蚕。
try_files指令接受兩個以上任意數(shù)量的參數(shù),每個參數(shù)都指定了一個 URI. 這里假設(shè)配置了N個參數(shù)铺坞,則 Nginx 會在try-files階段起宽,依次把前N-1個參數(shù)映射為文件系統(tǒng)上的對象(文件或者目錄),然后檢查這些對象是否存在济榨。一旦 Nginx 發(fā)現(xiàn)某個文件系統(tǒng)對象存在坯沪,就會在try-files階段把當(dāng)前請求的 URI 改寫為該對象所對應(yīng)的參數(shù) URI(但不會包含末尾的斜杠字符,也不會發(fā)生 “內(nèi)部跳轉(zhuǎn)”)腐晾。如果前N-1個參數(shù)所對應(yīng)的文件系統(tǒng)對象都不存在库车,try-files階段就會立即發(fā)起“內(nèi)部跳轉(zhuǎn)”到最后一個參數(shù)(即第N個參數(shù))所指定的 URI.
前面在(六)和(七)中已經(jīng)看到靜態(tài)資源服務(wù)模塊會把當(dāng)前請求的 URI 映射到文件系統(tǒng),通過root配置指令所指定的“文檔根目錄”進(jìn)行映射呛占。例如笙隙,當(dāng)“文檔根目錄”是/var/www/的時候莽鸿,請求 URI/foo/bar會被映射為文件/var/www/foo/bar创千,而請求 URI/foo/baz/則會被映射為目錄/var/www/foo/baz/. 注意這里是如何通過 URI 末尾的斜杠字符是否存在來區(qū)分“目錄”和“文件”的丙曙。我們正在討論的try_files配置指令使用同樣的規(guī)則來完成其各個參數(shù) URI 到文件系統(tǒng)對象的映射耸黑。
????不妨來看下面這個例子:
root/var/www/;
location/test?{
try_files/foo?/bar/?/baz;
echo?"uri:?$uri";
}
location/foo?{
echo?foo;
}
location/bar/?{
echo?bar;
}
location/baz?{
echo?baz;
}
這里通過root指令把“文檔根目錄”配置為/var/www/男翰,如果你系統(tǒng)中的/var/www/路徑下存放有重要數(shù)據(jù),則可以把它替換為其他任意路徑昆箕,但此路徑對運行 Nginx worker 進(jìn)程的系統(tǒng)帳號至少有可讀權(quán)限。我們在location?/test中使用了try_files配置指令窟社,并提供了三個參數(shù)关炼,/foo、/bar/和/baz. 根據(jù)前面對try_files指令的介紹匣吊,我們可以知道儒拂,它會在try-files階段依次檢查前兩個參數(shù)/foo和/bar/所對應(yīng)的文件系統(tǒng)對象是否存在。
不妨先來做一組實驗缀去。假設(shè)現(xiàn)在/var/www/路徑下是空的侣灶,則第一個參數(shù)/foo映射成的文件/var/www/foo是不存在的;同樣缕碎,對于第二個參數(shù)/bar/所映射成的目錄/var/www/bar/也是不存在的褥影。于是此時 Nginx 會在try-files階段發(fā)起到最后一個參數(shù)所指定的 URI(即/baz)的“內(nèi)部跳轉(zhuǎn)”。實際的請求結(jié)果證實了這一點:
$?curl?localhost:8080/test
baz
顯然咏雌,該請求最終和location?/baz綁定在一起凡怎,執(zhí)行了輸出baz字符串的工作。上例中定義的location?/foo和location?/bar/完全不會參與這里的運行過程赊抖,因為對于try_files的前N-1個參數(shù)统倒,Nginx 只會檢查文件系統(tǒng),而不會去執(zhí)行 URI 與location之間的匹配氛雪。
????對于上面這個請求房匆,Nginx 會產(chǎn)生類似下面這樣的“調(diào)試日志”:
$?grep?trying?logs/error.log
[debug]?3869#0:?*1?trying?to?use?file:?"/foo"?"/var/www/foo"
[debug]?3869#0:?*1?trying?to?use?dir:?"/bar"?"/var/www/bar"
[debug]?3869#0:?*1?trying?to?use?file:?"/baz"?"/var/www/baz"
通過這些信息可以清楚地看到try-files階段發(fā)生的事情:Nginx 依次檢查了文件/var/www/foo和目錄/var/www/bar,末了又處理了最后一個參數(shù)/baz. 這里最后一條“調(diào)試信息”容易產(chǎn)生誤解报亩,會讓人誤以為 Nginx 也把最后一個參數(shù)/baz給映射成了文件系統(tǒng)對象進(jìn)行檢查浴鸿,事實并非如此。當(dāng)try_files指令處理到它的最后一個參數(shù)時弦追,總是直接執(zhí)行“內(nèi)部跳轉(zhuǎn)”岳链,而不論其對應(yīng)的文件系統(tǒng)對象是否存在。
接下來再做一組實驗:在/var/www/下創(chuàng)建一個名為foo的文件劲件,其內(nèi)容為hello?world(注意你需要有/var/www/目錄下的寫權(quán)限):
????$?echo?'hello?world'?>?/var/www/foo
然后再請求/test接口:
$?curl?localhost:8080/test
uri:?/foo
這里發(fā)生了什么掸哑?我們來看世剖,try_files指令的第一個參數(shù)/foo可以映射為文件/var/www/foo吩跋,而 Nginx 在try-files階段發(fā)現(xiàn)此文件確實存在疆柔,于是立即把當(dāng)前請求的 URI 改寫為這個參數(shù)的值弊攘,即/foo,并且不再繼續(xù)檢查后面的參數(shù)俭嘁,而直接運行后面的請求處理階段躺枕。
上面這個請求在try-files階段所產(chǎn)生的“調(diào)試日志”如下:
$?grep?trying?logs/error.log
[debug]?4132#0:?*1?trying?to?use?file:?"/foo"?"/var/www/foo"
顯然,在try-files階段供填,Nginx 確實只檢查和處理了/foo這一個參數(shù)拐云,而后面的參數(shù)都被“短路”掉了。
類似地近她,假設(shè)我們刪除剛才創(chuàng)建的/var/www/foo文件叉瘩,而在/var/www/下創(chuàng)建一個名為bar的子目錄:
????$?mkdir?/var/www/bar
則請求/test的結(jié)果也是類似的:
$?curl?localhost:8080/test
uri:?/bar
在這種情況下,Nginx 在try-files階段發(fā)現(xiàn)第一個參數(shù)/foo對應(yīng)的文件不存在粘捎,就會轉(zhuǎn)向檢查第二個參數(shù)對應(yīng)的文件系統(tǒng)對象(在這里便是目錄/var/www/bar/)薇缅。由于此目錄存在,Nginx 就會把當(dāng)前請求的 URI 改寫為第二個參數(shù)的值攒磨,即/bar(注意泳桦,原始參數(shù)值是/bar/,但try_files會自動去除末尾的斜杠字符)娩缰。
????這一組實驗所產(chǎn)生的“調(diào)試日志”如下:
$?grep?trying?logs/error.log
[debug]?4223#0:?*1?trying?to?use?file:?"/foo"?"/var/www/foo"
[debug]?4223#0:?*1?trying?to?use?dir:?"/bar"?"/var/www/bar"
我們看到灸撰,try_files指令在這里只檢查和處理了它的前兩個參數(shù)。
通過前面這幾組實驗不難看到拼坎,try_files指令本質(zhì)上只是有條件地改寫當(dāng)前請求的 URI浮毯,而這里說的“條件”其實就是文件系統(tǒng)上的對象是否存在。當(dāng)“條件”都不滿足時泰鸡,它就會無條件地發(fā)起一個指定的“內(nèi)部跳轉(zhuǎn)”债蓝。當(dāng)然,除了無條件地發(fā)起“內(nèi)部跳轉(zhuǎn)”之外盛龄,try_files指令還支持直接返回指定狀態(tài)碼的 HTTP 錯誤頁饰迹,例如:
try_files/foo?/bar/?=404;
這行配置是說,當(dāng)/foo和/bar/參數(shù)所對應(yīng)的文件系統(tǒng)對象都不存在時余舶,就直接返回404?Not?Found錯誤頁蹦锋。注意這里它是如何使用等號字符前綴來標(biāo)識 HTTP 狀態(tài)碼的。