1?Nginx簡介
Web服務器市場份額
Nginx [engine x] 最初由 Lgor Sysoev 編寫。根據 Netcraft 的數據,到2020年9月,Nginx 服務或代理了25.76%站點,市場份額占到了約34.03%。
Nginx 被廣泛用作:
· HTTP服務器
· 反向代理服務器
· 郵件代理服務器
· 通用的TCP/UDP代理
?
2?Nginx架構
?
Nginx服務由一個Master進程和多個Worker進程構成。Master進程的主要職責是讀取、解析配置文件,并維護工作進程。工作進程則負責實際的請求處理。
為了高效的在Worker之間分發請求,Nginx引入了依賴于操作系統的、高效的事件驅動模型。Worker進程的數量常常根據CPU核心數設置。這種模式的好處是每個worker進程都相互獨立,無需添加鎖,減少鎖的開銷。采用獨立的進程并且不相互影響,一個進程進程退出并不影響其他的進程。一個worker進程的異常退出只會影響當前worker的上的請求,不影響其他worker的請求,降低了風險。
?
3?分Nginx處理http請求的11個階段
Nginx 的11個執行階段以及對應的http模塊
Nginx 的11個執行階段的枚舉類可以參考nginx源碼中的ngx_http_core_module.h 中ngx_http_phases枚舉
typedef enum {NGX_HTTP_POST_READ_PHASE = 0,NGX_HTTP_SERVER_REWRITE_PHASE,NGX_HTTP_FIND_CONFIG_PHASE,NGX_HTTP_REWRITE_PHASE,NGX_HTTP_POST_REWRITE_PHASE,NGX_HTTP_PREACCESS_PHASE,NGX_HTTP_ACCESS_PHASE,NGX_HTTP_POST_ACCESS_PHASE,NGX_HTTP_PRECONTENT_PHASE,NGX_HTTP_CONTENT_PHASE,NGX_HTTP_LOG_PHASE
} ngx_http_phases;
模塊介紹可以參考Modules reference
11個執行階段的各個模塊的執行流程圖:
3.1?post_read 階段介紹
Nginx的第一個階段post_read 階段是在正式處理請求之前工作的。在這個階段剛剛獲取了請求頭的信息,還沒有進行任何處理。我們可以拿到當前請求的原始數據,比如當前請求的真實IP。
在http協議中有兩種方式獲取用戶IP:
· X-Forwardex-For 是用來傳遞 IP 的,這個頭部會把經過的節點 IP 都記錄下來。
· X-Real-IP:可以記錄用戶真實的 IP 地址,只能有一個。
3.1.1 ngx_http_realip_module 模塊介紹
post_read涉及到的模塊 ngx_http_realip_module 當前模塊不會自動編譯進Nginx中所以需要手動編譯進入nginx源碼文件夾中執行下列操作
./configure --with-http_realip_module
· 配置示例
set_real_ip_from 192.168.1.0/24;
set_real_ip_from 192.168.2.1;
set_real_ip_from 2001:0db8 :: / 32;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
· 內嵌變量
$realip_remote_addr #保留原始客戶地址
$realip_remote_port #保留原始客戶端端口
· 模塊指令
當前模塊有三個指令 set_real_ip_from、real_ip_header、real_ip_recursive
· set_real_ip_from
句法: set_real_ip_from address | CIDR | unix:;
默認: -
內容: http,server,location
指定可信的地址,只有從該地址建立的連接,獲取的 realip 才是可信的
· real_ip_header
句法: real_ip_header field | X-Real-IP | X-Forwarded-For | proxy_protocol;
默認: real_ip_header X-Real-IP;
內容: http,server,location
指定從哪個頭部取真實的 IP 地址,默認從 X-Real-IP 中取,如果設置從 X-Forwarded-For 中取,會先從最后一個 IP 開始取
· real_ip_recursive
句法: real_ip_recursive on | off;
默認: real_ip_recursive off;
內容: http,server,location
環回地址,默認關閉,打開的時候,如果 X-Forwarded-For 最后一個地址與客戶端地址相同,會過濾掉該地址
· 實戰 realIp 模塊
修改nginx.conf 配置文件內容如下:
server {listen 80;server_name abbila.com;set_real_ip_from 10.211.55.2;real_ip_recursive off;#real_ip_recursive on;real_ip_header X-Forwarded-For;location / {return 200 "client real Ip is:$remote_addr \n";}
」
上面的配置中設置了可信Ip為10.211.55.2,real_ip_recursive是關閉狀態的,real_ip_header 從X-Forwarded-For獲取。測試結果:
curl -H "X-Forwarded-For: 1.1.1.1,22.22.22.22,10.211.55.2" abbila.com
client real Ip is:10.211.55.2
然后把real_ip_recursive 打開 內容如下:
server {listen 80;server_name abbila.com;set_real_ip_from 10.211.55.2;#real_ip_recursive off;real_ip_recursive on;real_ip_header X-Forwarded-For;location / {return 200 "client real Ip is:$remote_addr \n";}
」
測試結果
curl -H "X-Forwarded-For: 1.1.1.1,22.22.22.22,10.211.55.2" abbila.com
client real Ip is:22.22.22.22
通過上面兩個實驗可以看出來,如果real_ip_recursive關閉的話,獲取的realIp為X-Forwarded-For的最后一個IP如果是打開狀態的話那就把X-Forwarded-For中與可信IP重復的IP過濾掉然后取剩下的最后一個IP為realip。如果使用 X-Forwarded-For 獲取 realip 的話,real_ip_recursive 需要打開。并且,realip 依賴于 set_real_ip_from 設置的可信地址。那么有人可能就會問了,那直接用 X-Real-IP 來選取真實的 IP 地址不就好了。這是可以的,但是 X-Real-IP 是 Nginx 獨有的,如果客戶端與服務器之間還有其他非 Nginx 軟件實現的代理,就會造成取不到 X-Real-IP 頭部,所以這個要根據實際情況來定。
3.2 rewrite 階段
3.2.1 ngx_http_rewrite_model 模塊介紹
rewrite 階段設計到兩個部分
· NGX_HTTP_REWRITE_PHASE
· NGX_HTTP_SERVER_REWRITE_PHASE
當前模塊有 break、if、return、rewrite、rewrite_log、set、uninitialized_variable_warn 指令
· break 指令
句法: break;
默認: —
內容: server,location,if
停止處理當前ngx_http_rewrite_module指令集 。
· if 指令
句法: if (condition) { ... }
默認: —
內容: server, location
如果condition為true則執行{}中的內容。
condition可以為以下的一種或者幾種:
· 變量名;如果變量的值為空字符串或“ 0”,則為false;否則為false。
· 使用“ =”和“ !=”運算符將變量與字符串進行比較。
· 使用“ ~”(區分大小寫的匹配)和“ ~*”(區分大小寫的匹配)運算符將變量與正則表達式進行匹配。正則表達式可以包含捕獲。
這些捕獲可用于以后在$1…$9變量中重用。負運算符“ !~”和“ !~*”也可用。如果正則表達式包含“ }”或“ ;”字符,則整個表達式應用單引號或雙引號引起來。
· 使用“ -f”和“ !-f”運算符檢查文件是否存在;
· 使用“ -d”和“ !-d”運算符檢查目錄是否存在;
· 使用“ -e”和“ !-e”運算符檢查文件,目錄或符號鏈接是否存在;
· 使用“ -x”和“ !-x”運算符檢查可執行文件。
· return 指令
句法: return code [text];return code URL;return URL;
默認: —
內容: server,location,if
狀態碼可以包含以下幾種:
· Nginx 自定義
· 444:立刻關閉連接,用戶收不到響應
· HTTP 1.0 標準:
· 301:永久重定向
· 302:臨時重定向,禁止被緩存
· HTTP 1.1 標準:
· 303:臨時重定向,允許改變方法,禁止被緩存
· 307:臨時重定向,不允許改變方法,禁止被緩存
· 308:永久重定向,不允許改變方法
也可以
return 200 "the status code is 200";
可以返回對應的 error_page 可以參考:
http://nginx.org/en/docs/http/ngx_http_core_module.html#error_page
· rewrite 指令
句法: rewrite regex replacement [flag];
默認: —
內容: server,location,if
可選flag參數可以是以下之一:
· last
停止處理當前ngx_http_rewrite_module指令集, 并開始搜索與更改后的URI相匹配的新位置;
· break
ngx_http_rewrite_module與break指令一樣, 停止處理當前的指令集 ;
· redirect
返回帶有302代碼的臨時重定向;如果替換字符串不是以“ http://”,“ https://”或“ $scheme” 開頭,則使用
· permanent
返回帶有301代碼的永久重定向。
· rewrite_log 指令
句法: rewrite_log on | off;
默認: rewrite_log off;
內容: http,server,location,if
如果打開當前指令之后,會把 rewrite 的日志寫入 logs/rewrite_error.log 日志文件中,
· set 指令
句法: set $variable value;
默認: —
內容: server,location,if
可以為variable 設置對應的value值。該value可以包含文本,變量,他們的組合。
· uninitialized_variable_warn 指令
句法: uninitialized_variable_warn on | off;
默認: uninitialized_variable_warn on;
內容: http,server,location,if
控制是否記錄有關未初始化變量的警告。比如有些值沒有找到等等。
· 實戰一下rewrite模塊:
nginx.conf 配置文件如下:
server {listen 80;server_name abbila.com;location /break/ {rewrite ^/break/(.*) /test/$1 break;}location /last/ {rewrite_log on;rewrite ^/last/(.*) /test/$1 last;}location /test/ {return 200 "test page";}
}
測試下last:
curl abbila.com/last/
test page%
在測試下break:
curl abbila.com/break/ <html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.18.0</center>
</body>
</html>
通過測試可以看到break是跳過當前請求的rewrite階段,并繼續執行本請求的其他階段,last與break最大的不同是,last會重新發起一個新請求,并重新匹配location,所以對于/last,重新匹配請求以后會匹配到/test/,所以最終對應的content階段的輸出是test page;然后看一下日志error.log
2020/10/16 15:51:25 [error] 29625#0: *26 "/usr/local/webserver/nginx/html/test/index.html" is not found (2: No such file or directory), client: 10.211.55.2, server: abbila.com, request: "GET /break/ HTTP/1.1", host: "abbila.com"
2020/10/16 15:51:36 [notice] 29625#0: *27 "^/last/(.*)" matches "/last/", client: 10.211.55.2, server: abbila.com, request: "GET /last/ HTTP/1.1", host: "abbila.com"
2020/10/16 15:51:36 [notice] 29625#0: *27 rewritten data: "/test/", args: "", client: 10.211.55.2, server: abbila.com, request: "GET /last/ HTTP/1.1", host: "abbila.com"
因為在break里面沒有打開rewrite_log 在last中設置了rewrite_log 可以看到里面有rewrite日志信息,注意要把error.log的級別調成notice級別。
error_log logs/error.log notice;
3.3 preaccess 階段
我們經常會遇到一個問題,就是如何限制每個客戶端的并發連接數?如何限制訪問頻率?當前階段設計到兩個模塊 ngx_http_limit_conn_module ngx_http_limit_req_module這兩個模塊都是默認編譯到nginx中的可以使用–without-http_limit_req_module --without-http_limit_conn_module 移除。
3.3.1 http_limit_req_module 和 http_limit_conn_module 模塊
req模塊有四個指令 limit_req、limit_req_log_level、limit_req_status、limit_req_zone
與指對應的conn也存在四個指令limit_conn、limit_conn_log_level、limit_conn_status、limit_conn_zone
· limit_req 指令
句法: limit_req zone=name [burst=number] [nodelay | delay=number];
默認: —
內容: http, server, location
定義共享內存(包括大小),以及 key 關鍵字和限制速率 rate 單位為 r/s 或者 r/m(每分鐘或者每秒處理多少個請求)
· limit_req_log_level指令
句法: limit_req_log_level info | notice | warn | error;
默認: limit_req_log_level error;
內容: http,server,location
如果超過了設置的閥值則把日記的記錄級別修改為對應的級別。
· limit_req_status 指令
句法: limit_req_status code;
默認: limit_req_status 503;
內容: http,server,location
為拒絕的服務請求設置對應點狀態碼。
· limit_req_zone 指令
句法: limit_req_zone key zone=name:size rate=rate [sync];
默認: —
內容: http
設置共享內存區域的參數,該參數將保留各種鍵的狀態。特別是,狀態存儲當前的過多請求數。該key可以包含文本,變量,他們的組合。具有空鍵值的請求不予考慮。
可以看到上面各個模塊的執行順序。當兩個同事設置時,limit_req 在 limit_conn 處理之前,因此是 limit_req 會生效
如果不添加 nodelay,請求會等待,直到能夠處理請求;添加 nodelay,在不超出 burst 的限制的情況下會立刻處理并返回,超出限制則會返回 limit_req_status狀態。
· 實戰:限流模塊
nginx.conf 配置如下
limit_conn_zone $binary_remote_addr zone=addr:10m; #申請共享內存
server {listen 80;server_name abbila.com;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; }
}
通過curl 訪問
curl abbila.com/last/
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.18.0</center>
</body>
</html>
因為設置的limit_rate 是50個字節每秒,所以返回會特別慢,限制發生時設置的status是500所以同時發送兩個請求會返回
curl abbila.com/last/
<html>
<head><title>500 Internal Server Error</title></head>
<body>
<center><h1>500 Internal Server Error</h1></center>
<hr><center>nginx/1.18.0</center>
</body>
</html>
3.4. access 階段
當前模塊涉及到ngx_http_access_module模塊、ngx_http_auth_basic_module 、ngx_http_auth_request_module
3.4.1 ngx_http_access_module 模塊
當前模塊默認編譯到nginx中,可以使用–without-http_access_module
當前模塊有兩個指令allow、deny
· allow 指令
句法: allow address | CIDR | unix: | all;
默認: —
內容: http,server,location,limit_except
允許訪問指定的網絡或地址。如果unix:指定了特殊值,則允許訪問所有UNIX域套接字。
· deny 指令
句法: deny address | CIDR | unix: | all;
默認: —
內容: http,server,location,limit_except
拒絕訪問指定的網絡或地址。如果unix:指定了特殊值,則允許訪問所有UNIX域套接字。
3.4.2 ngx_http_auth_basic_module 模塊
當前模塊默認編譯進nginx中,可使用 --without-http_auth_basic_module 指令移除
該模塊允許通過使用“ HTTP基本身份驗證”協議驗證用戶名和密碼來限制對資源的訪問。當開啟這這個模塊后,如果通過瀏覽器訪問url時
會返回"401 Unauthorized" ,瀏覽器會返回一個用戶名密碼的對話框。
該模塊有兩個指令auth_basic、auth_basic_user_file
· auth_basic 指令
句法: auth_basic string | off;
默認: auth_basic off;
內容: http,server,location,limit_except
· auth_basic_user_file
句法: auth_basic_user_file file;
默認: —
內容: http,server,location,limit_except
這里面會用到 htpasswd(依賴httpd-tools安裝包,如果自己試驗可自行安裝),這個工具可以用來生成密碼文件,而 auth_basic_user_file 就依賴這個密碼文件。
使用方法:
htpasswd -c file -b user password
· 實戰:鑒權模塊
nginx.conf 配置文件內容,通過上面命令生成 abbila.pass 文件。
server {listen 80;server_name abbila.com;location / {auth_basic "close the web site";auth_basic_user_file abbila.pass;}
|
然后通過瀏覽器訪問nginx 地址就可以看到:
然后輸入賬號密碼就可以正常訪問頁面了。
3.4.3 ngx_http_auth_request_module 模塊
當前模塊沒有編譯到nginx中,可使用 --with-http_auth_basic_module 添加到nginx中。該模塊可以將客戶端輸入的用戶名、密碼 username:password 通過 Base64 編碼后寫入 Request Headers 中。例如:abbila:abbila -> Authorization: Basic YWJiaWxhMTphYmJpbGE= 然后通過第三方程序解碼后跟數據庫中用戶名、密碼進行比較,Nginx 服務器通過 header 的返回狀態判斷是否認證通過。當前模塊設計兩個指令auth_request、auth_request_set
· auth_request 指令
句法: auth_request uri | off;
默認: auth_request off;
內容: http,server,location
如果子請求返回2xx響應代碼,則允許訪問。如果返回401或403,則使用相應的錯誤代碼拒絕訪問。子請求返回的其他響應代碼都被視為錯誤。
· auth_request_set 指令
句法: auth_request_set $variable value;
默認: —
內容: http,server,location
授權成功后可以把賦值一些變量,可以包括鑒權請求的變量。
配置示例:
location /private/ {auth_request /auth;...
}location = /auth {proxy_pass https://auth.server.com/HttpBasicAuthenticate;#認證服務地址proxy_pass_request_body off;proxy_set_header Content-Length "";proxy_set_header X-Original-URI $request_uri;
}
根據認證服務地址返回的狀態碼來判斷是否可以訪問 /private/ 的請求。