Nginx 實戰核心知識點整理(上)

Rhythm_2019發表於2022-07-05

Nginx 作為一款高效能的代理軟體,在工作中常被用做負載均衡器、正向反向代理。最近在看了一個關於 Nginx 的視訊,老師講得很不錯。筆者也記錄了一下整套課程的一些重點和自己的理解。

筆者學習習慣是先實戰後理論,所以本文主要偏實戰,希望讀者可以通過本文快速搭建 Nginx,並嘗試使用它的特性。關於 Nginx 的執行原理、核心模組、原始碼分析等內容可能要等下次了(其實是筆者學藝不精,暫時看不太懂),大家感興趣可以先看看其他優質文章。

思維導圖
思維導圖
那我們開始吧!

準備工作

Nginx 是什麼這裡就不贅述了,相信大家都用過,Nginx 目前的發行版本如下:

  1. Nginx 開源版
  2. Nginx Plus:F5 基於 Nginx 開源版開發的商業版本
  3. Openresty:國人開發,整合了 lua 模組
  4. 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 通過先用特殊符號替換雙引號中的內容避免誤操作,通過正規表示式的方式去除空白字元,專案連結:

  1. nginx-config-formatter:https://github.com/slomkowski...
  2. 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,網路拓撲如下:
flask-demo網路拓撲

這張圖是使用虛擬畫板畫的: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 塊。這裡可以使用萬用字元,優先順序自上而下。基於此可以實現

  1. 多租戶二級域名:

    比如 xxx.domain.com,ServerName 配置 *.domain.com,後端伺服器解析 Http 報文中的 Host,根據 xxx 展示對應使用者資訊,當然這樣子不太安全。

  2. 短網站:這個需要和後面介紹 URLRewrite 配合使用

我們可以自定義一個 Server 實現 HTTP DNS,但這需要我們學習到後面 lua 的時候才好實現。

HTTPDNS:由於 DNS 汙染、本地 DNS 服務不夠智慧、DNS 根據自己而非客戶返回最近伺服器錯誤等問題,我們可以自己基於 HTTP 協議搭建 DNS 伺服器,但這需要客戶端自己實現發起解析的邏輯,但是瀏覽器不支援 HttpDNS,解決方式是本地啟動一個 DNS 伺服器結合 FakeIP 解決,這個可以參考我之前寫的 Clash 代理工具解析。

server_name 配置方式:

  1. 完整配置

    server_name  domain1.com domain2.com;
  2. 正則配置

    server_name  domain1.~^[0-9]+\.domain.com;
  3. 萬用字元配置

    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 的話需要使用下面模組

負載均衡

在負載不斷增加的情況下,我們很有可能對上游服務進行擴容,即增加更多的節點。這時候就需要 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 還有幾個可以設定的引數:

  1. down:不參與排程
  2. backup:除了 backup 以外的伺服器都無法提高服務時被排程

這兩個一般不會使用,比如 backup,我們試圖希望服務出現故障後備用機器可以頂替,但如果是因為程式碼邏輯不正確導致的當機,理論上備用機器上的程式碼也是有問題的,所以切換後很可鞥也會當機。

ip_hash
輪詢演算法有個問題就是每次請求必須是無狀態的,即不能使用單個伺服器的 session 暫存資料。ip_hash 演算法根據客戶端 IP 進行雜湊,所以每次請求都會打到同一個伺服器,但該演算法的問題在於:

  1. 由於手機移動過程中 IP 地址會發生改變,如果上游服務使用了 session 會導致失效
  2. 可能出現大部分 IP 地址雜湊到同一臺上遊服務
  3. 對於內網使用者數量較多,比如考試系統、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

在同一個配置檔案中也有一些規定:

  1. 多個正則按書寫順序進行匹配,fallthrough
  2. 非正則也是按書寫順序匹配,找到匹配度最高的規則執行
  3. 正則模式優先順序高於非正則,優先順序順序為 “=”匹配 > “^~”匹配 > 正則匹配 > 普通

比如我們可以將 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 進行重寫:

  1. 防盜鏈:有時候我們不希望我們的圖片、JS 被其他網站直接引用
  2. 隱藏 URL:讓使用者看不出請求地址,防止惡意訪問
  3. 新增檔案字尾:比如在連線後新增 .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 基於 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 並使用,下篇會介紹一下高階模組、二次開發、快取等功能,敬請期待!

相關文章