Nginx 介紹
Nginx (engine x) 是一個高性能的HTTP和反向代理服務(wù)器,也是一個IMAP/POP3/SMTP服務(wù)器。
Nginx是一款輕量級的Web 服務(wù)器/反向代理服務(wù)器及電子郵件(IMAP/POP3)代理服務(wù)器,并在一個BSD-like 協(xié)議下發(fā)行。其特點是占有內(nèi)存少,并發(fā)能力強
OpenResty介紹
OpenResty 是一個基于 Nginx 與 Lua 的高性能 Web 平臺,其內(nèi)部集成了大量精良的 Lua 庫、第三方模塊以及大多數(shù)的依賴項。用于方便地搭建能夠處理超高并發(fā)、擴展性極高的動態(tài) Web 應(yīng)用、Web 服務(wù)和動態(tài)網(wǎng)關(guān)
執(zhí)行階段前言
location /test { set $a 32; echo $a; set $a 56; echo $a; }
兩次都會輸出56,因為set階段始終在content階段之前執(zhí)行,跟代碼的先后順序無關(guān)。
Nginx執(zhí)行階段
Nginx 處理請求的過程一共劃分為 11 個階段,按照執(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 讀取并解析完請求頭(request headers)之后就立即開始運行。標(biāo)準(zhǔn)模塊 ngx_realip 就在 post-read 階段注冊了處理程序,它的功能是迫使 Nginx 認(rèn)為當(dāng)前請求的來源地址是指定的某一個請求頭的值。下面這個例子就使用了 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 把那些來自 127.0.0.1 的所有請求的來源地址,都改寫為請求頭 X-My-IP 所指定的值。同時該例使用了標(biāo)準(zhǔn)內(nèi)建變量 $remote_addr 來輸出當(dāng)前請求的來源地址,以確認(rèn)是否被成功改寫。
$ 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 模塊的配置指令直接書寫在 server 配置塊中時,基本上都是運行在 server-rewrite 階段。
server { listen 8080; 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
find-config 階段
這個階段并不支持 Nginx 模塊注冊處理程序,而是由 Nginx 核心來完成當(dāng)前請求與 location 配置塊之間的配對工作。
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 模塊注冊處理程序,而是由 Nginx 核心完成 rewrite 階段所要求的“內(nèi)部跳轉(zhuǎn)”操作
“內(nèi)部跳轉(zhuǎn)”的工作原理:本質(zhì)上其實就是把當(dāng)前的請求處理階段強行倒退到 find-config 階段,以便重新進行請求 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í)行。
為什么不直接在 rewrite 指令執(zhí)行時立即進行跳轉(zhuǎn)呢?
為了在最初匹配的 location 塊中支持多次反復(fù)地改寫 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 配置指令對請求 URI 進行改寫,則不會涉及“內(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 其實也在這個階段注冊了處理程序
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 配置塊中。前面我們介紹過,Nginx 匹配 location 的動作發(fā)生在 find-config 階段,而 find-config 階段遠(yuǎn)遠(yuǎn)晚于 post-read 階段執(zhí)行,所以在 post-read 階段,當(dāng)前請求還沒有和任何 location 相關(guān)聯(lián)。
建議是:盡量在 server 配置塊中配置 ngx_realip 這樣的模塊
post-access階段
該階段不支持 Nginx 模塊注冊處理程序,而是由 Nginx 核心自己完成一些處理工作
try-files 階段
實現(xiàn)標(biāo)準(zhǔn)配置指令 try_files 的功能,并不支持 Nginx 模塊注冊處理程序。
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.
location /test { try_files /foo /bar/ /baz; echo "uri: $uri"; } location /foo { echo foo; } location /bar/ { echo bar; } location /baz { echo baz; }
我們在 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
接下來再做一組實驗:在 /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 指令本質(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)碼的。
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
所有請求的標(biāo)準(zhǔn)輸出都在改階段。幾乎所有的邏輯代碼也在改階段執(zhí)行。這個階段比較常見
log階段
改階段包含ngx的acces_log error_log以及openresty函數(shù)log_by_lua
該階段主要記錄日志
其它
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; deny all; access_by_lua 'ngx.exit(ngx.OK)'; echo something important; }
如果我們把上例中的 satisfy all 語句更改為 satisfy any,
location /test { satisfy any; deny all; 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 語句總是會通過驗證的。
ngx_index 模塊, ngx_autoindex 模塊,以及 ngx_static 模塊
Nginx 一般會在 content 階段安排三個這樣的靜態(tài)資源服務(wù)模塊。按照它們在 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/; index index.htm index.html; }
為了進一步確認(rèn) ngx_index 模塊在找到文件時的“內(nèi)部跳轉(zhuǎn)”行為,我們不妨設(shè)計下面這個小例子:
location / { root /var/www/; index index.html; } location /index.html { set $a 32; echo "a = $a"; }
此時我們在本機的 /var/www/ 目錄下創(chuàng)建一個空白的 index.html 文件,并確保該文件的權(quán)限設(shè)置對于運行 Nginx worker 進程的帳戶可讀
$ curl 'http://localhost:8080/' a = 32
如果此時把 /var/www/index.html 文件刪除,再訪問 / 又會發(fā)生什么事情呢?答案是返回 403 Forbidden 出錯頁。為什么呢?因為 ngx_index 模塊找不到 index 指令指定的文件(在這里就是 index.html),接著把處理權(quán)轉(zhuǎn)給 content 階段的后續(xù)模塊,而后續(xù)的模塊也都無法處理這個請求,于是 Nginx 只好放棄,輸出了錯誤頁
運行在 ngx_index 模塊之后的 ngx_autoindex 模塊就可以用于自動生成這樣的“目錄索引”網(wǎng)頁。我們來把上例修改一下:
location / { root /var/www/; index index.html; autoindex on; }
此時仍然保持文件系統(tǒng)中的 /var/www/index.html 文件不存在。我們再訪問 / 位置時,就會得到一張漂亮的網(wǎng)頁:
$ curl 'http://localhost:8080/'
ngx_static 模塊服務(wù)磁盤文件的例子。我們使用下面這個配置片段:
location / {
root /var/www/;
}
現(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
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)頭。
很多初學(xué)者會想當(dāng)然地把 404 錯誤理解為某個 location 不存在,其實上面這個例子表明,即使 location 存在并成功匹配,也是可能返回 404 錯誤頁的。因為決定著 404 錯誤頁的是抽象的“資源”是否存在,而非某個具體的 location 是否存在。
location /auth {
access_by_lua ‘
‘;
}
顯然,這個 /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ù)嘏渲昧?content 階段的模塊指令,例如 content_by_lua、 echo 以及 proxy_pass 之類。
openresty請求處理順序
set_by_lua: 流程分支處理判斷變量初始化
rewrite_by_lua: 轉(zhuǎn)發(fā)、重定向、緩存等功能(例如特定請求代理到外網(wǎng))
access_by_lua: IP 準(zhǔn)入、接口權(quán)限等情況集中處理(例如配合 iptable 完成簡單防火墻)
content_by_lua: 內(nèi)容生成
header_filter_by_lua: 響應(yīng)頭部過濾處理(例如添加頭部信息)
body_filter_by_lua: 響應(yīng)體過濾處理(例如完成應(yīng)答內(nèi)容統(tǒng)一成大寫) log_by_lua*:會話完成后本地異步完成日志記錄(日志可以記錄在本地,還可以同步到其他機器)