Nginx 作為一款高效能的代理軟體,在工作中常被用做負載均衡器、正向反向代理。最近在看了一個關於 Nginx 的視訊,老師講得很不錯。筆者也記錄了一下整套課程的一些重點和自己的理解。
筆者學習習慣是先實戰後理論,所以本文主要偏實戰,希望讀者可以通過本文快速搭建 Nginx,並嘗試使用它的特性。關於 Nginx 的執行原理、核心模組、原始碼分析等內容可能要等下次了(其實是筆者學藝不精,暫時看不太懂),大家感興趣可以先看看其他優質文章。
思維導圖
下篇:https://segmentfault.com/a/11...
那我們開始吧!
準備工作
Nginx 是什麼這裡就不贅述了,相信大家都用過,Nginx 目前的發行版本如下:
- Nginx 開源版
- Nginx Plus:F5 基於 Nginx 開源版開發的商業版本
- Openresty:國人開發,整合了 lua 模組
- Tengine:淘寶基於開源版改造,經過雙十一驗證
本文主要講述開源版的 Nginx 和 Openresty 開源版
快速安裝
大家到 nginx.com 下載一下原始碼包,解壓編譯安裝
# 安裝依賴
$ yum install -y pcre pcre-devel
$ yum install -y zlib zlib-devel
# 解壓編譯
$ tar -zxf nginx-1.21.6.tar.gz
$ ./configure --prefix=/usr/local/nginx
$ make && make install
編寫一下 service 檔案,方便操作
$ cat <<EOF >> /usr/lib/systemd/system/nginx.service
[Unit]
Description=nginx - web server
After=network.target remote-fs.target nss-lookup.target
[Service]
Type=forking
PIDFile=/usr/local/nginx/logs/nginx.pid
ExecStartPre=/usr/local/nginx/sbin/nginx -t -c /usr/local/nginx/conf/nginx.conf
ExecStart=/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
ExecReload=/usr/local/nginx/sbin/nginx -s reload
ExecStop=/usr/local/nginx/sbin/nginx -s stop
ExecQuit=/usr/local/nginx/sbin/nginx -s quit
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF
$ systemctl daemon-reload
$ systemctl start nginx.service
$ ystemctl enable nginx.service
# 以後重啟 nginx 只需要
$ systemctl restart nginx
# 重新載入配置
$ systemctl reload nginx
配置檔案在 conf/nginx.conf
,簡化一下
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name localhost;
location / {
root html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
有沒有發現我的配置檔案比較工整,以前是在 SFTP 上記事本編輯,縮排需要手動調整特別麻煩,後面直接用 Vscode 的 nginx-formatter 格式化(後面會跟大家說怎麼安裝)。
nginx-formatter 通過先用特殊符號替換雙引號中的內容避免誤操作,通過正規表示式的方式去除空白字元,專案連結:
- nginx-config-formatter:https://github.com/slomkowski...
- nginxbeautifier:https://github.com/vasilevich...
對於最小配置的一些說明:
- worker_processes:Nginx 基於多程式 React 模型,啟動時會啟動一個 Master 和 N 個 Worker,這裡配置 Worker 個數,一般設定為 CPU 邏輯核心數 + 1
- worker_connections:Worker 是真正處理請求的程式,這個配置主要描述一個 Worker 可以處理的連線上限
- include:包含其他 nginx 配置檔案
- mine.type:配置了資源字尾名和響應體的 Content-Type 的對映關係
- sendfile:是否開啟零拷貝,只有在磁碟的內容會觸發 sendfile
- server:定義一個服務
- listen:監聽埠
- server_name:可以配置主機名或者域名
- location:匹配的 URL 字尾,root 指的是檔案系統路徑,index 是歡迎頁
- root:定義根目錄
- index:歡迎頁
儲存後,使用 curl 命令簡單測試一下是否成功
$ curl localhost
curl 是一個簡單的 HTTP 請求工具,常用的幾個引數如下:
- -H "Conten-Type: application/josn": 新增請求頭
- -I:顯示響應頭
- -e:設定 Reference
- -X 設定請求方式
- --proxy:設定代理
- -sSL:跟蹤重定向
詳細文件:https://itbilu.com/linux/man/...
測試環境搭建
為了更好的測試,這裡就直接使用 Flask(容器執行) 模擬上游服務,宿主機作為 Nginx,網路拓撲如下:
這張圖是使用虛擬畫板畫的:https://board.oktangle.com/
$ mkdir ~/flask-demo
$ cd ~/flask-demo
建立app.py
from flask import Flask
from flask import render_template
from flask import request
import socket
app = Flask(__name__)
@app.route("/")
def index():
return render_template('index.html', data={
'hostname': socket.gethostname(),
'num': request.args.get('num', 'None')
})
建立 templates 目錄,新建 index.html 檔案,內容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nginx 實戰</title>
</head>
<body>
<h3>當前伺服器 hostname: {{ data.hostname }}</h3>
<h3>從 URL 中獲取 引數 num: {{ data.num }}</h3>
<p></p>
<img src="/static/img/demo.png" alt="防盜鏈測試蹄片">
</body>
</html>
本來想輸出 IP 的,但是沒找到 flask 從 IP 報文獲取源地址的 API,拿主機名代替一下
回到主目錄,建立 Dockerfile
FROM python:3.8-slim-buster
WORKDIR /python-docker
COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt
COPY . .
CMD [ "python3", "-m" , "flask", "run", "--host=0.0.0.0", "--port=80"]
建立依賴檔案 requirements.txt
Flask==2.0.3
最終目錄結構如下:
│ app.py
│ Dockerfile
│ requirements.txt
│
├─static
│ └─img
│ demo.png
│
├─templates
└─ index.html
在 Linux 中使用下面命令進行測試環境搭建:
$ docker build -t flask-demo:0.1 .
$ docker network create --driver bridge --subnet 172.20.0.0/24 --gateway 172.20.0.1 flask-cluster
# 啟動三個映象
$ for i in $(seq 1 3); do docker run --name flask-demo-$i --hostname flask-demo-$i --restart=always --network flask-cluster --ip 172.20.0.1$i -d flask-demo:0.1; done;
# 開啟 nginx 埠
$ firewall-cmd --add-port=80/tcp --permanent
$ firewall-cmd --reload
# 下面命令用於刪除容器
for i in `seq 1 3`; do docker rm -f flask-demo-$i; done
VsCode配置
使用 Vsocde 中的外掛會有更好的學習體驗,安裝下面外掛,Remote SSH 連線虛擬機器或遠端主機,以後修改配置就非常方便了。
- nginxbeautifier:Nginx 配置檔案格式化工具(感興趣可以閱讀一下程式碼,300多行)
- vscode-nginx-conf:VSCode 配置檔案程式碼補全、連線到官方文件外掛
- luaHelper:Lua 指令碼語法補全、高亮外掛
- Remote SSH:連線遠端主機外掛
Nginx 核心功能
Nginx 的核心功能主要圍繞著 http - server 模組進行描述。在 server 模組下,server_name 可以配置主機名和域名,Nginx 在解析 HTTP 報文中的 header —— Host
來判斷進入哪一個 server 塊。這裡可以使用萬用字元,優先順序自上而下。基於此可以實現
多租戶二級域名:
比如
xxx.domain.com
,ServerName 配置*.domain.com
,後端伺服器解析 Http 報文中的 Host,根據 xxx 展示對應使用者資訊,當然這樣子不太安全。- 短網站:這個需要和後面介紹 URLRewrite 配合使用
我們可以自定義一個 Server 實現 HTTP DNS,但這需要我們學習到後面 lua 的時候才好實現。
HTTPDNS:由於 DNS 汙染、本地 DNS 服務不夠智慧、DNS 根據自己而非客戶返回最近伺服器錯誤等問題,我們可以自己基於 HTTP 協議搭建 DNS 伺服器,但這需要客戶端自己實現發起解析的邏輯,但是瀏覽器不支援 HttpDNS,解決方式是本地啟動一個 DNS 伺服器結合 FakeIP 解決,這個可以參考我之前寫的 Clash 代理工具解析。
server_name
配置方式:
完整配置
server_name domain1.com domain2.com;
正則配置
server_name domain1.~^[0-9]+\.domain.com;
萬用字元配置
server_name domain1.*
配置完 server_name
後,接著就要編寫匹配規則 location
了,目前 server 已經匹配 URL 上的 host 部分,剩下的 uri 則由 location 進行匹配。你可以選擇讓 Nginx 幫助你將請求轉發出去(正反向代理),也可以讓 Nginx 返回本地檔案(資源服務)。
正反向代理
為了保證伺服器的安全性,暴露統一網路入口,我們會使用 Nginx 作為服務的入口,所有流量都會經過這個入口進入上游服務。
這種方式稱為反向代理,客戶端不知道服務端的具體地址,只關注與代理伺服器的通訊。
正向代理則更常用於跨網段通訊,比如機房 A 中的主機不能訪問機房 B 的主機,需要通過能夠同時於機房 A、機房 B 中主機通訊的代理伺服器作為跳板進行跨機房訪問。
其實正向代理和反向代理沒有區別,只是通常情況下反向代理是對請求發起方無感,正向代理對發起者有感(甚至需要發起者自己去代理中配置)
這裡的 Nginx 也被稱為閘道器,由於閘道器數量比較少,容易出現瓶頸,比如下載大檔案的場景,Response 都要經過閘道器。LVS 的 DR 模式開放上游伺服器的 OUTPUT 防火牆,使得入棧經過閘道器,出棧由上游服務直接返回客戶端。
反向代理配置
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://flask;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
upstream flask {
server 172.20.0.11:80;
server 172.20.0.12:80;
server 172.20.0.13:80;
}
proxy_pass:會將請求內容轉發到 upstream 上有服務上
正向代理配置
正向代理其實就是 HTTP 報文寫著需要訪問的目標地址(header 中的 hsot 和 uri),但將報文發給了代理伺服器。所以如果你希望 Nginx 充當代理,只需要進行下面的配置
location / {
proxy_pass $scheme://$host$request_uri;
resolver 8.8.8.8;
}
這是基於七層代理,四層需要引入 stream 模組,七層正向代理只支援 HTTP 協議,如果需要支援 HTTPS 的話需要使用下面模組
- ngx_http_proxy_connect_module:https://github.com/chobits/ng...
負載均衡
在負載不斷增加的情況下,我們很有可能對上游服務進行擴容,即增加更多的節點。這時候就需要 Nginx 負責將請求儘量均勻的分攤到上游服務上。Nginx 提供了好幾種演算法:
Round Robin 輪詢
雨露均沾,一人一下。但很多時候物理機或者虛擬機器的配置不一樣,我們可以為資源較多的主機配置更高的權重。
upstream flask {
server 172.20.0.11:80 weight=10;
server 172.20.0.12:80 weight=2;
server 172.20.0.13:80 weight=1;
}
這裡會大概以 10 : 2 : 1 分配流量,處理 weight 還有幾個可以設定的引數:
- down:不參與排程
- backup:除了 backup 以外的伺服器都無法提高服務時被排程
這兩個一般不會使用,比如 backup,我們試圖希望服務出現故障後備用機器可以頂替,但如果是因為程式碼邏輯不正確導致的當機,理論上備用機器上的程式碼也是有問題的,所以切換後很可鞥也會當機。
ip_hash
輪詢演算法有個問題就是每次請求必須是無狀態的,即不能使用單個伺服器的 session 暫存資料。ip_hash 演算法根據客戶端 IP 進行雜湊,所以每次請求都會打到同一個伺服器,但該演算法的問題在於:
- 由於手機移動過程中 IP 地址會發生改變,如果上游服務使用了 session 會導致失效
- 可能出現大部分 IP 地址雜湊到同一臺上遊服務
- 對於內網使用者數量較多,比如考試系統、ERP 系統,出口 IP 地址只有幾個,容易導致流量傾斜
least_conn
最少連線,即將流量分給流量最小的主機。這個也不常用,之所以會有流量傾斜很可能是服務權重具備差異。
url_hash
根據 URL 進行雜湊,適合資源定位,比如多個檔案散落在不同服務中,可以通過 URL 中的檔名進行 hash 找到目標節點。缺點在於:基於 url_hash 上游服務只能部署在一臺主機上
fair
基於響應時間的排程策略,需要依賴第三方外掛,這種方式不常用,因為響應時間和很多因素有關係,會導致交換機過熱流量傾斜。
綜上,我們大多數情況下都會使用 RR 輪詢,如果上游服務是有狀態的,可以選擇 hash,更特殊的如果系統正在進行灰度釋出,可以自己編寫 lua 指令碼動態調整。
資源服務
如果你選擇讓 Nginx 作為資源伺服器,比如你希望將前端靜態資源、圖片前置到 Nginx 伺服器,上游服務只負責業務處理,那麼你很能需要進行動靜分離的配置
server {
location / {
proxy_pass http://127.0.0.1:8080;
root html;
index index.html index.htm;
}
location /css {
root /usr/local/nginx/static;
index index.html index.htm;
}
location /images {
root /usr/local/nginx/static;
index index.html index.htm;
}
location /js {
root /usr/local/nginx/static;
index index.html index.htm;
}
}
我們也可以使用正則匹配的方式
location ~*/[js|css|images] {
root /usr/local/nginx/static;
index index.html index.htm;
}
對於 location 的書寫有通用模式、精準模式和正則模式:
- / 通用匹配,任何請求都會匹配到。
- = 精準匹配,不是以指定模式開頭
- ~ 正則匹配,區分大小寫
- ~* 正則匹配,不區分大小寫
- ^~ 非正則匹配,匹配以指定模式開頭的location
在同一個配置檔案中也有一些規定:
- 多個正則按書寫順序進行匹配,fallthrough
- 非正則也是按書寫順序匹配,找到匹配度最高的規則執行
- 正則模式優先順序高於非正則,優先順序順序為 “=”匹配 > “^~”匹配 > 正則匹配 > 普通
比如我們可以將 Flask 中的圖片刪除,在 Nginx 中進行下面配置
$ docker exec flask-demo-1 rm /python-docker/static/img/demo.png
$ docker exec flask-demo-2 rm /python-docker/static/img/demo.png
$ docker exec flask-demo-3 rm /python-docker/static/img/demo.png
編輯配置檔案
location ~*/static {
root flask-demo;
index index.html index.htm;
}
將圖片上傳到 $NGINX_DIR/html/flask-demo/static/img
中。這裡也可以使用 alias。
root 和 alias 的區別:
- root:設定根目錄,nginx 會在根目錄,按照 URL 的層級尋找檔案,root 保留匹配上的部分進行搜尋
alias:和 root 類似,但會捨棄匹配上的部分進行搜尋
location ^~ /static { alias flask-demo/static/; }
使用 alias 時,當 nginx 匹配上 location 後,會捨棄匹配上的文字,比如上面配置的
/static
,當訪問http://localhost/static/img/demo.png
時,會保留/img/demo.png
,如果配置為/static/
則保留下img/demo.png
,保留下來的部分會拼接到`alias
後面,所以這裡 alias 最後加了 /,如果 location 保留了 /,這裡就不要加。如果填寫相對路徑,父目錄是 NGINX_DIR
URI 重寫
現在我們已經可以完整配置 Nginx 了,但有一些場景我們需要對 URI 進行重寫:
- 防盜鏈:有時候我們不希望我們的圖片、JS 被其他網站直接引用
- 隱藏 URL:讓使用者看不出請求地址,防止惡意訪問
- 新增檔案字尾:比如在連線後新增 .html 字尾,搜尋引擎的蜘蛛會識別並收集裡面的內容
只需要在 location
中新增配置
rewrite ^/([0-9]+).html$ /?num=$1 break;
# break 表示使用當前規則
# last 表示匹配下一條規則,如果沒有下一條則使用當前規則重寫
# redirect 表示301臨時重定向,瀏覽器會重新訪問重寫後的 URL
# permanent 永久重定向,301 和 302 主要是給搜尋引擎的蜘蛛識別
比如
location / {
rewrite ^/([0-9]+).html$ /?num=$1 break;
proxy_pass http://flask;
}
訪問 localhost/10086.html
,響應體的 html 中可以讀取到 arg,正因為進行了 URI 重寫
防盜鏈
Gitee 圖床之前在別的網站引用會返回一個 Gitee 的圖示,防止其他網站濫用圖床,這裡使用到了防盜鏈。當我們使用瀏覽去訪問一個網頁後,再次向伺服器獲取圖片、js 等資源時會自動帶上 Referer 頭。
在 Nginx 中我們可以進行配置,檢測 Referer是否合法
valid_referers none | blocked | server_names | strings ....;
- none:允許沒有攜帶 Referer 頭的請求
- blocked:請求頭中存在 Referer欄位,但值不是以"http(s)://"開頭的字串
我們可以為 demo.png 設定防盜鏈
location ~* ^.+\.*..(jpeg|gif|png|jpg) {
valid_referers 192.168.80.154;
if ($invalid_referer) {
rewirte ^/ /invalid.png
}
}
location /static {
valid_referers 192.168.80.154;
if ($invalid_referer) {
return 403;
}
root flask-demo;
index index.html index.htm;
}
location = /invalid.png {
root flask-demo;
}
嘗試用瀏覽器訪問一下 192.168.80.154/static/demo.png
會發現返回的是 invalid.png
高可用配置
單臺 Nginx 作為閘道器容易出現單點故障,所以需要為其準備一臺從機進行 backup。使用 VIP + keepalived 的方式,主節點的 keepalived 通過程式狀態或者指令碼監控 Nginx 服務,如果 Nginx 當機則主動放棄 Master 角色,或者從機的 keepalived 發現主節點傳送 VRRP Report 超時也會升級為 Master。
- keepalived 官網:https://www.keepalived.org/in...
- VRRP 詳解:https://cshihong.github.io/20...
- keepalived 進行服務監控:https://www.wumingx.com/linux...
- keepalived 腦裂問題:https://www.zhihu.com/questio...
簡單概括一下,keepalived 基於 VRRP 協議,VRRP 協議能夠將多個裝置虛擬成一臺裝置對外暴露服務。VRRP 協議中存在 Master 和 Backup,Master 會不斷通過組播方式通告當前持有的虛擬 IP,並響應區域網內的 ARP 請求。Backup 則維護定時器,只要在一定時間內接收不到 Master 的通告報文,或是 Master 的優先順序小於當前 Backup 的優先順序(相等則比較 IP 地址大小,大者優先)則升級為 Master,舊 Master 接收到通告也會降級為 Backup。
這裡會有幾個問題:
如果不通暢,Backup定時器超時升級為 Master 後受到舊 Master 的通告,發現自己優先順序不高又回到 Backup,造成頻繁切換。可以設定一下切換延遲,延遲時間設定需要權衡。
按照 keepalived 並進行配置
$ yum -y install openssl-devel
$ ./configure --prefix=/usr/local/keepalived
$ make && make install
$ vim etc/keepalived/keepalived.conf
# Master 節點配置
global_defs {
router_id node1
}
vrrp_instance nginx-demo {
state MASTER
interface ens33
virtual_router_id 51
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass admin
}
virtual_ipaddress {
192.168.80.220 # 虛IP
}
}
# Backup 節點配置
global_defs {
router_id node2
}
vrrp_instance nginx-demo {
state BACKUP
interface ens33
virtual_router_id 51
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass admin
}
virtual_ipaddress {
192.168.80.220 # 虛IP
}
}
$ systemctl start keepalived
Https 配置
https 是二十一世紀最偉大的發明之一,通告非對稱加密,公鑰加密私鑰解密、私鑰加密公鑰解密、公鑰加密公鑰無法解密、CA 機構認證等方式保證通訊內容不被篡改、不被偽造、不被竊聽
整個 https 原理這裡就不贅述了,這裡有一點想說的是,比如我們的伺服器和域名在阿里雲上購買,向 CA 申請證照時需要認證當前伺服器和當前域名都是同一個使用者的,阿里雲使用 DNS 自動檢測的方式,也就是讓使用者修改 DNS 記錄到一個指定地址,CA 認證伺服器訪問看看是否成功(這一步也是自動的)。除了這種方式,還可以讓使用者自己上傳一個檔案到伺服器中並暴露訪問地址,CA 通過訪問該地址確認伺服器的歸屬人。
Nginx 上配置也比較簡單,將證照和私鑰上傳並配置即可
server {
# 伺服器埠使用443,開啟ssl, 這裡ssl就是上面安裝的ssl模組
listen 443 ssl;
# 域名,多個以空格分開
server_name hack520.com www.hack520.com;
# ssl證照地址
ssl_certificate /usr/local/nginx/cert/ssl.pem; # pem檔案的路徑
ssl_certificate_key /usr/local/nginx/cert/ssl.key; # key檔案的路徑
# ssl驗證相關配置
ssl_session_timeout 5m; #快取有效期
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; #加密演算法
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #安全連結可選的加密協議
ssl_prefer_server_ciphers on; #使用伺服器端的首選演算法
location / {
root html;
index index.html index.htm;
}
}
server {
listen 80;
server_name hack520.com www.hack520.com;
return 301 https://$server_name$request_uri;
}
以上就是實戰上篇所有內容啦,希望大家能夠通過本文快速配置 Nginx 並使用,下篇會介紹一下高階模組、二次開發、快取等功能,敬請期待!