獲取聯絡與訂閱

nginx 範式談

2021.05.24

欲揚先抑

這大致是一份重寫的文章, 在 blog 重構前寫過同樣的主題發現實用性非常強以至於經常會翻出來作為實際應用中的參考, 還是打算再次發布一份. 還是要先說明的是 本文不作為任何入門參考, 也不建議透過任何一篇文章學習 nginx 的配置 和使用, 此類需求始終應該將 nginx 官方文檔 作第一選擇. 既然是欲揚先抑自然要先簡述 nginx 的些許缺陷, 實際上稱其為缺陷是並不合適的, 只是 nginx 的運作模式或底層邏輯並不適合於這麼做罷了:

略顯僵硬的代理策略

相信在 nginx 設計之初就沒有將 按路徑反代 列入問題清單之內. 不僅如此, 在一些不同語種或不同結構的資源運作於不同伺服器路徑的大型站點而言, 將這些被肢解的資源透過一個一個 location 組合在一起也是噩夢, 任何錯誤的配置都會 proxy_pass 失敗, 如果不同結構由不同團隊開發就更是亦然. 對於這類需求不妨試試 traefik 靈活處理各類代理需求, 透過不同路徑不同請求頭不同 UA 等等各類反代方式滿足各種場景的需要.

對動態域名的處理

許多場景下 nginx 代理的後端伺服器地址並不是固定的, 比如眾所周知的 DDNS 動態域. 直接使用 proxy_pass 去反向代理很快就會發現後端地址變化後 nginx 並不嘗試重新解析域名, 而是依然連接後端最初的 IP 地址, 這個地址就是 nginx 加載配置文件時解析得到的地址, 只有 nginx -s reload 或完全重新啟動 nginx 進程才會再次解析並永久緩存下來, 這一問題在 nginx 本身可以透過配置變量的方式解決:

resolver 1.1.1.1;

location / {
  set $example www.example.com;
  proxy_pass http://$example;
}

實際應用場景中, 特別是 後端伺服器為 DDNS 的情況或許更應該選擇內網穿透 的方式解決, 從而保障服務質量和高可用性, 甚至可以不依賴公共網路地址而運作. 此類項目有很多, 比如 frp 就是一個很好的選擇, 支持協議豐富可插件並支持熱加載, 底層是 golang 對網路處理也比較得心應手.

範式談

配置路徑與常見場景

透過源安裝的 nginx 通常會部署在 /etc/nginx 目錄下, 在大多數情況下我們並不需要直接對 /etc/nginx/nginx.conf 進行修改, 而是在 /etc/nginx/sites-available 下建立新的配置檔並透過軟鏈接方式 ln -s /etc/nginx/sites-enabled 啟用需要的配置並對外提供服務.


為避免在文章中過多的贅述, 我們不妨對一些常見的應用場景整合說明. 在接觸一段時間 nginx 的使用後很容易發現我們經常在重複一些同樣的配置, 比如基本的反向代理及 websocket 這樣最常見類型的基礎服務配置不妨參考:

location /example {
  proxy_pass http://www.example.com;
  proxy_set_header Host $host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

location /websocket {
  proxy_redirect off;
  proxy_pass http://websocket:example.com;
  proxy_set_header Host $host;
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection "upgrade";
}

對於 http 端口則始終建議配置到 https 的自動跳轉:

server {
  listen 80;
  listen [::]:80;
  return 301 https://$host$request_uri;
}

根據域名反代

虛擬主機的便利是顯而易見的, 實際上在一臺伺服器承載更多站點或後端展現的當前, 如果將很多 常規的反代入口 寫作虛擬主機還是顯得非常纍贅, 特別是需要不斷創建新的配置檔, 這時便可寫作一份配置檔並根據域名來判斷需要反代的具體後端:

location / {
  proxy_set_header Host $host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection "upgrade";
  proxy_set_header X-Forwarded-Proto https;
  proxy_set_header X-Forwarded-Host $host;
  if ($host ~ ^(grafana)\.example\.com$) {
    proxy_pass http://10.0.0.1:3000;
  }
  if ($host ~ ^(proxmox)\.example\.com$) {
    proxy_pass https://10.0.0.1:8006;
  }
  if ($host ~ ^(jupyter)\.example\.com$) {
    proxy_pass http://10.0.0.3:8080;
  }
}

在這裏所有反代都寫作 websocket 旨在兼容長連接, 如果並不是 websocket 後端並不會受到影響. 要特別注意的是 server_name 必須寫作泛域名形式 以便後續的判斷流程正常完成.

proxy_connect_timeout 90;
proxy_read_timeout 90;

對於很多節點的情況, 可能會出現部分節點首次建立連接時間較長的情況, 可以嘗試延長鏈接和讀取超時設定.

靜態資源展示

儘管這是 nginx 的主要特性之一, 但想要配置得當依舊不是一行代碼就可以完成的, 需要考慮到 web 端下當地時間的正確展示, 文件大小的單位及中文編碼等方面:

location /pub {
  alias /etc/pub;
  autoindex_localtime on;
  autoindex on;
  autoindex_exact_size off;
  charset utf-8;
}

對於 aliasroot 的區別不再贅述, 詳見文後.

根據來源 IP 拒絕服務

儘管此類問題通常應該由 iptables 處理, 但就處理的粒度上並不能夠滿足我們針對主伺服器上某些站點的針對性策略配置. 換言之, 從端口下手的安全策略 在這類場景下 還是太過底層了. 我們不妨模擬一個場景需求, 現需要對 10.0.0.0/8 內網段單獨開放某目錄的訪問權限:

geo $block_connection {
  default        1;
  10.0.0.0/8     0;
}

location / {
  if ($block_connection) {
      return 444;
  }
  root html;
  index index.html;
}

其中主要有兩個部分的內容, 首先是 geo $block_connection 這份判斷列表, 對應 location 中的判斷語句一旦滿足 1 則返回狀態碼 444 結束連接, 否則放行並展示對應的靜態資源, 完成整個判斷與阻斷過程. 而這個 return 444; 是非常實用的 nginx 特性, 通常可以用來使 nginx 主動斷開連接, 在用戶端側看來就是單純的連接失敗, 搭配一些安全策略使用體驗極佳.

狀態碼替換

承上接下地講, 狀態碼常常攜帶了很多信息及含義, 對其靈活的處理也十分重要, 否則很容易誤導訪問者和使用者. 在此以替換 400 狀態碼為 404 為實例, 模擬隱藏接口規避被探測的場景:

proxy_intercept_errors on;
error_page 400 404 =404 /404.html;

非常簡潔有效, 但需要注意的是配置關鍵並不是 /404.html 這個具體的返回頁是什麼, 而是 =404 這個替換狀態碼的過程, 否則用戶側是十分詭異帶200 狀態碼的 404 頁面.

返回用戶側地址

儘管這看起來是個比較扭曲的需求, 但實際上在需要維護 DDNS 站點時是非常有效的, 沒有靜態地址的設備上得到自身地址的方式只有如此, 而服務側的返回地址透過 nginx 便可以快捷實現:

location /ip {
  default_type text/plain;
  return 200 "$remote_addr\n";
}

這是幾乎沒有什麼開銷的額外服務, 能夠幫助許多 DDNS 用戶. 此前向些許大站的同類請求得到了不屑的回應, 也就只好另尋合作或自建, 倒是意外收穫非常多的 DDNS 用戶.

雜記

root 和 alias

儘管這是非常基礎的兩個配置方式, 但時常會遇到因此出現問題的場景. 簡言之 root 會帶上路徑去請求實際目錄而 alias 並不會, 這便是區別. 簡要舉例説明:

location /foo {
  root /var/www/foo;
}

實際請求的目錄是 /var/www/foo/foo, 因此如果想要請求的是 /var/www/foo 而使用上述寫法只會得到 404 錯誤.

location /foo {
  alias /var/www/foo;
}

而同樣的配置路徑下將 root 換成 alias 便可以正常工作, 因為 alias 不會再次帶上路徑去請求實際目錄, 這樣便顯而易見了.

資料大小限制

通常在沒有額外指定的默認情況下 nginx 對於資料文件的傳輸限制並不大, 截止目前以較新的版本而言也將這一參數限制在了 1MB 以下. 如果我們在請求過程中遇到 413 等錯誤便需要特別修正:

client_max_body_size 1024m;

待續

儘管上述已概括 nginx 大部分常用場景, 但並不意味著 nginx 只有這些特性和可用空間, 願在日後的使用過程中也能夠不斷續寫記錄並完善, 也為個人使用提供更多參照與便利.