前端er須知的Nginx技巧

  • 2019 年 12 月 6 日
  • 筆記

前述

Nginx 對於大多數開發者來說不算陌生,企業團隊用它來搭建請求網關,我們私下用它 「科學上網」(價值觀警告)。但對於前端 er 來說,平日里開發大多時候都只是專註於業務,根本不需要也沒機會涉及到 Nginx 這一塊的內容,也就導致我們也對它的了解少之甚少。隨著 serverless 孕育普及,越來越多的人相信,不需要掌握任何運維知識,也能簡單快速地實現自己的技術 idea。

然而事實上並不是這樣的,Node 的興起讓前端工程師開始涉足後端領域,我們可以獨立維護一些 BFF 服務,即使這只是一些簡單的應用,也需要你掌握一定的運維技巧。另一方面,在快速變革的軟體開發體系下,不同職責之間的部分邊界變得越來越模糊,DevOps 理念的深入,也讓我們不得不把目光投嚮應用運維,開始思考在新體系下如何構建一體化工程。所以,懂得一些簡單易用的 Nginx 技巧,對於前端開發者來說,是非常必要的。

所謂 「技多不壓身」,在你還在思考學不學的時候,有些人已經學完了。

Nginx 是什麼

Nginx 是一個開源且高性能、可靠的 http 中間件,代理服務。Nginx(發音同 engine x)是一個 Web 伺服器,也可以用作反向代理,負載平衡器和 HTTP 快取。

這是個經典的概述。Nginx 的 「高性能」 主要體現在支援海量並發的 webserver 服務,而 「可靠」 則意味著穩定性高、容錯率大,同時,由於 Nginx 架構基於模組,我們大可以通過內置模組和第三方模組的自由組合,來構建適配自身業務的 Nginx 服務。正因如此,Nginx 才備受青睞,得以廣泛出現在各種規模的企業團隊中,成為技術體系的重要參與者。

對於 Nginx,我們可以深入探索的有很多,但對前端開發者而言,能夠熟悉掌握和編寫 Nginx 的核心配置文件 nginx.conf,其實已經能解決 80% 的問題了。

Docker 快速搭建 Nginx 服務

純手工安裝 Nginx 經典的步驟是 「四項確認、兩項安裝、一次初始化」,過程繁瑣而且容易踩坑,但是利用 Docker,我們完全沒必要這麼麻煩。Docker 是一個基於 Golang 的開源的應用容器引擎,支援開發者打包他們的應用以及依賴包到一個輕量可移植的沙箱容器中,因此我們可以使用 Docker 輕而易舉地在我們本地搭建一個 Nginx 服務,完全跳過安裝流程。關於 Docker 這裡不做細講,有興趣的同學可以自行了解[1]。

為了簡便演示,我們使用更加高效的 Docker-Compose 來構建我們的 Nginx 服務。Docker-Compose 是 Docker 提供的一個命令行工具,用來定義和運行由多個容器組成的應用。使用 Docker-Compose,我們可以通過 YAML 文件聲明式的定義應用程式的各個服務,並由單個命令完成應用的創建和啟動。

要完成接下來的操作,首先你需要安裝 Docker,不同的作業系統有不同的安裝方式[2]。

環境就位後,我們新建一個項目 nginx-quick,在根目錄新建一個 docker-compose.yml 文件,這是 Docker-Compose 的配置文件:

version: "3"    services:    nginx: # 服務的名稱      image: nginx      volumes: # 文件夾映射        - ./nginx/nginx.conf:/etc/nginx/nginx.conf # nginx 配置文件      ports: # 埠轉發        - "8080:80"

我們定義了一組服務 nginx,用於啟動一個 docker 容器。容器對應的鏡像是 nginx,在容器內 Nginx 服務的啟動埠是 80,外部訪問埠是 8080,同時,我們把本地自定義的 Nginx 配置文件 ./nginx/nginx.conf 對應同步到容器中的 /etc/nginx/nginx.conf 路徑。

新建 nginx/nginx.conf

# 全局配置  user  nginx;         # 配置用戶或者組  worker_processes  1; # 允許生成的進程數    error_log  /var/log/nginx/error.log warn; # 錯誤日誌路徑,warn 代表日誌級別,級別越高記錄越少  pid        /var/run/nginx.pid;            # Nginx 進程運行文件存放地址    events {    accept_mutex on;          # 設置網路連接序列化,防止驚群現象發生    multi_accept on;          # 設置一個進程是否同時接受多個網路連接    worker_connections  1024; # 每個進程的最大連接數,因此理論上每台 Nginx 伺服器的最大連接數 = worker_processes * worker_connections  }    # HTTP 配置  http {    include       /etc/nginx/mime.types;    # 文件擴展名與文件類型映射表    default_type  application/octet-stream; # 默認文件類型      log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '                      '$status $body_bytes_sent "$http_referer" '                      '"$http_user_agent" "$http_x_forwarded_for"'; # 日誌格式      access_log  /var/log/nginx/access.log  main; # 訪問日誌路徑      sendfile        on; # 允許 sendfile 方式傳輸文件      keepalive_timeout  65; # 連接超時時間      server {      listen       80;         # 監聽埠      server_name  localhost;  # 監聽地址        location / {                    # 請求的url過濾,正則匹配        root   /usr/share/nginx/html; # 根目錄        index  index.html index.htm;  # 默認頁      }    }  }

這是一份最基礎的 Nginx 配置,相關配置項及對應詳細的釋義可以看看注釋,這裡我們簡單配置了一個 localhost:80 的訪問監聽(注意這裡的 localhost 不是本地,是容器內部)。

執行 docker-compose up -d 創建服務,訪問 localhost:8080 可以看到 Nginx 的默認主頁 Welcome to nginx!

執行 docker exec -it nginx-quick_nginx_1 bash 進入容器內部,再執行 cat /etc/nginx/nginx.conf,可以看到我們自定義的 Nginx 配置文件成功覆蓋了默認的 Nginx 配置。

Nginx 的 HTTP 配置

HTTP 配置是 Nginx 配置最關鍵,同時也是 Nginx 實用技巧中最常涉及的部分。Nginx 的 HTTP 配置主要分為三個層級的上下文:http — server — location。

http

http 主要存放協議級別的配置,包括常用的諸如文件類型、連接時限、日誌存儲以及數據格式等網路連接配置,這些配置對於所有的服務都是有效的。

server

server 是虛擬主機配置,主要存放服務級別的配置,包括服務地址和埠、編碼格式以及服務默認的根目錄和主頁等。部分特殊的配置各級上下文都可以擁有,比如 charest (編碼格式) access_log (訪問日誌)等,因此你可以單獨指定該服務的訪問日誌,如果沒有則默認向上繼承。

location

location 是請求級別的配置,它通過 url 正則匹配來指定對某個請求的處理方式,主要包括代理配置、快取配置等。location 配置的語法規則主要為:

# location [修飾符] 匹配模式 { ... }  location [=|~|~*|^~] pattern { ... }

1)沒有任何修飾符時表示路徑前綴匹配,下邊這個例子,匹配 http://www.jd.com/testhttp://www.jd.com/test/may

server {    server_name www.jd.com;    location /test { }  }

2)= 表示路徑精確匹配,下邊這個例子,只匹配 http://www.jd.com/test

server {    server_name www.jd.com;    location = /test { }  }

3)~ 表示正則匹配時要區分大小寫,下邊這個例子,匹配 http://www.jd.com/test,但不匹配 http://www.jd.com/TEST

server {    server_name www.jd.com;    location ~ ^/test$ { }  }

4)~* 表示正則匹配時不需要區分大小寫,下邊這個例子,既匹配 http://www.jd.com/test,也匹配 http://www.jd.com/TEST

server {    server_name www.jd.com;    location ~* ^/test$ { }  }

5)^~ 表示如果該符號後面的字元是最佳匹配,採用該規則,不再進行後續的查找。

Nginx location 有自己的一套匹配優先順序:

  • 先精確匹配 =
  • 再前綴匹配 ^~
  • 再按文件中順序的正則匹配 ~~*
  • 最後匹配不帶任何修飾的前綴匹配

下邊這個例子,http://www.jd.com/test/may 雖然命中了兩個 location 規則,但是由於 ^~ 匹配優先順序高於 ~* 匹配,所以將優先使用第二個 location。

server {    server_name www.jd.com;    location ~* ^/test/may$ { }    location ^~ /test { }  }

Nginx 實用技巧

了解完一些 Nginx 的基礎語法,我們再來看看在前端人手裡,Nginx 可以有哪些實用的場景及技巧。

正向代理

代理轉發是 Nginx 最為普遍的使用場景,正向代理就是其中一種。

客戶端通過訪問一個代理服務,由它將請求轉發到目標服務,再接受目標服務的請求響應並最終返回給客戶端,這就是一個代理的過程。「科學上網」 就是一種典型的正向代理,在這個過程中,Nginx 就充當了代理中介的角色。

我們在根目錄下新建 web/ 目錄,添加一個 index1.html,作為目標服務的訪問主頁:

<!DOCTYPE html>  <html lang="en">  <head>    <meta charset="UTF-8">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <meta http-equiv="X-UA-Compatible" content="ie=edge">    <title>Web服務主頁</title>    <style>    p {      margin: 80px 0;      text-align: center;      font-size: 28px;    }    </style>  </head>  <body>    <p>這是 Web1 服務的首頁</p>  </body>  </html>

修改 docker-compose.yml,新增一個 Nginx 服務 web1 作為目標服務,用自定義的 html 去覆蓋默認的主頁 html,同時,我們用 link: - web1:web1 建立起代理服務 nginx 和目標服務 web1 之間的容器連接:

version: "3"    services:    nginx: # 服務的名稱      image: nginx      links:        - web1:web1      volumes: # 文件夾映射        - ./nginx/nginx.conf:/etc/nginx/nginx.conf # nginx 配置文件      ports: # 埠轉發        - "8080:80"    web1:      image: nginx      volumes:        - ./web/index1.html:/usr/share/nginx/html/index.html      ports:        - "80"

修改 Nginx 的 location 配置,利用 proxy_pass 屬性讓主路徑訪問請求轉發到目標服務 web1

// ...  location / {    proxy_redirect off;    proxy_pass http://web1; ## 轉發到web1  }  // ...

重啟容器,訪問 localhost:8080,可以發現代理服務成功將我們的請求轉發到目標 Web 服務:

負載均衡

代理還包括反向代理,我們業務中最常提到的負載均衡,就是一種典型的反向代理。當網站的訪問量達到一定程度後,單台伺服器不能滿足用戶的請求時,就需要用多台伺服器構建集群服務了,此時多台伺服器將以合理的方式分擔負載,避免出現某台伺服器負載高宕機而某台伺服器閑置的情況。

利用 Nginx 的 upstream 配置,我們可以簡單地實現負載均衡。負載均衡需要多個目標服務,因此我們在 web 目錄下新建 index2.htmlindex3.html,作為新增服務的訪問主頁。

修改 docker-compose.yml,新增兩個服務 web2web3,並建立容器連接:

# ...    services:    nginx: # 服務的名稱      # ...      links:        # ...        - web2:web2        - web3:web3      # ...      web2:      image: nginx      volumes:        - ./web/index2.html:/usr/share/nginx/html/index.html      ports:        - "80"    web3:      image: nginx      volumes:        - ./web/index3.html:/usr/share/nginx/html/index.html      ports:        - "80"

nginx.conf 中,我們創建了一個 upstream 配置 web-appweb-app 配置了三個目標服務,因此我們的請求將經由 web-app 代理到目標服務。Nginx 自帶的負載均衡策略有多種,包括默認的輪詢方式、權重方式、依據 IP 分配的 ip_hash 方式以及最少連接的 least_conn 方式等,採取哪種策略需要根據不同的業務和並發場景而定,這裡我們使用 least_conn 策略來處理請求的分發。

// ...  upstream web-app {    least_conn;   # 最少連接,選取活躍連接數與權重weight的比值最小者,為下一個處理請求的server    server web1 weight=10 max_fails=3 fail_timeout=30s;    server web2 weight=10 max_fails=3 fail_timeout=30s;    server web3 weight=10 max_fails=3 fail_timeout=30s;  }    server {    listen       80;         # 監聽埠    server_name  localhost;  # 監聽地址      location / {      proxy_redirect off;      proxy_pass http://web-app; ## 轉發到web-app    }  }  // ...

重新啟動容器,可以發現多次請求時,代理服務都轉發到了不同的目標 Web 服務:

Server-side Include

Server-side Include(簡稱 SSI)是一種簡單的解釋型服務端腳本語言,是指在頁面被獲取時,伺服器端能夠進行 SSI 指令解析,對現有 HTML 頁面增加動態生成的內容。SSI 是早期 Web 實現模組化的一個重要手段,適用於多種運行環境,且解析效率比 JSP 高,目前仍然在一些大型網站中廣泛應用。

在 HTML 中使用 SSI 的格式就像這樣:

<!--#include virtual="/global/foot.html"-->

一行注釋,通過服務端的 SSI 解析,會被置換成 /global/foot.html 的內容,virtual 可以是絕對路徑,也可以是相對路徑。

Nginx 可以簡單快速地支援 SSI,讓你的頁面實現動態引入其他 HTML 內容。我們在 web 目錄下新建一個 HTML 頁面片 sinclude.html

<style>  * {    color: red;  }  </style>

修改 web/index1.html,加上 SSI 指令,引入頁面片 ./sinclude.html

<head>    <!-- ... -->    <!--#include virtual="./sinclude.html"-->  </head>

修改 docker-compose.yml,把 sinclude.html 也放到 web 服務的訪問根目錄下:

version: "3"    services:    nginx: # 服務的名稱      image: nginx      links:        - web1:web1      volumes: # 文件夾映射        - ./nginx/nginx.conf:/etc/nginx/nginx.conf # nginx 配置文件      ports: # 埠轉發        - "8080:80"    web1:      image: nginx      volumes:        - ./web/index1.html:/usr/share/nginx/html/index.html        - ./web/sinclude.html:/usr/share/nginx/html/sinclude.html      ports:        - "80"

最後在 nginx.conf 中簡單配置以下兩個屬性,開啟 Nginx 的 SSI 支援,其中 ssi_silent_errors 表示處理 SSI 文件出錯時需要輸出錯誤提示:

location / {    ssi on;    ssi_silent_errors on; # 處理 SSI 文件出錯時輸出錯誤提示,默認 off      proxy_redirect off;    proxy_pass http://web1; ## 轉發到web1  }

效果如下,Nginx 成功解析 SSI 指令,並將頁面片插入到 HTML 頁面中:

需要注意的是,如果這裡使用了反向代理,存在多個 web 服務,那麼請保證每一個 web 服務都存在 sinclude.html 文件並且路徑相同,因為獲取 index.html 和獲取 sinclude.html 是兩趟分發,除非使用了 ip_hash 策略,否則就有可能轉發到兩個不同的服務上,導致獲取不到頁面片文件。

GZIP 壓縮

HTTP 傳輸主要以文本為主,其中大量是一些靜態資源文件,包括 JS / CSS / HTML / IMAGE 等。GZIP 壓縮可以在傳輸的過程中對內容進行壓縮,減少頻寬壓力的同時提高用戶訪問速度,是一個有效的 Web 頁面性能優化手段。

Nginx 利用 gzip 屬性配置來開啟響應內容的 GZIP 壓縮:

location / {    # ...    gzip on;    gzip_min_length 1k; # 大於1K的文件才會壓縮      # ...  }

gzip_min_length 指定接受壓縮的最小數據大小,以上是小於 1K 的不予壓縮,壓縮後的請求響應頭中多了 Content-Encoding: gzip。我們可以給 HTML 文件中多放點內容,這樣才能讓壓縮效果更加明顯,下邊是 GZIP 開啟前和開啟後的效果對比:

1)壓縮前,HTML 大小 3.3 KB

2)開啟 GZIP 壓縮後,HTML 大小 555 B

防盜鏈

某些情況下我們不希望自己的資源文件被外部網站使用,比如有時候我會把 JD 圖片服務上的圖片鏈接直接複製到 GitHub 上使用,這個時候假如 JD 要禁用來自 GitHub 的圖片訪問,可以怎麼做呢?很簡單:

location ~* .(gif|jpg|png|webp)$ {     valid_referers none blocked server_names jd.com *.jd.com;     if ($invalid_referer) {      return 403;     }     return 200 "get image successn";  }

我們利用 Nginx 自帶的 valid_referers 指令,對所有圖片請求做了一個 referer 校驗,只有 jd.com 及其子域下的圖片請求才能成功,其他的都走 403 禁止,變數 $invalid_referer 的值正是校驗結果。我們測試一下訪問結果,可以發現,非法 referer 的請求都被攔截禁止了:

ECCMAC-48ed2e556:nginx-quick hankle$ curl -H 'referer: http://jd.com' http://localhost:8080/test.jpg  get image success  ECCMAC-48ed2e556:nginx-quick hankle$ curl -H 'referer: http://wq.jd.com' http://localhost:8080/test.jpg  get image success  ECCMAC-48ed2e556:nginx-quick hankle$ curl -H 'referer: http://baidu.com' http://localhost:8080/test.jpg  <html>  <head><title>403 Forbidden</title></head>  <body>  <center><h1>403 Forbidden</h1></center>  <hr><center>nginx/1.17.5</center>  </body>  </html>

HTTPS

HTTPS 大家都比較熟悉了,它是在 HTTP 基礎上引入 SSL 層來建立安全通道,通過對傳輸內容進行加密以及身份驗證,避免數據在傳輸過程中被中間人劫持、篡改或盜用的一種技術。Chrome 從 62 版本開始將帶有輸入數據的 HTTP 站點和以隱身模式查看的所有 HTTP 站點自動標記為 「不安全」 站點,可見在網路安全規範普及下,HTTPS 化是未來 Web 網站的一大趨勢。

Nginx 可以簡單快速地搭建起 HTTPS 服務,需要依賴於 http_ssl_module 模組。nginx -V 能夠列出 Nginx 的編譯參數,查看是否已安裝 http_ssl_module 模組。

搭建 HTTPS 服務需要生成密鑰和自簽 SSL 證書(測試用,正式的需要簽署第三方可信任的 SSL 證書),我們需要利用到 openssl 庫。新建 nginx/ssl_cert 目錄:

1)生成密鑰 .key

openssl genrsa -out nginx_quick.key 1024

2)生成證書籤名請求文件 .csr

openssl req -new -key nginx_quick.key -out nginx_quick.csr

3)生成證書籤名文件 .crt

openssl x509 -req -days 3650 -in nginx_quick.csr -signkey nginx_quick.key -out nginx_quick.crt

完成這三步後,我們也就生成了 HTTPS 所需的密鑰和 SSL 證書,直接配置到 nginx.conf 中:

# ...  server {    listen       443 ssl;    # 監聽埠    server_name  localhost;  # 監聽地址      ssl_certificate /etc/nginx/ssl_cert/nginx_quick.crt;    ssl_certificate_key /etc/nginx/ssl_cert/nginx_quick.key;      # ...  }

修改 docker-compose.yml,把自定義證書文件傳到 Nginx 的對應路徑下:

services:    nginx: # 服務的名稱      # ...      volumes: # 文件夾映射        - ./nginx/nginx.conf:/etc/nginx/nginx.conf # nginx 配置文件        - ./nginx/ssl_cert:/etc/nginx/ssl_cert # 證書文件      ports: # 埠轉發        - "443:443"

重啟後訪問 https://localhost,發現頁面被 Chrome 標記為不安全訪問,這是因為自簽證書是無效證書導致的,點擊「繼續前往」可正常訪問到頁面:

頁面快取

我們常說的頁面快取主要分為三類:客戶端快取、代理快取、服務端快取,這裡重點討論的是代理快取。

當 Nginx 做代理時,假如接收的大多是一些響應數據不怎麼變化的請求,比如靜態資源請求,使用 Nginx 快取將大幅度提升請求速度。Nginx 中的快取是以文件系統上的分層數據存儲的形式實現的,快取鍵可配置,並且可以使用不同的特定於請求的參數來控制進入快取的內容。

Nginx 利用 proxy_cache_pathproxy_cache 來開啟內容快取,前者用來設置快取的路徑和配置,後者用來啟用快取:

http {    # ...    proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=mycache:10m max_size=10g inactive=60m;      server {      # ...        proxy_cache mycache;        # ...    }  }

上邊我們設置了一個快取 mycache,並在 server 中啟用:

1)/data/nginx/cache 指定了本地快取的根目錄;

2)level 代表快取目錄結構是兩層的,最多設置3層,數字代表命名長度,比如 1:2 就會生成諸如 /data/nginx/cache/w/0d 的目錄,對於大量快取場景,合理的分層快取是必要的;

3)keys_zone 設置了一個共享記憶體區,10m 代表記憶體區的大小,該記憶體區用於存儲快取鍵和元數據,保證 Nginx 在不檢索磁碟的情況下能夠快速判斷出快取是否命中;

4)max_size 設置了快取的上限,默認是不限制;

5)inactive 設置了快取在未被訪問時能夠持續保留的最長時間,也就是失活時間。

尾言

以上是一些簡單實用的 Nginx 應用場景和使用技巧,對於前端開發來說,Nginx 依然還是很有必要深入了解的。但是面對繁瑣複雜的 Nginx 配置和不堪入目的官方文檔,不少人都要叫苦了,並且就算語法熟練編寫無障礙,也會因為調試困難等各種問題浪費大量時間來排查錯誤。這裡推薦一個 Nginx 配置在線生成工具[3],可以簡單快速地生成你需要的 nginx.conf 配置,媽媽再也不用擔心我學不好 Nginx 了!

引用

[1] https://www.docker.com/

[2] https://docs.docker.com/install/

[3] https://nginxconfig.io/