Nginx 介紹
Nginx (engine x) 是一個(gè)高性能的HTTP和反向代理服務(wù)器,也是一個(gè)IMAP/POP3/SMTP服務(wù)器。
Nginx是一款輕量級(jí)的Web 服務(wù)器/反向代理服務(wù)器及電子郵件(IMAP/POP3)代理服務(wù)器,并在一個(gè)BSD-like 協(xié)議下發(fā)行。其特點(diǎn)是占有內(nèi)存少,并發(fā)能力強(qiáng)
OpenResty介紹
OpenResty 是一個(gè)基于 Nginx 與 Lua 的高性能 Web 平臺(tái),其內(nèi)部集成了大量精良的 Lua 庫(kù)、第三方模塊以及大多數(shù)的依賴(lài)項(xiàng)。用于方便地搭建能夠處理超高并發(fā)、擴(kuò)展性極高的動(dòng)態(tài) Web 應(yīng)用、Web 服務(wù)和動(dòng)態(tài)網(wǎng)關(guān)
執(zhí)行階段前言
location /test { set $a 32; echo $a; set $a 56; echo $a; }
兩次都會(huì)輸出56,因?yàn)閟et階段始終在content階段之前執(zhí)行,跟代碼的先后順序無(wú)關(guān)。
Nginx執(zhí)行階段
Nginx 處理請(qǐng)求的過(guò)程一共劃分為 11 個(gè)階段,按照?qǐng)?zhí)行順序依次是 post-read、server-rewrite、find-config、rewrite、post-rewrite、preaccess、access、post-access、try-files、content 以及 log
post-read 階段
該階段Nginx標(biāo)準(zhǔn)函數(shù) set_real_ip_from、real_ip_header
最先執(zhí)行的 post-read 階段在 Nginx 讀取并解析完請(qǐng)求頭(request headers)之后就立即開(kāi)始運(yùn)行。標(biāo)準(zhǔn)模塊 ngx_realip 就在 post-read 階段注冊(cè)了處理程序,它的功能是迫使 Nginx 認(rèn)為當(dāng)前請(qǐng)求的來(lái)源地址是指定的某一個(gè)請(qǐng)求頭的值。下面這個(gè)例子就使用了 ngx_realip 模塊提供的 set_real_ip_from 和 real_ip_header
server { listen 8080; set_real_ip_from 127.0.0.1; real_ip_header X-My-IP; location /test { set $addr $remote_addr; echo "from: $addr"; } }
這里的配置是讓 Nginx 把那些來(lái)自 127.0.0.1 的所有請(qǐng)求的來(lái)源地址,都改寫(xiě)為請(qǐng)求頭 X-My-IP 所指定的值。同時(shí)該例使用了標(biāo)準(zhǔn)內(nèi)建變量 $remote_addr 來(lái)輸出當(dāng)前請(qǐng)求的來(lái)源地址,以確認(rèn)是否被成功改寫(xiě)。
$ curl -H 'X-My-IP: 1.2.3.4' localhost:8080/test from: 1.2.3.4
server-rewrite階段
該階段包含標(biāo)準(zhǔn)函數(shù)ngx_rewrite、set 以及openresty函數(shù)set_by_lua、rewrite_by_lua
post-read 階段之后便是 server-rewrite 階段。當(dāng) ngx_rewrite 模塊的配置指令直接書(shū)寫(xiě)在 server 配置塊中時(shí),基本上都是運(yùn)行在 server-rewrite 階段。
server { listen 8080; location /test { set $b "$a, world"; echo $b; } set $a hello; }
這里,配置語(yǔ)句 set $a hello 直接寫(xiě)在了 server 配置塊中,因此它就運(yùn)行在 server-rewrite 階段。而 server-rewrite 階段要早于 rewrite 階段運(yùn)行,因此寫(xiě)在 location 配置塊中的語(yǔ)句 set $b “$a, world” 便晚于外面的 set $a hello 語(yǔ)句運(yùn)行。該例的測(cè)試結(jié)果證明了這一點(diǎn):
$ curl localhost:8080/test hello, world
find-config 階段
這個(gè)階段并不支持 Nginx 模塊注冊(cè)處理程序,而是由 Nginx 核心來(lái)完成當(dāng)前請(qǐng)求與 location 配置塊之間的配對(duì)工作。
location /hello { echo "hello world"; }
rewrite 階段
該階段包含標(biāo)準(zhǔn)函數(shù)set_unescape_uri、rewrite以及openresty函數(shù)set_by_lua、 rewrite_by_lua
post-rewrite 階段
post-rewrite 階段,不接受 Nginx 模塊注冊(cè)處理程序,而是由 Nginx 核心完成 rewrite 階段所要求的“內(nèi)部跳轉(zhuǎn)”操作
“內(nèi)部跳轉(zhuǎn)”的工作原理:本質(zhì)上其實(shí)就是把當(dāng)前的請(qǐng)求處理階段強(qiáng)行倒退到 find-config 階段,以便重新進(jìn)行請(qǐng)求 URI 與 location 配置塊的配對(duì)。比如例中,運(yùn)行在 rewrite 階段的 rewrite 指令就讓當(dāng)前請(qǐng)求的處理階段倒退回了 find-config 階段。由于此時(shí)當(dāng)前請(qǐng)求的 URI 已經(jīng)被 rewrite 指令修改為了 /bar,所以這一次換成了 location /bar 與當(dāng)前請(qǐng)求相關(guān)聯(lián),然后再接著從 rewrite 階段往下執(zhí)行。
為什么不直接在 rewrite 指令執(zhí)行時(shí)立即進(jìn)行跳轉(zhuǎn)呢?
為了在最初匹配的 location 塊中支持多次反復(fù)地改寫(xiě) URI
server { listen 8080; location /foo { set $a hello; rewrite ^ /bar; } location /bar { echo "a = [$a]"; } }
location /foo { rewrite ^ /bar; rewrite ^ /baz; echo foo; } location /bar { echo bar; } location /baz { echo baz; }
注意的:如果在 server 配置塊中直接使用 rewrite 配置指令對(duì)請(qǐng)求 URI 進(jìn)行改寫(xiě),則不會(huì)涉及“內(nèi)部跳轉(zhuǎn)”
server { listen 8080; rewrite ^/foo /bar; location /foo { echo foo; } location /bar { echo bar; } }
preaccess 階段
該階段包含標(biāo)準(zhǔn)函數(shù)ngx_access-allow deny ngx_limit_req 和 ngx_limit_zone ngx_auth_request 以及openresty函數(shù)access_by_lua其中也包含了限頻限流模塊resty.limit.req resty.limit.conn
注意的是:標(biāo)準(zhǔn)模塊 ngx_realip 其實(shí)也在這個(gè)階段注冊(cè)了處理程序
server { listen 8080; location /test { set_real_ip_from 127.0.0.1; real_ip_header X-Real-IP; echo "from: $remote_addr"; } }
與先看前到的例子相比,此例最重要的區(qū)別在于把 ngx_realip 的配置指令放在了 location 配置塊中。前面我們介紹過(guò),Nginx 匹配 location 的動(dòng)作發(fā)生在 find-config 階段,而 find-config 階段遠(yuǎn)遠(yuǎn)晚于 post-read 階段執(zhí)行,所以在 post-read 階段,當(dāng)前請(qǐng)求還沒(méi)有和任何 location 相關(guān)聯(lián)。
建議是:盡量在 server 配置塊中配置 ngx_realip 這樣的模塊
post-access階段
該階段不支持 Nginx 模塊注冊(cè)處理程序,而是由 Nginx 核心自己完成一些處理工作
try-files 階段
實(shí)現(xiàn)標(biāo)準(zhǔn)配置指令 try_files 的功能,并不支持 Nginx 模塊注冊(cè)處理程序。
try_files 指令接受兩個(gè)以上任意數(shù)量的參數(shù),每個(gè)參數(shù)都指定了一個(gè) URI. 這里假設(shè)配置了 N 個(gè)參數(shù),則 Nginx 會(huì)在 try-files 階段,依次把前 N-1 個(gè)參數(shù)映射為文件系統(tǒng)上的對(duì)象(文件或者目錄),然后檢查這些對(duì)象是否存在。一旦 Nginx 發(fā)現(xiàn)某個(gè)文件系統(tǒng)對(duì)象存在,就會(huì)在 try-files 階段把當(dāng)前請(qǐng)求的 URI 改寫(xiě)為該對(duì)象所對(duì)應(yīng)的參數(shù) URI(但不會(huì)包含末尾的斜杠字符,也不會(huì)發(fā)生 “內(nèi)部跳轉(zhuǎn)”)。如果前 N-1 個(gè)參數(shù)所對(duì)應(yīng)的文件系統(tǒng)對(duì)象都不存在,try-files 階段就會(huì)立即發(fā)起“內(nèi)部跳轉(zhuǎn)”到最后一個(gè)參數(shù)(即第 N 個(gè)參數(shù))所指定的 URI.
location /test { try_files /foo /bar/ /baz; echo "uri: $uri"; } location /foo { echo foo; } location /bar/ { echo bar; } location /baz { echo baz; }
我們?cè)?location /test 中使用了 try_files 配置指令,并提供了三個(gè)參數(shù),/foo、/bar/ 和 /baz. 根據(jù)前面對(duì) try_files 指令的介紹,我們可以知道,它會(huì)在 try-files 階段依次檢查前兩個(gè)參數(shù) /foo 和 /bar/ 所對(duì)應(yīng)的文件系統(tǒng)對(duì)象是否存在。
不妨先來(lái)做一組實(shí)驗(yàn)。假設(shè)現(xiàn)在 /var/www/ 路徑下是空的,則第一個(gè)參數(shù) /foo 映射成的文件 /var/www/foo 是不存在的;同樣,對(duì)于第二個(gè)參數(shù) /bar/ 所映射成的目錄 /var/www/bar/ 也是不存在的。于是此時(shí) Nginx 會(huì)在 try-files 階段發(fā)起到最后一個(gè)參數(shù)所指定的 URI(即 /baz)的“內(nèi)部跳轉(zhuǎn)”。實(shí)際的請(qǐng)求結(jié)果證實(shí)了這一點(diǎn):
$ curl localhost:8080/test baz
接下來(lái)再做一組實(shí)驗(yàn):在 /var/www/ 下創(chuàng)建一個(gè)名為 foo 的文件,其內(nèi)容為 hello world(注意你需要有 /var/www/ 目錄下的寫(xiě)權(quán)限):
$ echo 'hello world' > /var/www/foo
然后再請(qǐng)求 /test 接口:
$ curl localhost:8080/test uri: /foo
這里發(fā)生了什么?我們來(lái)看, try_files 指令的第一個(gè)參數(shù) /foo 可以映射為文件 /var/www/foo,而 Nginx 在 try-files 階段發(fā)現(xiàn)此文件確實(shí)存在,于是立即把當(dāng)前請(qǐng)求的 URI 改寫(xiě)為這個(gè)參數(shù)的值,即 /foo,并且不再繼續(xù)檢查后面的參數(shù),而直接運(yùn)行后面的請(qǐng)求處理階段。
通過(guò)前面這幾組實(shí)驗(yàn)不難看到, try_files 指令本質(zhì)上只是有條件地改寫(xiě)當(dāng)前請(qǐng)求的 URI,而這里說(shuō)的“條件”其實(shí)就是文件系統(tǒng)上的對(duì)象是否存在。當(dāng)“條件”都不滿(mǎn)足時(shí),它就會(huì)無(wú)條件地發(fā)起一個(gè)指定的“內(nèi)部跳轉(zhuǎn)”。當(dāng)然,除了無(wú)條件地發(fā)起“內(nèi)部跳轉(zhuǎn)”之外, try_files 指令還支持直接返回指定狀態(tài)碼的 HTTP 錯(cuò)誤頁(yè),例如:
try_files /foo /bar/ =404;
這行配置是說(shuō),當(dāng) /foo 和 /bar/ 參數(shù)所對(duì)應(yīng)的文件系統(tǒng)對(duì)象都不存在時(shí),就直接返回 404 Not Found 錯(cuò)誤頁(yè)。注意這里它是如何使用等號(hào)字符前綴來(lái)標(biāo)識(shí) HTTP 狀態(tài)碼的。
content階段
該階段包含標(biāo)準(zhǔn)函數(shù)echo proxy_pass 以及openresty 函數(shù)content_by_lua balance_by_lua header_filter_by_lua body_filter_by_lua
log
所有請(qǐng)求的標(biāo)準(zhǔn)輸出都在改階段。幾乎所有的邏輯代碼也在改階段執(zhí)行。這個(gè)階段比較常見(jiàn)
log階段
改階段包含ngx的acces_log error_log以及openresty函數(shù)log_by_lua
該階段主要記錄日志
其它
satisfy指令
對(duì)于多個(gè) Nginx 模塊注冊(cè)在 access 階段的處理程序, satisfy 配置指令可以用于控制它們彼此之間的協(xié)作方式。比如模塊 A 和 B 都在 access 階段注冊(cè)了與訪(fǎng)問(wèn)控制相關(guān)的處理程序,那就有兩種協(xié)作方式,一是模塊 A 和模塊 B 都得通過(guò)驗(yàn)證才算通過(guò),二是模塊 A 和模塊 B 只要其中任一個(gè)通過(guò)驗(yàn)證就算通過(guò)。第一種協(xié)作方式稱(chēng)為 all 方式(或者說(shuō)“與關(guān)系”),第二種方式則被稱(chēng)為 any 方式(或者說(shuō)“或關(guān)系”)。默認(rèn)情況下,Nginx 使用的是 all 方式。
location /test { satisfy all; deny all; access_by_lua 'ngx.exit(ngx.OK)'; echo something important; }
如果我們把上例中的 satisfy all 語(yǔ)句更改為 satisfy any,
location /test { satisfy any; deny all; access_by_lua 'ngx.exit(ngx.OK)'; echo something important; }
結(jié)果則會(huì)完全不同:
$ curl localhost:8080/test something important
在 any 方式下,access 階段只要有一個(gè)模塊通過(guò)了驗(yàn)證,就會(huì)認(rèn)為請(qǐng)求整體通過(guò)了驗(yàn)證,而在上例中, ngx_lua 模塊的 access_by_lua 語(yǔ)句總是會(huì)通過(guò)驗(yàn)證的。
ngx_index 模塊, ngx_autoindex 模塊,以及 ngx_static 模塊
Nginx 一般會(huì)在 content 階段安排三個(gè)這樣的靜態(tài)資源服務(wù)模塊。按照它們?cè)?content 階段的運(yùn)行順序,依次是 ngx_index 模塊, ngx_autoindex 模塊,以及 ngx_static 模塊。
ngx_index 和 ngx_autoindex 模塊都只會(huì)作用于那些 URI 以 / 結(jié)尾的請(qǐng)求,例如請(qǐng)求 GET /cats/,而對(duì)于不以 / 結(jié)尾的請(qǐng)求則會(huì)直接忽略,同時(shí)把處理權(quán)移交給 content 階段的下一個(gè)模塊。而 ngx_static 模塊則剛好相反,直接忽略那些 URI 以 / 結(jié)尾的請(qǐng)求。
ngx_index 模塊主要用于在文件系統(tǒng)目錄中自動(dòng)查找指定的首頁(yè)文件,類(lèi)似 index.html 和 index.htm 這樣的,例如:
location / { root /var/www/; index index.htm index.html; }
為了進(jìn)一步確認(rèn) ngx_index 模塊在找到文件時(shí)的“內(nèi)部跳轉(zhuǎn)”行為,我們不妨設(shè)計(jì)下面這個(gè)小例子:
location / { root /var/www/; index index.html; } location /index.html { set $a 32; echo "a = $a"; }
此時(shí)我們?cè)诒緳C(jī)的 /var/www/ 目錄下創(chuàng)建一個(gè)空白的 index.html 文件,并確保該文件的權(quán)限設(shè)置對(duì)于運(yùn)行 Nginx worker 進(jìn)程的帳戶(hù)可讀
$ curl 'http://localhost:8080/' a = 32
如果此時(shí)把 /var/www/index.html 文件刪除,再訪(fǎng)問(wèn) / 又會(huì)發(fā)生什么事情呢?答案是返回 403 Forbidden 出錯(cuò)頁(yè)。為什么呢?因?yàn)?ngx_index 模塊找不到 index 指令指定的文件(在這里就是 index.html),接著把處理權(quán)轉(zhuǎn)給 content 階段的后續(xù)模塊,而后續(xù)的模塊也都無(wú)法處理這個(gè)請(qǐng)求,于是 Nginx 只好放棄,輸出了錯(cuò)誤頁(yè)
運(yùn)行在 ngx_index 模塊之后的 ngx_autoindex 模塊就可以用于自動(dòng)生成這樣的“目錄索引”網(wǎng)頁(yè)。我們來(lái)把上例修改一下:
location / { root /var/www/; index index.html; autoindex on; }
此時(shí)仍然保持文件系統(tǒng)中的 /var/www/index.html 文件不存在。我們?cè)僭L(fǎng)問(wèn) / 位置時(shí),就會(huì)得到一張漂亮的網(wǎng)頁(yè):
$ curl 'http://localhost:8080/'
ngx_static 模塊服務(wù)磁盤(pán)文件的例子。我們使用下面這個(gè)配置片段:
location / {
root /var/www/;
}
現(xiàn)在來(lái)通過(guò) HTTP 協(xié)議請(qǐng)求一下這兩個(gè)文件所對(duì)應(yīng)的 URI:
$ curl 'http://localhost:8080/index.html' this is my home $ curl 'http://localhost:8080/hello.html' hello world
location / 中沒(méi)有使用運(yùn)行在 content 階段的模塊指令,于是也就沒(méi)有模塊注冊(cè)這個(gè) location 的“內(nèi)容處理程序”,處理權(quán)便自動(dòng)落到了在 content 階段“墊底”的那 3 個(gè)靜態(tài)資源服務(wù)模塊。首先運(yùn)行的 ngx_index 和 ngx_autoindex 模塊先后看到當(dāng)前請(qǐng)求的 URI,/index.html 和 /hello.html,并不以 / 結(jié)尾,于是直接棄權(quán),將處理權(quán)轉(zhuǎn)給了最后運(yùn)行的 ngx_static 模塊。ngx_static 模塊根據(jù) root 指令指定的“文檔根目錄”(document root),分別將請(qǐng)求 URI /index.html 和 /hello.html 映射為文件系統(tǒng)路徑 /var/www/index.html 和 /var/www/hello.html,在確認(rèn)這兩個(gè)文件存在后,將它們的內(nèi)容分別作為響應(yīng)體輸出,并自動(dòng)設(shè)置 Content-Type、Content-Length 以及 Last-Modified 等響應(yīng)頭。
很多初學(xué)者會(huì)想當(dāng)然地把 404 錯(cuò)誤理解為某個(gè) location 不存在,其實(shí)上面這個(gè)例子表明,即使 location 存在并成功匹配,也是可能返回 404 錯(cuò)誤頁(yè)的。因?yàn)闆Q定著 404 錯(cuò)誤頁(yè)的是抽象的“資源”是否存在,而非某個(gè)具體的 location 是否存在。
location /auth {
access_by_lua ‘
‘;
}
顯然,這個(gè) /auth 接口只定義了 access 階段的配置指令,即 access_by_lua,并未定義任何 content 階段的配置指令。于是當(dāng)我們請(qǐng)求 /auth 接口時(shí),在 access 階段的 Lua 代碼會(huì)如期執(zhí)行,然后 content 階段的那些靜態(tài)文件服務(wù)會(huì)緊接著自動(dòng)發(fā)生作用,直至 ngx_static 模塊去文件系統(tǒng)上找名為 auth 的文件。而經(jīng)常地,404 錯(cuò)誤頁(yè)會(huì)拋出,除非運(yùn)氣太好,在對(duì)應(yīng)路徑上確實(shí)存在一個(gè)叫做 auth 的文件。所以,一條經(jīng)驗(yàn)是,當(dāng)遇到意外的 404 錯(cuò)誤并且又不涉及靜態(tài)文件服務(wù)時(shí),應(yīng)當(dāng)首先檢查是否在對(duì)應(yīng)的 location 配置塊中恰當(dāng)?shù)嘏渲昧?content 階段的模塊指令,例如 content_by_lua、 echo 以及 proxy_pass 之類(lèi)。
openresty請(qǐng)求處理順序
set_by_lua: 流程分支處理判斷變量初始化
rewrite_by_lua: 轉(zhuǎn)發(fā)、重定向、緩存等功能(例如特定請(qǐng)求代理到外網(wǎng))
access_by_lua: IP 準(zhǔn)入、接口權(quán)限等情況集中處理(例如配合 iptable 完成簡(jiǎn)單防火墻)
content_by_lua: 內(nèi)容生成
header_filter_by_lua: 響應(yīng)頭部過(guò)濾處理(例如添加頭部信息)
body_filter_by_lua: 響應(yīng)體過(guò)濾處理(例如完成應(yīng)答內(nèi)容統(tǒng)一成大寫(xiě)) log_by_lua*:會(huì)話(huà)完成后本地異步完成日志記錄(日志可以記錄在本地,還可以同步到其他機(jī)器)