前端er須知的Nginx技巧

沐童Hankle發表於2019-11-03

前述

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 這裡不做細講,有興趣的同學可以自行了解 Docker

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

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

環境就位後,我們新建一個專案 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 最為普遍的使用場景,正向代理就是其中一種。

前端er須知的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 服務:

前端er須知的Nginx技巧

負載均衡

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

前端er須知的Nginx技巧

利用 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 服務:

前端er須知的Nginx技巧

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 頁面中:

前端er須知的Nginx技巧

需要注意的是,如果這裡使用了反向代理,存在多個 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

前端er須知的Nginx技巧

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

前端er須知的Nginx技巧

防盜鏈

某些情況下我們不希望自己的資原始檔被外部網站使用,比如有時候我會把 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 success\n";
}
複製程式碼

我們利用 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 標記為不安全訪問,這是因為自簽證書是無效證書導致的,點選「繼續前往」可正常訪問到頁面:

前端er須知的Nginx技巧

頁面快取

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

當 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 配置線上生成工具,可以簡單快速地生成你需要的 nginx.conf 配置,媽媽再也不用擔心我學不好 Nginx 了!

相關文章