Nginx 處理一個 HTTP 請求的全過程
前面給大家講了 Nginx 是如何處理 HTTP請求頭部的纵菌,接下來就到了真正處理 HTTP 請求的階段了。先看下面這張圖贫母,這張圖是 Nginx 處理 HTTP 請求的示意圖洞就,雖然簡單卵皂,但是卻很好的說明了整個過程悼粮。
- Read Request Headers:解析請求頭闲勺。
- Identify Configuration Block:識別由哪一個 location 進(jìn)行處理,匹配 URL扣猫。
- Apply Rate Limits:判斷是否限速菜循。例如可能這個請求并發(fā)的連接數(shù)太多超過了限制,或者 QPS 太高申尤。
- Perform Authentication:連接控制癌幕,驗(yàn)證請求。例如可能根據(jù) Referrer 頭部做一些防盜鏈的設(shè)置昧穿,或者驗(yàn)證用戶的權(quán)限勺远。
- Generate Content:生成返回給用戶的響應(yīng)。為了生成這個響應(yīng)粤咪,做反向代理的時候可能會和上游服務(wù)(Upstream Services)進(jìn)行通信谚中,然后這個過程中還可能會有些子請求或者重定向,那么還會走一下這個過程(Internal redirects and subrequests)寥枝。
- Response Filters:過濾返回給用戶的響應(yīng)宪塔。比如壓縮響應(yīng),或者對圖片進(jìn)行處理囊拜。
- Log:記錄日志某筐。
以上這七個步驟從整體上介紹了一下處理流程,下面還會再說一下實(shí)際的處理過程冠跷。
Nginx 處理 HTTP 請求的 11 個階段
下面介紹一下詳細(xì)的 11 個階段南誊,每個階段都可能對應(yīng)著一個甚至多個 HTTP 模塊,通過這樣一個模塊對比蜜托,我們也能夠很好的理解這些模塊具體是怎么樣發(fā)揮作用的抄囚。
- POST_READ:在 read 完請求的頭部之后,在沒有對頭部做任何處理之前橄务,想要獲取到一些原始的值幔托,就應(yīng)該在這個階段進(jìn)行處理。這里面會涉及到一個 realip 模塊蜂挪。
- SERVER_REWRITE:和下面的 REWRITE 階段一樣重挑,都只有一個模塊叫 rewrite 模塊,一般沒有第三方模塊會處理這個階段棠涮。
- FIND_CONFIG:做 location 的匹配谬哀,暫時沒有模塊會用到。
- REWRITE:對 URL 做一些處理严肪。
- POST_WRITE:處于 REWRITE 之后史煎,也是暫時沒有模塊會在這個階段出現(xiàn)谦屑。
接下來是確認(rèn)用戶訪問權(quán)限的三個模塊:
- PREACCESS:是在 ACCESS 之前要做一些工作,例如并發(fā)連接和 QPS 需要進(jìn)行限制劲室,涉及到兩個模塊:limt_conn 和 limit_req
- ACCESS:核心要解決的是用戶能不能訪問的問題伦仍,例如 auth_basic 是用戶名和密碼,access 是用戶訪問 IP很洋,auth_request 根據(jù)第三方服務(wù)返回是否可以去訪問充蓝。
- POST_ACCESS:是在 ACCESS 之后會做一些事情,同樣暫時沒有模塊會用到喉磁。
最后的三個階段處理響應(yīng)和日志:
PRECONTENT:在處理 CONTENT 之前會做一些事情谓苟,例如會把子請求發(fā)送給第三方的服務(wù)去處理,try_files 模塊也是在這個階段中协怒。
CONTENT:這個階段涉及到的模塊就非常多了涝焙,例如 index, autoindex, concat 等都是在這個階段生效的。
LOG:記錄日志 access_log 模塊孕暇。
以上的這些階段都是嚴(yán)格按照順序進(jìn)行處理的仑撞,當(dāng)然,每個階段中各個 HTTP 模塊的處理順序也很重要妖滔,如果某個模塊不把請求向下傳遞隧哮,后面的模塊是接收不到請求的。而且每個階段中的模塊也不一定所有都要執(zhí)行一遍座舍,下面就接著講一下各個階段模塊之間的請求順序沮翔。
11 個階段的順序處理
如下圖所示,每一個模塊處理之間是有序的曲秉,那么這個順序怎么才能得到呢采蚀?其實(shí)非常簡單,在源碼 ngx_module.c 中承二,有一個數(shù)組 ngx_module_name
榆鼠,其中包含了在編譯 Nginx 的時候的 with 指令所包含的所有模塊,它們之間的順序非常關(guān)鍵亥鸠,在數(shù)組中順序是相反的璧眠。
char *ngx_module_names[] = {
… …
"ngx_http_static_module",
"ngx_http_autoindex_module",
"ngx_http_index_module",
"ngx_http_random_index_module",
"ngx_http_mirror_module",
"ngx_http_try_files_module",
"ngx_http_auth_request_module",
"ngx_http_auth_basic_module",
"ngx_http_access_module",
"ngx_http_limit_conn_module",
"ngx_http_limit_req_module",
"ngx_http_realip_module",
"ngx_http_referer_module",
"ngx_http_rewrite_module",
"ngx_http_concat_module",
… …
}
灰色部分的模塊是 Nginx 的框架部分去執(zhí)行處理的,第三方模塊沒有機(jī)會在這里得到處理读虏。
在依次向下執(zhí)行的過程中,也可能不按照這樣的順序袁滥。例如盖桥,在 access 階段中,有一個指令叫 satisfy题翻,它可以指示當(dāng)有一個滿足的時候就直接跳到下一個階段進(jìn)行處理揩徊,例如當(dāng) access 滿足了腰鬼,就直接跳到 try_files 模塊進(jìn)行處理,而不會再執(zhí)行 auth_basic塑荒、auth_request 模塊熄赡。
在 content 階段中,當(dāng) index 模塊執(zhí)行了齿税,就不會再執(zhí)行 auto_index 模塊彼硫,而是直接跳到 log 模塊。
整個 11 個階段所涉及到的模塊和先后順序如下圖所示:
下面開始詳細(xì)講解一下各個階段凌箕。先來看下第一個階段 postread 階段拧篮,顧名思義,postread 階段是在正式處理請求之前起作用的牵舱。
postread 階段
postread 階段串绩,是 11 個階段的第 1 個階段,這個階段剛剛獲取到了請求的頭部芜壁,還沒有進(jìn)行任何處理礁凡,我們可以拿到一些原始的信息。例如慧妄,拿到用戶的真實(shí) IP 地址
問題:如何拿到用戶的真實(shí) IP 地址顷牌?
我們知道,TCP 連接是由一個四元組構(gòu)成的腰涧,在四元組中韧掩,包含了源 IP 地址。而在真實(shí)的互聯(lián)網(wǎng)中窖铡,存在非常多的正向代理和反向代理疗锐。例如最終的用戶有自己的內(nèi)網(wǎng) IP 地址,運(yùn)營商會分配一個公網(wǎng) IP费彼,然后訪問某個網(wǎng)站的時候滑臊,這個網(wǎng)站可能使用了 CDN 加速一些靜態(tài)文件或圖片,如果 CDN 沒有命中箍铲,那么就會回源雇卷,回源的時候可能還要經(jīng)過一個反向代理,例如阿里云的 SLB颠猴,然后才會到達(dá) Nginx关划。
我們要拿到的地址應(yīng)該是運(yùn)營商給用戶分配的公網(wǎng) IP 地址 115.204.33.1,對這個 IP 來進(jìn)行并發(fā)連接的控制或者限速翘瓮,而 Nginx 拿到的卻是 2.2.2.2贮折,那么怎么才能拿到真實(shí)的用戶 IP 呢?
HTTP 協(xié)議中资盅,有兩個頭部可以用來獲取用戶 IP:
- X-Forwardex-For 是用來傳遞 IP 的调榄,這個頭部會把經(jīng)過的節(jié)點(diǎn) IP 都記錄下來
- X-Real-IP:可以記錄用戶真實(shí)的 IP 地址踊赠,只能有一個
拿到真實(shí)用戶 IP 后如何使用?
針對這個問題每庆,Nginx 是基于變量來使用筐带。
例如 binary_remote_addr、remote_addr 這樣的變量缤灵,其值就是真實(shí)的 IP伦籍,這樣做連接限制也就是 limit_conn 模塊才有意義,這也說明了凤价,limit_conn 模塊只能在 preaccess 階段鸽斟,而不能在 postread 階段生效。
realip 模塊
-
默認(rèn)不會編譯進(jìn) Nginx
- 需要通過
--with-http_realip_module
啟用功能
- 需要通過
-
變量:如果還想要使用原來的 TCP 連接中的地址和端口利诺,需要通過這兩個變量保存
realip_remote_addr
realip_remote_port
-
功能
- 修改客戶端地址
-
指令
-
set_real_ip_from
指定可信的地址富蓄,只有從該地址建立的連接,獲取的 realip 才是可信的
-
real_ip_header
指定從哪個頭部取真實(shí)的 IP 地址慢逾,默認(rèn)從
X-Real-IP
中取立倍,如果設(shè)置從X-Forwarded-For
中取,會先從最后一個 IP 開始取 -
real_ip_recursive
環(huán)回地址口注,默認(rèn)關(guān)閉寝志,打開的時候材部,如果
X-Forwarded-For
最后一個地址與客戶端地址相同乐导,會過濾掉該地址
-
Syntax: set_real_ip_from address | CIDR | unix:;
Default: —
Context: http, server, location
Syntax: real_ip_header field | X-Real-IP | X-Forwarded-For | proxy_protocol;
Default: real_ip_header X-Real-IP;
Context: http, server, location
Syntax: real_ip_recursive on | off;
Default: real_ip_recursive off;
Context: http, server, location
實(shí)戰(zhàn)
上面關(guān)于 real_ip_recursive
指令可能不太容易理解物臂,我們來實(shí)戰(zhàn)練習(xí)一下棵磷,先來看 real_ip_recursive
默認(rèn)關(guān)閉的情況:
- 重新編譯一個帶有 realip 模塊的 nginx
關(guān)于如何編譯 Nginx,詳見:https://iziyang.github.io/2020/03/10/1-nginx/
# 下載 nginx 源碼晋涣,在源碼目錄下執(zhí)行
./configure --prefix=自己指定的目錄 --with-http_realip_module
make
make install
- 然后去上一步中自己指定的 Nginx 安裝目錄
#屏蔽默認(rèn)的 nginx.conf 文件的 server 塊內(nèi)容仪媒,并添加一行
include /Users/mtdp/myproject/nginx/test_nginx/conf/example/*.conf;
# 在 example 目錄下建立 realip.conf,set_real_ip_from 可以設(shè)置為自己的本機(jī) IP
server {
listen 80;
server_name ziyang.realip.com;
error_log /Users/mtdp/myproject/nginx/nginx/logs/myerror.log debug;
set_real_ip_from 192.168.0.108;
#real_ip_header X-Real-IP;
real_ip_recursive off;
# real_ip_recursive on;
real_ip_header X-Forwarded-For;
location / {
return 200 "Client real ip: $remote_addr\n";
}
}
在上面的配置文件中姻僧,我設(shè)置了可信代理地址為本機(jī)地址规丽,real_ip_recursive
為默認(rèn)的 off,real_ip_header
設(shè)為從 X-Forwarded-For
中取撇贺。
- 重載配置文件
./sbin/nginx -s reload
- 測試響應(yīng)結(jié)果
? test_nginx curl -H 'X-Forwarded-For: 1.1.1.1,192.168.0.108' ziyang.realip.com
Client real ip: 192.168.0.108
然后再來測試 real_ip_recursive
打開的情況:
- 配置文件中打開
real_ip_recursive
server {
listen 80;
server_name ziyang.realip.com;
error_log /Users/mtdp/myproject/nginx/nginx/logs/myerror.log debug;
set_real_ip_from 192.168.0.108;
#real_ip_header X-Real-IP;
#real_ip_recursive off;
real_ip_recursive on;
real_ip_header X-Forwarded-For;
location / {
return 200 "Client real ip: $remote_addr\n";
}
}
- 測試響應(yīng)結(jié)果
? test_nginx curl -H 'X-Forwarded-For: 1.1.1.1,2.2.2.2,192.168.0.108' ziyang.realip.com
Client real ip: 2.2.2.2
所以這里面也可看出來赌莺,如果使用 X-Forwarded-For
獲取 realip 的話,需要打開 real_ip_recursive
松嘶,并且艘狭,realip 依賴于 set_real_ip_from
設(shè)置的可信地址。
那么有人可能就會問了翠订,那直接用 X-Real-IP
來選取真實(shí)的 IP 地址不就好了巢音。這是可以的,但是 X-Real-IP
是 Nginx 獨(dú)有的,不是 RFC 規(guī)范傲绣,如果客戶端與服務(wù)器之間還有其他非 Nginx 軟件實(shí)現(xiàn)的代理塞琼,就會造成取不到 X-Real-IP
頭部,所以這個要根據(jù)實(shí)際情況來定。
rewrite 階段的 rewrite 模塊
下面來看一下 rewrite 模塊。
首先 rewrite 階段分為兩個残拐,一個是 server_rewrite 階段错沃,一個是 rewrite玉掸,這兩個階段都涉及到一個 rewrite 模塊啊易,而在 rewrite 模塊中续捂,有一個 return 指令,遇到該指令就不會再向下執(zhí)行,直接返回響應(yīng)酒繁。
return 指令
return 指令的語法如下:
- 返回狀態(tài)碼郎哭,后面跟上 body
- 返回狀態(tài)碼依鸥,后面跟上 URL
- 直接返回 URL
Syntax: return code [text];
return code URL;
return URL;
Default: —
Context: server, location, if
返回狀態(tài)碼包括以下幾種:
- Nginx 自定義
- 444:立刻關(guān)閉連接絮供,用戶收不到響應(yīng)
- HTTP 1.0 標(biāo)準(zhǔn)
- 301:永久重定向
- 302:臨時重定向睡榆,禁止被緩存
- HTTP 1.1 標(biāo)準(zhǔn)
- 303:臨時重定向宿崭,允許改變方法,禁止被緩存
- 307:臨時重定向,不允許改變方法洲守,禁止被緩存
- 308:永久重定向叙谨,不允許改變方法
return 指令與 error_page
error_page
的作用大家肯定經(jīng)常見到配椭。當(dāng)訪問一個網(wǎng)站出現(xiàn) 404 的時候,一般不會直接出現(xiàn)一個 404 NOT FOUND歧杏,而是會有一個比較友好的頁面兑凿,這就是 error_page
的功能咐鹤。
Syntax: error_page code ... [=[response]] uri;
Default: —
Context: http, server, location, if in location
我們來看幾個例子:
1. error_page 404 /404.html;
2. error_page 500 502 503 504 /50x.html;
3. error_page 404 =200 /empty.gif;
4. error_page 404 = /404.php;
5. location / {
error_page 404 = @fallback;
}
location @fallback {
proxy_pass http://backend;
}
6. error_page 403 http://example.com/forbidden.html;
7. error_page 404 =301 http://example.com/notfound.html;
那么現(xiàn)在就會有兩個問題捧请,大家看下下面這個配置文件:
server {
server_name ziyang.return.com;
listen 80;
root html/;
error_page 404 /403.html;
#return 405;
location / {
#return 404 "find nothing!";
}
}
- 當(dāng) server 下包含 error_page 且 location 下有 return 指令的時候氧吐,會執(zhí)行哪一個呢陨舱?
- return 指令同時出現(xiàn)在 server 塊下和同時出現(xiàn)在 location 塊下,它們有合并關(guān)系嗎莺奔?
這兩個問題我們通過實(shí)戰(zhàn)驗(yàn)證一下。
實(shí)戰(zhàn)
- 將上面的配置添加到配置文件 return.conf
- 在本機(jī)的 hosts 文件中綁定 ziyang.return.com 為本地的 IP 地址
- 訪問一個不存在的頁面
? test_nginx curl ziyang.return.com/text
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.17.8</center>
</body>
</html>
這個時候可以看到,是 error_page
生效了,返回的響應(yīng)是 403乐严。
那么假如打開了 location
下 return
指令的注釋呢?
- 打開
return
指令注釋,reload 配置文件 - 重新訪問頁面
? test_nginx curl ziyang.return.com/text
find nothing!%
這時候酌予,return
指令得到了執(zhí)行。也就是第一個問題奖慌,當(dāng) server
下包含 error_page
且 location
下有 return
指令的時候抛虫,會執(zhí)行 return
指令。
下面再看一下 server
下的 return
指令和 location
下的 return
指令會執(zhí)行哪一個简僧。
- 打開
server
下return
指令的注釋啦逆,reload 配置文件 - 重新訪問頁面
? test_nginx curl ziyang.return.com/text
<html>
<head><title>405 Not Allowed</title></head>
<body>
<center><h1>405 Not Allowed</h1></center>
<hr><center>nginx/1.17.8</center>
</body>
</html>
針對上面兩個問題也就有了答案:
-
當(dāng)
server
下包含error_page
且location
下有return
指令的時候,會執(zhí)行哪一個呢?會執(zhí)行
location
下的return
指令。 -
return
指令同時出現(xiàn)在server
塊下和同時出現(xiàn)在location
塊下奶陈,它們有合并關(guān)系嗎遏匆?沒有合并關(guān)系,先遇到哪個
return
指令就先執(zhí)行哪一個芜飘。
rewrite 指令
rewrite
指令用于修改用戶傳入 Nginx 的 URL壹罚。來看下 rewrite
的指令規(guī)則:
Syntax: rewrite regex replacement [flag];
Default: —
Context: server, location, if
它的功能主要有下面幾點(diǎn):
- 將
regex
指定的 URL 替換成replacement
這個新的 URL- 可以使用正則表達(dá)式及變量提取
- 當(dāng)
replacement
以 http:// 或者 https:// 或者 $schema 開頭,則直接返回 302 重定向 - 替換后的 URL 根據(jù) flag 指定的方式進(jìn)行處理
- last:用
replacement
這個 URL 進(jìn)行新的 location 匹配 - break:break 指令停止當(dāng)前腳本指令的執(zhí)行银亲,等價于獨(dú)立的 break 指令
- redirect:返回 302 重定向
- permanent:返回 301 重定向
- last:用
指令示例
現(xiàn)在我們有這樣的一個目錄結(jié)構(gòu):
html/first/
└── 1.txt
html/second/
└── 2.txt
html/third/
└── 3.txt
配置文件如下所示:
server {
listen 80;
server_name rewrite.ziyang.com;
rewrite_log on;
error_log logs/rewrite_error.log notice;
root html/;
location /first {
rewrite /first(.*) /second$1 last;
return 200 'first!\n';
}
location /second {
rewrite /second(.*) /third$1;
return 200 'second!\n';
}
location /third {
return 200 'third!\n';
}
location /redirect1 {
rewrite /redirect1(.*) $1 permanent;
}
location /redirect2 {
rewrite /redirect2(.*) $1 redirect;
}
location /redirect3 {
rewrite /redirect3(.*) http://rewrite.ziyang.com$1;
}
location /redirect4 {
rewrite /redirect4(.*) http://rewrite.ziyang.com$1 permanent;
}
}
那么我們的問題是:
- return 指令 與 rewrite 指令的順序關(guān)系陪白?
- 訪問 /first/3.txt粹排,/second/3.txt弄抬,/third/3.txt 分別返回的是什么?
- 如果不攜帶 flag 會怎么樣?
帶著這三個問題青瀑,我們來實(shí)際演示一下璧亮。
實(shí)戰(zhàn)
準(zhǔn)備工作
- 將上面的配置添加到配置文件 rewrite.conf
- 在本機(jī)的 hosts 文件中綁定 rewrite.ziyang.com 為 127.0.0.1
last flag
首先訪問 rewrite.ziyang.com/first/3.txt,結(jié)果如下:
? ~ curl rewrite.ziyang.com/first/3.txt
second!
為什么結(jié)果是 second! 呢斥难?應(yīng)該是 third! 呀枝嘶,可能有人會有這樣的疑問。實(shí)際的匹配步驟如下:
- curl rewrite.ziyang.com/first/3.txt
- 由于
rewrite /first(.*) /second$1 last;
這條指令的存在哑诊,last 表示使用新的 URL 進(jìn)行 location 匹配群扶,因此接下來會去匹配 second/3.txt - 匹配到 /second 塊之后,會依次執(zhí)行指令镀裤,最后返回 200
- 注意竞阐,location 塊中雖然也改寫了 URL,但是并不會去繼續(xù)匹配暑劝,因?yàn)楹竺鏇]有指定 flag骆莹。
break flag
下面將 rewrite /second(.*) /third$1;
這條指令加上 break flag,rewrite /second(.*) /third$1 break;
繼續(xù)訪問 rewrite.ziyang.com/first/3.txt担猛,結(jié)果如下:
? ~ curl rewrite.ziyang.com/first/3.txt
test3%
這時候返回的是 3.txt 文件的內(nèi)容 test3幕垦。實(shí)際的匹配步驟如下:
- curl rewrite.ziyang.com/first/3.txt
- 由于
rewrite /first(.*) /second$1 last;
這條指令的存在,last 表示使用新的 URL 進(jìn)行 location 匹配傅联,因此接下來會去匹配 second/3.txt - 匹配到 /second 塊之后先改,由于 break flag 的存在,會繼續(xù)匹配 rewrite 過后的 URL
- 匹配 /third location
因此蒸走,這個過程實(shí)際請求的 URL 是 rewrite.ziyang.com/third/3.txt仇奶,這樣自然結(jié)果就是 test3 了。你還可以試試訪問 rewrite.ziyang.com/third/2.txt 看看會返回什么比驻。
redirect 和 permanent flag
配置文件中還有 4 個 location该溯,你可以分別試著訪問一下岛抄,結(jié)果是這樣的:
- redirect1:返回 301
- redirect2:返回 302
- redirect3:返回 302
- redirect4:返回 301
rewrite 行為記錄日志
主要是一個指令 rewrite_log
:
Syntax: rewrite_log on | off;
Default: rewrite_log off;
Context: http, server, location, if
這個指令打開之后,會把 rewrite 的日志寫入 logs/rewrite_error.log 日志文件中狈茉,這是請求 /first/3.txt 的日志記錄:
2020/05/06 06:24:05 [notice] 86959#0: *25 "/first(.*)" matches "/first/3.txt", client: 127.0.0.1, server: rewrite.ziyang.com, request: "GET /first/3.txt HTTP/1.1", host: "rewrite.ziyang.com"
2020/05/06 06:24:05 [notice] 86959#0: *25 rewritten data: "/second/3.txt", args: "", client: 127.0.0.1, server: rewrite.ziyang.com, request: "GET /first/3.txt HTTP/1.1", host: "rewrite.ziyang.com"
2020/05/06 06:24:05 [notice] 86959#0: *25 "/second(.*)" matches "/second/3.txt", client: 127.0.0.1, server: rewrite.ziyang.com, request: "GET /first/3.txt HTTP/1.1", host: "rewrite.ziyang.com"
2020/05/06 06:24:05 [notice] 86959#0: *25 rewritten data: "/third/3.txt", args: "", client: 127.0.0.1, server: rewrite.ziyang.com, request: "GET /first/3.txt HTTP/1.1", host: "rewrite.ziyang.com"
if 指令
if 指令也是在 rewrite 階段生效的弦撩,它的語法如下所示:
Syntax: if (condition) { ... }
Default: —
Context: server, location
它的規(guī)則是:
- 條件 condition 為真,則執(zhí)行大括號內(nèi)的指令论皆;同時還遵循值指令的繼承規(guī)則(詳見我之前的文章 Nginx 的配置指令)
那么 if 指令的條件表達(dá)式包含哪些內(nèi)容呢益楼?它的規(guī)則如下:
- 檢查變量為空或者值是否為 0
- 將變量與字符串做匹配,使用 = 或 !=
- 將變量與正則表達(dá)式做匹配
- 大小寫敏感点晴,~ 或者 !~
- 大小寫不敏感感凤,~* 或者 !~*
- 檢查文件是否存在,使用 -f 或者 !-f
- 檢查目錄是否存在粒督,使用 -d 或者 !-d
- 檢查文件陪竿、目錄、軟鏈接是否存在屠橄,使用 -e 或者 !-e
- 檢查是否為可執(zhí)行文件族跛,使用 -x 或者 !-x
下面是一些例子:
if ($http_user_agent ~ MSIE) { # 與變量 http_user_agent 匹配
rewrite ^(.*)$ /msie/$1 break;
}
if ($http_cookie ~* "id=([^;]+)(?:;|$)") { # 與變量 http_cookie 匹配
set $id $1;
}
if ($request_method = POST) { # 與變量 request_method 匹配,獲取請求方法
return 405;
}
if ($slow) { # slow 變量在 map 模塊中自定義锐墙,也可以進(jìn)行匹配
limit_rate 10k;
}
if ($invalid_referer) {
return 403;
}
find_config 階段
當(dāng)經(jīng)過 rewrite 模塊礁哄,匹配到 URL 之后,就會進(jìn)入 find_config 階段溪北,開始尋找 URL 對應(yīng)的 location 配置桐绒。
location 指令
指令語法
還是老規(guī)矩,咱們先來看一下 location 指令的語法:
Syntax: location [ = | ~ | ~* | ^~ ] uri { ... }
location @name { ... }
Default: —
Context: server, location
Syntax: merge_slashes on | off;
Default: merge_slashes on;
Context: http, server
這里面有一個 merge_slashes
指令之拨,這個指令的作用是茉继,加入 URL 中有兩個重復(fù)的 /,那么會合并為一個蚀乔,這個指令默認(rèn)是打開的烁竭,只有當(dāng)對 URL 進(jìn)行 base64 之類的編碼時才需要關(guān)閉。
匹配規(guī)則
location 的匹配規(guī)則是僅匹配 URI吉挣,忽略參數(shù)派撕,有下面三種大的情況:
- 前綴字符串
- 常規(guī)匹配
- =:精確匹配
- ^~:匹配上后則不再進(jìn)行正則表達(dá)式匹配
- 正則表達(dá)式
- ~:大小寫敏感的正則匹配
- ~*:大小寫不敏感
- 用戶內(nèi)部跳轉(zhuǎn)的命名 location
- @
對于這些規(guī)則剛看上去肯定是很懵的,完全不知道在說什么听想,下面來實(shí)戰(zhàn)看幾個例子腥刹。
實(shí)戰(zhàn)
先看一下 Nginx 的配置文件:
server {
listen 80;
server_name location.ziyang.com;
error_log logs/error.log debug;
#root html/;
default_type text/plain;
merge_slashes off;
location ~ /Test1/$ {
return 200 'first regular expressions match!\n';
}
location ~* /Test1/(\w+)$ {
return 200 'longest regular expressions match!\n';
}
location ^~ /Test1/ {
return 200 'stop regular expressions match!\n';
}
location /Test1/Test2 {
return 200 'longest prefix string match!\n';
}
location /Test1 {
return 200 'prefix string match!\n';
}
location = /Test1 {
return 200 'exact match!\n';
}
}
問題就來了马胧,訪問下面幾個 URL 會分別返回什么內(nèi)容呢汉买?
/Test1
/Test1/
/Test1/Test2
/Test1/Test2/
/test1/Test2
例如訪問 /Test1 時,會有幾個部分都匹配上:
- 常規(guī)前綴匹配:location /Test1
- 精確匹配:location = /Test1
訪問 /Test1/ 時佩脊,也會有幾個部分匹配上:
- location ~ /Test1/$
- location ^~ /Test1/
那么究竟會匹配哪一個呢蛙粘?Nginx 其實(shí)是遵循一套規(guī)則的垫卤,如下圖所示:
全部的前綴字符串是放置在一棵二叉樹中的,Nginx 會分為兩部分進(jìn)行匹配:
- 先遍歷所有的前綴字符串出牧,選取最長的一個前綴字符串穴肘,如果這個字符串是 = 的精確匹配或 ^~ 的前綴匹配,會直接使用
- 如果第一步中沒有匹配上 = 或 ^~舔痕,那么會先記住最長匹配的前綴字符串 location
- 按照 nginx.conf 文件中的配置依次匹配正則表達(dá)式
- 如果所有的正則表達(dá)式都沒有匹配上评抚,那么會使用最長匹配的前綴字符串
下面看下實(shí)際的響應(yīng)是怎么樣的:
? test_nginx curl location.ziyang.com/Test1
exact match!
? test_nginx curl location.ziyang.com/Test1/
stop regular expressions match!
? test_nginx curl location.ziyang.com/Test1/Test2
longest regular expressions match!
? test_nginx curl location.ziyang.com/Test1/Test2/
longest prefix string match!
? test_nginx curl location.ziyang.com/Test1/Test3
stop regular expressions match!
- /Test1 匹配 location = /Test1
- /Test1/ 匹配 location ^~ /Test1/
- /Test1/Test2 匹配 location ~* /Test1/(\w+)$
- /Test1/Test2/ 匹配 location /Test1/Test2
- /Test1/Test3 匹配 location ^~ /Test1/
這里面重點(diǎn)解釋一下 /Test1/Test3 的匹配過程:
- 遍歷所有可以匹配上的前綴字符串,總共有兩個
- ^~ /Test1/
- /Test1
- 選取最長的前綴字符串 /Test1/伯复,由于前面有 ^~ 禁止正則表達(dá)式匹配慨代,因此直接使用 location ^~ /Test1/ 的規(guī)則
- 返回
stop regular expressions match!
preaccess 階段
下面就來到了 preaccess 階段。我們經(jīng)常會遇到一個問題啸如,就是如何限制每個客戶端的并發(fā)連接數(shù)侍匙?如何限制訪問頻率?這些就是在 preaccess 階段處理完成的叮雳,顧名思義想暗,preaccess 就是在連接之前。先來看下 limit_conn 模塊帘不。
limit_conn 模塊
這里面涉及到的模塊是 ngx_http_limit_conn_module
说莫,它的基本特性如下:
- 生效階段:
NGX_HTTP_PREACCESS_PHASE
階段 - 模塊:
http_limit_conn_module
- 默認(rèn)編譯進(jìn) Nginx,通過
--without-http_limit_conn_module
禁用 - 生效范圍
- 全部 worker 進(jìn)程(基于共享內(nèi)存)
- 進(jìn)入 preaccess 階段前不生效
- 限制的有效性取決于 key 的設(shè)計(jì):依賴 postread 階段的 realip 模塊取到真實(shí) IP
這里面有一點(diǎn)需要注意寞焙,就是 limit_conn key 的設(shè)計(jì)唬滑,所謂的 key 指的就是對哪個變量進(jìn)行限制,通常我們?nèi)〉亩际怯脩舻恼鎸?shí) IP棺弊。
說完了 limit_conn 的模塊晶密,再來說一下指令語法。
指令語法
- 定義共享內(nèi)存(包括大心K)稻艰,以及 key 關(guān)鍵字
Syntax: limit_conn_zone key zone=name:size;
Default: —
Context: http
- 限制并發(fā)連接數(shù)
Syntax: limit_conn zone number;
Default: —
Context: http, server, location
- 限制發(fā)生時的日志級別
Syntax: limit_conn_log_level info | notice | warn | error;
Default: limit_conn_log_level error;
Context: http, server, location
- 限制發(fā)生時向客戶端返回的錯誤碼
Syntax: limit_conn_status code;
Default: limit_conn_status 503;
Context: http, server, location
實(shí)戰(zhàn)
下面又到了實(shí)戰(zhàn)的環(huán)節(jié)了,通過一個實(shí)際的例子來看一下以上的幾個指令是怎么起作用的侈净。
老規(guī)矩尊勿,先上配置文件:
limit_conn_zone $binary_remote_addr zone=addr:10m;
#limit_req_zone $binary_remote_addr zone=one:10m rate=2r/m;
server {
listen 80;
server_name limit.ziyang.com;
root html/;
error_log logs/myerror.log info;
location /{
limit_conn_status 500;
limit_conn_log_level warn;
limit_rate 50;
limit_conn addr 1;
#limit_req zone=one burst=3 nodelay;
#limit_req zone=one;
}
}
- 在本地的 hosts 文件中添加 limit.ziyang.com 為本機(jī) IP
在這個配置文件中,做了兩條限制畜侦,一個是 limit_rate
限制為 50 個字節(jié)元扔,并發(fā)連接數(shù) limit_conn
限制為 1。
? test_nginx curl limit.ziyang.com
這時候訪問 limit.ziyang.com 這個站點(diǎn)旋膳,會發(fā)現(xiàn)速度非常慢嚣州,因?yàn)槊棵腌娭挥?50 個字節(jié)。
如果再同時訪問這個站點(diǎn)的話血崭,則會返回 500。
我在另一個終端里面同時訪問:
? ~ curl limit.ziyang.com
<html>
<head><title>500 Internal Server Error</title></head>
<body>
<center><h1>500 Internal Server Error</h1></center>
<hr><center>nginx/1.17.8</center>
</body>
</html>
可以看到尸变,Nginx 直接返回了 500。
limit_req 模塊
在本節(jié)開頭我們就提出了兩個問題:
如何限制每個客戶端的并發(fā)連接數(shù)减俏?
如何限制訪問頻率召烂?
第一個問題限制并發(fā)連接數(shù)的問題已經(jīng)解決了,下面來看第二個問題娃承。
這里面生效的模塊是 ngx_http_limit_req_module
奏夫,它的基本特性如下:
- 生效階段:
NGX_HTTP_PREACCESS_PHASE
階段 - 模塊:
http_limit_req_module
- 默認(rèn)編譯進(jìn) Nginx,通過
--without-http_limit_req_module
禁用 - 生效算法:leaky bucket 算法
- 生效范圍
- 全部 worker 進(jìn)程(基于共享內(nèi)存)
- 進(jìn)入 preaccess 階段前不生效
leaky bucket 算法
leaky bucket 叫漏桶算法历筝,其他用來限制請求速率的還有令牌環(huán)算法等桶蛔,這里面不展開講。
漏桶算法的原理是漫谷,先定義一個桶的大小仔雷,所有進(jìn)入桶內(nèi)的請求都會以恒定的速率被處理,如果請求太多超出了桶的容量舔示,那么就會立刻返回錯誤碟婆。用一張圖解釋一下。
這張圖里面惕稻,水龍頭在不停地滴水竖共,就像用戶發(fā)來的請求,所有的水滴都會以恒定的速率流出去俺祠,也就是被處理公给。漏桶算法對于突發(fā)流量有很好的限制作用,會將所有的請求平滑的處理掉蜘渣。
指令語法
- 定義共享內(nèi)存(包括大刑暑怼),以及 key 關(guān)鍵字和限制速率
Syntax: limit_req_zone key zone=name:size rate=rate ;
Default: —
Context: http
rate 單位為 r/s 或者 r/m(每分鐘或者每秒處理多少個請求)
- 限制并發(fā)連接數(shù)
Syntax: limit_req zone=name [burst=number] [nodelay];
Default: —
Context: http, server, location
- burst 默認(rèn)為 0
- nodelay蔫缸,如果設(shè)置了這個參數(shù)腿准,那么對于漏桶中的請求也會立刻返回錯誤
- 限制發(fā)生時的日志級別
Syntax: limit_req_log_level info | notice | warn | error;
Default: limit_req_log_level error;
Context: http, server, location
- 限制發(fā)生時向客戶端返回的錯誤碼
Syntax: limit_req_status code;
Default: limit_req_status 503;
Context: http, server, location
實(shí)戰(zhàn)
在實(shí)際驗(yàn)證之前呢,需要注意兩個問題:
- limit_req 與 limit_conn 配置同時生效時拾碌,哪個優(yōu)先級高吐葱?
- nodelay 添加與否,有什么不同校翔?
添加配置文件弟跑,這個配置文件與上一節(jié)的配置文件其實(shí)是相同的只不過需要注釋一下:
limit_conn_zone $binary_remote_addr zone=addr:10m;
limit_req_zone $binary_remote_addr zone=one:10m rate=2r/m;
server {
listen 80;
server_name limit.ziyang.com;
root html/;
error_log logs/myerror.log info;
location /{
limit_conn_status 500;
limit_conn_log_level warn;
#limit_rate 50;
#limit_conn addr 1;
#limit_req zone=one burst=3 nodelay;
limit_req zone=one;
}
}
結(jié)論:在 limit_req zone=one
指令下,超出每分鐘處理的請求數(shù)后就會立刻返回 503防症。
? test_nginx curl limit.ziyang.com
<html>
<head><title>503 Service Temporarily Unavailable</title></head>
<body>
<center><h1>503 Service Temporarily Unavailable</h1></center>
<hr><center>nginx/1.17.8</center>
</body>
</html>
改變一下注釋的指令:
limit_req zone=one burst=3;
#limit_req zone=one;
在沒有添加 burst 參數(shù)時孟辑,會立刻返回錯誤哎甲,而加上之后,不會返回錯誤扑浸,而是等待請求限制解除,直到可以處理請求時再返回燕偶。
再來看一下 nodelay 參數(shù):
limit_req zone=one burst=3 nodelay;
添加了 nodelay 之后喝噪,請求在沒有達(dá)到 burst 限制之前都可以立刻被處理并返回,超出了 burst 限制之后指么,才會返回 503酝惧。
現(xiàn)在可以回答一下剛開始提出的兩個問題:
- limit_req 與 limit_conn 配置同時生效時,哪個優(yōu)先級高伯诬?
- limit_req 在 limit_conn 處理之前晚唇,因此是 limit_req 會生效
- nodelay 添加與否,有什么不同盗似?
- 不添加 nodelay哩陕,請求會等待,直到能夠處理請求赫舒;添加 nodelay悍及,在不超出 burst 的限制的情況下會立刻處理并返回,超出限制則會返回 503接癌。
access 階段
經(jīng)過 preaccess 階段對用戶的限流之后心赶,就到了 access 階段。
access 模塊
這里面涉及到的模塊是 ngx_http_access_module
缺猛,它的基本特性如下:
- 生效階段:
NGX_HTTP_ACCESS_PHASE
階段 - 模塊:
http_access_module
- 默認(rèn)編譯進(jìn) Nginx缨叫,通過
--without-http_access_module
禁用 - 生效范圍
- 進(jìn)入 access 階段前不生效
指令語法
Syntax: allow address | CIDR | unix: | all;
Default: —
Context: http, server, location, limit_except
Syntax: deny address | CIDR | unix: | all;
Default: —
Context: http, server, location, limit_except
access 模塊提供了兩條指令 allow
和 deny
,來看幾個例子:
location / {
deny 192.168.1.1;
allow 192.168.1.0/24;
allow 10.1.1.0/16;
allow 2001:0db8::/32;
deny all;
}
對于用戶訪問來說荔燎,這些指令是順序執(zhí)行的耻姥,當(dāng)滿足了一條之后,就不會再向下執(zhí)行有咨。這個模塊比較簡單咏闪,我們這里不做實(shí)戰(zhàn)演練了。
auth_basic 模塊
auth_basic 模塊是用作用戶認(rèn)證的摔吏,當(dāng)開啟了這個模塊之后鸽嫂,我們通過瀏覽器訪問網(wǎng)站時,就會返回一個 401 Unauthorized征讲,當(dāng)然這個 401 用戶不會看見据某,瀏覽器會彈出一個對話框要求輸入用戶名和密碼。這個模塊使用的是 RFC2617 中的定義诗箍。
指令語法
- 基于 HTTP Basic Authutication 協(xié)議進(jìn)行用戶密碼的認(rèn)證
- 默認(rèn)編譯進(jìn) Nginx
- --without-http_auth_basic_module
- disable ngx_http_auth_basic_module
Syntax: auth_basic string | off;
Default: auth_basic off;
Context: http, server, location, limit_except
Syntax: auth_basic_user_file file;
Default: —
Context: http, server, location, limit_except
這里面我們會用到一個工具叫 htpasswd癣籽,這個工具可以用來生成密碼文件,而 auth_basic_user_file
就依賴這個密碼文件。
htpasswd 依賴安裝包 httpd-tools
生成密碼的命令為:
htpasswd –c file –b user pass
生成的密碼文件的格式為:
# comment
name1:password1
name2:password2:comment
name3:password3
實(shí)戰(zhàn)
- 在 example 目錄下生成密碼文件 auth.pass
htpasswd -bc auth.pass ziyang 123456
- 添加配置文件
server {
server_name access.ziyang.com;
listen 80;
error_log logs/error.log debug;
default_type text/plain;
location /auth_basic {
satisfy any;
auth_basic "test auth_basic";
auth_basic_user_file example/auth.pass;
deny all;
}
}
- 重載 Nginx 配置文件
- 在 /etc/hosts 文件中添加 access.ziyang.com
這時候訪問 access.ziyang.com 就會彈出對話框筷狼,提示輸入密碼:
auth_request 模塊
- 功能:向上游的服務(wù)轉(zhuǎn)發(fā)請求瓶籽,若上游服務(wù)返回的響應(yīng)碼是 2xx,則繼續(xù)執(zhí)行埂材,若上游服務(wù)返回的響應(yīng)碼是 2xx塑顺,則繼續(xù)執(zhí)行,若上游服務(wù)返回的是 401 或者 403俏险,則將響應(yīng)返回給客戶端
- 原理:收到請求后严拒,生成子請求,通過反向代理技術(shù)把請求傳遞給上游服務(wù)
- 默認(rèn)未編譯進(jìn) Nginx竖独,需要通過 --with-http_auth_request_module 編譯進(jìn)去
指令語法
Syntax: auth_request uri | off;
Default: auth_request off;
Context: http, server, location
Syntax: auth_request_set $variable value;
Default: —
Context: http, server, location
實(shí)戰(zhàn)
- 在上一個配置文件中添加以下內(nèi)容
server {
server_name access.ziyang.com;
listen 80;
error_log logs/error.log debug;
#root html/;
default_type text/plain;
location /auth_basic {
satisfy any;
auth_basic "test auth_basic";
auth_basic_user_file example/auth.pass;
deny all;
}
location / {
auth_request /test_auth;
}
location = /test_auth {
proxy_pass http://127.0.0.1:8090/auth_upstream;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
}
}
- 這個配置文件中裤唠,/ 路徑下會將請求轉(zhuǎn)發(fā)到另外一個服務(wù)中去,可以用 nginx 再搭建一個服務(wù)
- 如果這個服務(wù)返回 2xx莹痢,那么鑒權(quán)成功种蘸,如果返回 401 或 403 則鑒權(quán)失敗
限制所有 access 階段模塊的 satisfy 指令
指令語法
Syntax: satisfy all | any;
Default: satisfy all;
Context: http, server, location
satisfy
指令有兩個值一個是 all,一個是 any竞膳,這個模塊對 acces 階段的三個模塊都生效:
- access 模塊
- auth_basic 模塊
- auth_request 模塊
- 其他模塊
如果 satisfy
指令的值是 all 的話劈彪,就表示必須所有 access 階段的模塊都要執(zhí)行,都通過了才會放行顶猜;值是 any 的話沧奴,表示有任意一個模塊得到執(zhí)行即可。
下面有幾個問題可以加深一下理解:
-
如果有 return 指令长窄,access 階段會生效嗎滔吠?
return 指令屬于 rewrite 階段,在 access 階段之前挠日,因此不會生效疮绷。
-
多個 access 模塊的順序有影響嗎?
ngx_http_auth_request_module, ngx_http_auth_basic_module, ngx_http_access_module,
有影響
-
輸對密碼嚣潜,下面可以訪問到文件嗎冬骚?
location /{ satisfy any; auth_basic "test auth_basic"; auth_basic_user_file examples/auth.pass; deny all; }
可以訪問到,因?yàn)?
satisfy
的值是 any懂算,因此只要有模塊滿足只冻,即可放行。 -
如果把 deny all 提到 auth_basic 之前呢计技?
依然可以喜德,因?yàn)楦鱾€模塊執(zhí)行順序和指令的順序無關(guān)。
-
如果改為 allow all垮媒,有機(jī)會輸入密碼嗎舍悯?
沒有機(jī)會航棱,因?yàn)?allow all 是 access 模塊,先于 auth_basic 模塊執(zhí)行萌衬。
precontent 階段
講到了這里饮醇,我們再來回顧一下 Nginx 處理 HTTP 請求的 11 個階段:
現(xiàn)在我們已經(jīng)來到了 precontent 階段,這個階段只有 try_files 這一個指令秕豫。
try_files 模塊
指令語法
Syntax: try_files file ... uri;
try_files file ... =code;
Default: —
Context: server, location
- 模塊:
ngx_http_try_files_module
模塊 - 依次試圖訪問多個 URL 對應(yīng)的文件(由 root 或者 alias 指令指定)朴艰,當(dāng)文件存在時,直接返回文件內(nèi)容馁蒂,如果所有文件都不存在呵晚,則按照最后一個 URL 結(jié)果或者 code 返回
實(shí)戰(zhàn)
下面我們實(shí)際看一個例子:
server {
server_name tryfiles.ziyang.com;
listen 80;
error_log logs/myerror.log info;
root html/;
default_type text/plain;
location /first {
try_files /system/maintenance.html
$uri $uri/index.html $uri.html
@lasturl;
}
location @lasturl {
return 200 'lasturl!\n';
}
location /second {
try_files $uri $uri/index.html $uri.html =404;
}
}
結(jié)果如下:
- 訪問 /first 實(shí)際上到了 lasturl蜘腌,然后返回 200
- 訪問 /second 則返回了 404
這兩個結(jié)果都與配置文件是一致的沫屡。
? test_nginx curl tryfiles.ziyang.com/second
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.17.8</center>
</body>
</html>
? test_nginx curl tryfiles.ziyang.com/first
lasturl!
mirror 模塊
mirror 模塊可以實(shí)時拷貝流量,這對于需要同時訪問多個環(huán)境的請求是非常有用的撮珠。
指令語法
- 模塊:
ngx_http_mirror_module
模塊沮脖,默認(rèn)編譯進(jìn) Nginx- 通過 --without-http_mirror_module 移除模塊
- 功能:處理請求時,生成子請求訪問其他服務(wù)芯急,對子請求的返回值不做處理
Syntax: mirror uri | off;
Default: mirror off;
Context: http, server, location
Syntax: mirror_request_body on | off;
Default: mirror_request_body on;
Context: http, server, location
實(shí)戰(zhàn)
- 配置文件如下所示勺届,需要再開啟另外一個 Nginx 來接收請求
server {
server_name mirror.ziyang.com;
listen 8001;
error_log logs/error_log debug;
location / {
mirror /mirror;
mirror_request_body off;
}
location = /mirror {
internal;
proxy_pass http://127.0.0.1:10020$request_uri;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
}
}
- 在 access.log 文件中可以看到有請求記錄日志
content 階段
下面開始就到了 content 階段,先來看 content 階段的 static 模塊娶耍,雖然這是位于 content 階段的最后一個處理模塊免姿,但是這里先來介紹它。
static 模塊
root 和 alias 指令
先來一下 root 和 alias 這兩個指令榕酒,這兩個指令都是用來映射文件路徑的胚膊。
Syntax: alias path;
Default: —
Context: location
Syntax: root path;
Default: root html;
Context: http, server, location, if in location
- 功能:將 URL 映射為文件路徑,以返回靜態(tài)文件內(nèi)容
- 差別:root 會將完整 URL 映射進(jìn)文件路徑中想鹰,alias 只會將 location 后的 URL 映射到文件路徑
實(shí)戰(zhàn)
下面來看一個問題:
現(xiàn)在有一個文件路徑:
html/first/
└── 1.txt
配置文件如下所示:
server {
server_name static.ziyang.com;
listen 80;
error_log logs/myerror.log info;
location /root {
root html;
}
location /alias {
alias html;
}
location ~ /root/(\w+\.txt) {
root html/first/$1;
}
location ~ /alias/(\w+\.txt) {
alias html/first/$1;
}
location /RealPath/ {
alias html/realpath/;
return 200 '$request_filename:$document_root:$realpath_root\n';
}
}
那么訪問以下 URL 會得到什么響應(yīng)呢紊婉?
/root
/alias
/root/1.txt
/alias/1.txt
? test_nginx curl static.ziyang.com/alias/1.txt
test1%
? test_nginx curl static.ziyang.com/alias/
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
? test_nginx curl static.ziyang.com/root/
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.17.8</center>
</body>
</html>
? test_nginx curl static.ziyang.com/root/1.txt
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.17.8</center>
</body>
</html>
訪問這四個路徑分別得到的結(jié)果是:
- /root:404
- /alias:200
- /root/1.txt:404
- /alias/1.txt:200
這是為什么呢?是因?yàn)榧希瑀oot 在映射 URL 時喻犁,會把 location 中的路徑也加進(jìn)去,也就是:
-
static.ziyang.com/root/
實(shí)際訪問的是html/root/
-
static.ziyang.com/root/1.txt
實(shí)際是html/first/1.txt/root/1.txt
-
static.ziyang.com/alias/
實(shí)際上是正確訪問到了html
文件夾何缓,由于后面有/
的存在肢础,因此實(shí)際訪問的是html/index.html
-
static.ziyang.com/alias/1.txt
實(shí)際訪問的是html/first/1.txt
,文件存在
三個相關(guān)變量
還是上面的配置文件:
location /RealPath/ {
alias html/realpath/;
return 200 '$request_filename:$document_root:$realpath_root\n';
}
這里有一個問題碌廓,在訪問 /RealPath/1.txt
時乔妈,這三個變量的值各為多少?
為了解答這個問題氓皱,我們先來解釋三個變量:
- request_filename:待訪問文件的完整路徑
- document_root:由 URI 和 root/alias 指令生成的文件夾路徑(可能包含軟鏈接的路徑)
- realpath_root:將 document_root 中的軟鏈接替換成真實(shí)路徑
為了驗(yàn)證這三個變量路召,在 html 目錄下建立一個軟鏈接指向 first 文件夾:
ln -s first realpath
? html curl static.ziyang.com/realpath/1.txt
/Users/mtdp/myproject/nginx/test_nginx/html/realpath/1.txt:/Users/mtdp/myproject/nginx/test_nginx/html/realpath/:/Users/mtdp/myproject/nginx/test_nginx/html/first
可以看出來勃刨,三個路徑分別是:
- /Users/mtdp/myproject/nginx/test_nginx/html/realpath/1.txt
- /Users/mtdp/myproject/nginx/test_nginx/html/realpath/
- /Users/mtdp/myproject/nginx/test_nginx/html/first
還有其他的一些配置指令,例如:
靜態(tài)文件返回時的 Content-Type
Syntax: types { ... }
Default: types { text/html html; image/gif gif; image/jpeg jpg; }
Context: http, server, location
Syntax: default_type mime-type;
Default: default_type text/plain;
Context: http, server, location
Syntax: types_hash_bucket_size size;
Default: types_hash_bucket_size 64;
Context: http, server, location
Syntax: types_hash_max_size size;
Default: types_hash_max_size 1024;
Context: http, server, location
未找到文件時的錯誤日志
Syntax: log_not_found on | off;
Default: log_not_found on;
Context: http, server, location
在生產(chǎn)環(huán)境中股淡,經(jīng)成硪可能會有找不到文件的情況,錯誤日志中就會打印出來:
[error] 10156#0: *10723 open() "/html/first/2.txt/root/2.txt" failed (2: No such file or directory)
如果不想記錄日志唯灵,可以關(guān)掉贾铝。
重定向跳轉(zhuǎn)的域名
現(xiàn)在有另外一個問題,當(dāng)我們訪問目錄時最后沒有帶 /
埠帕,static 模塊會返回 301 重定向垢揩,那么這個規(guī)則是怎么定義的呢,看下面三個指令:
# 該指令決定重定向時的域名敛瓷,可以決定返回哪個域名
Syntax: server_name_in_redirect on | off;
Default: server_name_in_redirect off;
Context: http, server, location
# 該指令決定重定向時的端口
Syntax: port_in_redirect on | off;
Default: port_in_redirect on;
Context: http, server, location
# 該指令決定是否填域名叁巨,默認(rèn)是打開的,也就是返回絕對路徑
Syntax: absolute_redirect on | off;
Default: absolute_redirect on;
Context: http, server, location
這三個指令的實(shí)際用法來實(shí)戰(zhàn)演示一下呐籽,先來看配置文件:
server {
server_name return.ziyang.com dir.ziyang.com;
server_name_in_redirect on;
listen 8088;
port_in_redirect on;
absolute_redirect off;
root html/;
}
absolute_redirect
默認(rèn)是打開的锋勺,我們把它關(guān)閉了,看下是怎么返回的:
? test_nginx curl localhost:8088/first -I
HTTP/1.1 301 Moved Permanently
Server: nginx/1.17.8
Date: Tue, 12 May 2020 00:31:36 GMT
Content-Type: text/html
Content-Length: 169
Connection: keep-alive
Location: /first/
這個時候看到返回的頭部 Location
中沒有加上域名狡蝶。
下面再把 absolute_redirect
打開(默認(rèn)是打開的庶橱,因此注釋掉就行了),看下返回什么:
absolute_redirect on
server_name_in_redirect on
port_in_redirect on
? test_nginx curl localhost:8088/first -I
HTTP/1.1 301 Moved Permanently
Server: nginx/1.17.8
Date: Tue, 12 May 2020 00:35:49 GMT
Content-Type: text/html
Content-Length: 169
Location: http://return.ziyang.com:8088/first/
Connection: keep-alive
可以看到贪惹,這時候就返回了域名苏章,而且返回的是我們配置的主域名加端口號,這是因?yàn)椋?code>server_name_in_redirect 和 port_in_redirect
這兩個指令打開了奏瞬,如果關(guān)閉掉這兩個指令枫绅,看下返回什么:
absolute_redirect on
server_name_in_redirect off
port_in_redirect off
? test_nginx curl localhost:8088/first -I
HTTP/1.1 301 Moved Permanently
Server: nginx/1.17.8
Date: Tue, 12 May 2020 00:39:31 GMT
Content-Type: text/html
Content-Length: 169
Location: http://localhost/first/
Connection: keep-alive
這兩個指令都設(shè)置為 off
之后,會發(fā)現(xiàn)返回的不再是主域名加端口號丝格,而是我們請求的域名和端口號撑瞧,如果在請求頭中加上 Host
,那么就會用 Host
請求頭中的域名显蝌。
index 模塊
模塊:
ngx_http_index_module
功能:指定
/
結(jié)尾的目錄訪問時预伺,返回 index 文件內(nèi)容-
語法:
Syntax: index file ...; Default: index index.html; Context: http, server, location
先于 autoindex 模塊執(zhí)行
這個模塊,當(dāng)我們訪問以 /
結(jié)尾的目錄時曼尊,會去找 root 或 alias 指令的文件夾下的 index.html酬诀,如果有這個文件,就會把文件內(nèi)容返回骆撇,也可以指定其他文件瞒御。
autoindex 模塊
模塊:
ngx_http_autoindex_module
,默認(rèn)編譯進(jìn) Nginx神郊,使用--without-http_autoindex_module
取消功能:當(dāng) URL 以
/
結(jié)尾時肴裙,嘗試以 html/xml/json/jsonp 等格式返回 root/alias 中指向目錄的目錄結(jié)構(gòu)-
語法:
# 開啟或關(guān)閉 Syntax: autoindex on | off; Default: autoindex off; Context: http, server, location # 當(dāng)以 HTML 格式輸出時趾唱,控制是否轉(zhuǎn)換為 KB/MB/GB Syntax: autoindex_exact_size on | off; Default: autoindex_exact_size on; Context: http, server, location # 控制以哪種格式輸出 Syntax: autoindex_format html | xml | json | jsonp; Default: autoindex_format html; Context: http, server, location # 控制是否以本地時間格式顯示還是 UTC 格式 Syntax: autoindex_localtime on | off; Default: autoindex_localtime off; Context: http, server, location
實(shí)戰(zhàn)
- 配置文件如下:
server {
server_name autoindex.ziyang.com;
listen 8080;
location / {
alias html/;
autoindex on;
#index b.html;
autoindex_exact_size on;
autoindex_format html;
autoindex_localtime on;
}
}
這里我把 index b.html
這條指令給注釋掉了,而 index 模塊是默認(rèn)編譯進(jìn) Nginx 的蜻懦,且默認(rèn)指令是 index index.html
甜癞,因此,會去找是否有 index.html 這個文件宛乃。
- 打開瀏覽器悠咱,訪問 autoindex.ziyang.com:8080,html 目錄下默認(rèn)是有 index.html 文件的征炼,因此顯示結(jié)果為:
- 打開
index b.html
指令注釋析既。由于 html 文件夾下并不存在 b.html 這個文件,所以請求會走到 autoindex 模塊谆奥,顯示目錄:
后面的文件大小顯示格式就是由 autoindex_exact_size on;
這條指令決定的眼坏。
concat模塊
下面介紹一個可以提升小文件性能的模塊,這個模塊是由阿里巴巴開發(fā)的雄右,在淘寶網(wǎng)中有廣泛應(yīng)用空骚。
模塊:ngx_http_concat_module
模塊開發(fā)者:Tengine(https://github.com/alibaba/nginx-http-concat)? --add-module=../nginx-http-concat/
功能:合并多個小文件請求纺讲,可以明顯提升 HTTP 請求的性能
-
指令:
#在 URI 后面加上 ??擂仍,通過 ”,“ 分割文件,如果還有參數(shù)熬甚,則在最后通過 ? 添加參數(shù) concat on | off default concat off Context http, server, location concat_types MIME types Default concat_types: text/css application/x-javascript Context http, server, location concat_unique on | off Default concat_unique on Context http, server, location concat_max_files numberp Default concat_max_files 10 Context http, server, location concat_delimiter string Default NONE Context http, server, locatione concat_ignore_file_error on | off Default off Context http, server, location
打開淘寶主頁逢渔,會發(fā)現(xiàn)小文件都是通過這個模塊來提高性能的:
這里就不做實(shí)戰(zhàn)了,感興趣的同學(xué)可以自己去編譯一下這個模塊乡括,做一下實(shí)驗(yàn)肃廓,我把配置文件放在這里:
server {
server_name concat.ziyang.com;
error_log logs/myerror.log debug;
concat on;
root html;
location /concat {
concat_max_files 20;
concat_types text/plain;
concat_unique on;
concat_delimiter ':::';
concat_ignore_file_error on;
}
}
log 階段
下面終于來到了 11 個階段的最后一個階段,記錄請求訪問日志的 log 模塊诲泌。
- 功能:將 HTTP 請求相關(guān)信息記錄到日志
- 模塊:
ngx_http_log_module
盲赊,無法禁用
access 日志格式
Syntax: log_format name [escape=default|json|none] string ...;
Default: log_format combined "...";
Context: http
默認(rèn)的 combined 日志格式:
log_format combined '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent ' '"$http_referer"
"$http_user_agent"';
配置日志文件路徑
Syntax: access_log path [format [buffer=size] [gzip[=level]] [flush=time] [if=condition]];
access_log off;
Default: access_log logs/access.log combined;
Context: http, server, location, if in location, limit_except
path 路徑可以包含變量:不打開 cache 時每記錄一條日志都需要打開、關(guān)閉日志文件
if 通過變量值控制請求日志是否記錄
-
日志緩存
功能:批量將內(nèi)存中的日志寫入磁盤
-
寫入磁盤的條件:
所有待寫入磁盤的日志大小超出緩存大蟹笊ā哀蘑;
達(dá)到 flush 指定的過期時間;
worker 進(jìn)程執(zhí)行 reopen 命令葵第,或者正在關(guān)閉绘迁。
-
日志壓縮
- 功能:批量壓縮內(nèi)存中的日志,再寫入磁盤
- buffer 大小默認(rèn)為 64KB
- 壓縮級別默認(rèn)為 1(1最快壓縮率最低卒密,9最慢壓縮率最高)
- 打開日志壓縮時缀台,默認(rèn)打開日志緩存功能
對日志文件名包含變量時的優(yōu)化
Syntax: open_log_file_cache max=N [inactive=time] [min_uses=N] [valid=time];
open_log_file_cache off;
Default: open_log_file_cache off;
Context: http, server, location
- max:緩存內(nèi)的最大文件句柄數(shù),超出后用 LRU 算法淘汰
- inactive:文件訪問完后在這段時間內(nèi)不會被關(guān)閉哮奇。默認(rèn) 10 秒
- min_uses:在 inactive 時間內(nèi)使用次數(shù)超過 min_uses 才會繼續(xù)存在內(nèi)存中膛腐。默認(rèn) 1
- valid:超出 valid 時間后睛约,將對緩存的日志文件檢查是否存在。默認(rèn) 60 秒
- off:關(guān)閉緩存功能
日志模塊沒有實(shí)戰(zhàn)哲身。
到了這里痰腮,我們已經(jīng)將 Nginx 處理 HTTP 請求的 11 個階段全部梳理了一遍,每個階段基本都有對應(yīng)的模塊律罢。相信對于這樣一個全流程的解析膀值,大家都能夠看懂 Nginx 的配置了,在此之上误辑,還能夠按照需求靈活配置出自己想要的配置沧踏,這樣就真正的掌握了 11 個階段。
最后巾钉,歡迎大家關(guān)注我的個人博客:iziyang.github.io
本文首發(fā)于我的個人博客:iziyang.github.io