:反向代理、負(fù)載均衡、HTTPS 一次講清)
開場你的 Node.js 應(yīng)用跑在127.0.0.1:3000產(chǎn)品說用戶訪問api.example.com能用就行。你打開 Nginx 配置文件復(fù)制一段網(wǎng)上的配置reload結(jié)果要么 404 要么 502。這個場景我見過不止一次。Nginx 配置的語法看起來很簡單文檔也寫得不算差但location和proxy_pass那幾個斜杠能把人卡半天。不是你不行是這兩個東西的行為確實有坑。這篇文章不是入門科普是給那些已經(jīng)部署過應(yīng)用、知道 Nginx 能當(dāng)反向代理、但配著配著就出問題的人寫的。讀完之后你配一個新的 Nginx 反代站點基本能做到一次過不用反復(fù) 502。一、Nginx 在你架構(gòu)里干什么先搞清楚 Nginx 站在哪里。外部請求先到 NginxNginx 再把請求轉(zhuǎn)發(fā)給你后頭的應(yīng)用。客戶端不知道你后端用的是 Node、Java 還是 Python它只跟 Nginx 說話。Nginx 在這中間扮演的是反向代理的角色。這里插一句正向代理和反向代理的區(qū)別只需要記一句話就夠了——正向代理是替客戶端做事你上 Google 是讓代理幫你拿反向代理是替服務(wù)端做事客戶端以為自己在直接訪問服務(wù)端但其實是服務(wù)端派的代理在接單。這個區(qū)別面試會問但配 Nginx 的時候不需要想太多。除了反向代理Nginx 還能干這些事靜態(tài)文件服務(wù)。你的 React/Vue 構(gòu)建產(chǎn)物直接丟到服務(wù)器目錄Nginx 配個root就能跑不用走 Node 服務(wù)。負(fù)載均衡。多臺后端機(jī)器搭一個 upstream 塊Nginx 自動把流量分散過去。SSL 終結(jié)。HTTPS 在 Nginx 這一層解密后端跑明文 HTTP減少后端的加密計算負(fù)擔(dān)。限流。limit_req_zone和limit_conn_zone配一配能防住一部分爬蟲和 CC 攻擊。我的觀點是小項目PV 百萬以下、單臺機(jī)器能扛住的直接用 Nginx 反代加靜態(tài)托管就夠了。別一上來就想著上 Kong、Traefik 或者 Istio那些東西的學(xué)習(xí)成本和運維成本不低你的業(yè)務(wù)還沒到那個量級之前Nginx 能解決大部分問題。二、配置文件長什么樣Nginx 的配置文件有一個固定的層級結(jié)構(gòu)全局塊 ← 啟動用戶、工作進(jìn)程數(shù)等全局設(shè)置 events ← 工作連接相關(guān)配置 http ← HTTP 全局配置可包含多個 server server ← 一個虛擬主機(jī) location ← 匹配 URL 路徑實際文件里長這樣user nginx; worker_processes auto; error_log /var/log/nginx/error.log warn; pid /run/nginx.pid; events { worker_connections 1024; } 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; keepalive_timeout 65; server { listen 80; server_name example.com; location / { root /usr/share/nginx/html; index index.html; } } }配置文件路徑在主流 Linux 發(fā)行版里通常是/etc/nginx/nginx.conf自定義的站點配置放在/etc/nginx/conf.d/或者通過include conf.d/*.conf引入。Ubuntu/Debian 系統(tǒng)還有sites-available/sites-enabled這套目錄用a2ensite命令管理其實底層也是 include。配完之后有兩個命令必須記住nginx-t# 測試配置文件語法有錯會報錯nginx-sreload# 熱重載不中斷現(xiàn)有連接踩坑點很多人改完配置直接nginx -s reload結(jié)果配置文件有語法錯誤Nginx 啟動失敗舊配置也被你 reload 掉了線上直接 502。正確做法永遠(yuǎn)是nginx -t確認(rèn)配置無誤之后再nginx -s reload。這條規(guī)矩沒有例外。三、反向代理最常用的玩法反向代理是 Nginx 最核心的用法。一段最小可用的反代配置長這樣server { listen 80; server_name api.example.com; location / { proxy_pass http://127.0.0.1:3000; 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_pass后面那個斜杠的問題你必須搞清楚。proxy_pass 斜杠不帶 / 和帶 / 有什么區(qū)別這是高頻踩坑點。兩種寫法路徑拼接規(guī)則完全不同寫法一不帶斜杠location /api/ { proxy_pass http://127.0.0.1:3000; # 不帶斜杠 }請求/api/users→ 轉(zhuǎn)發(fā)到http://127.0.0.1:3000/api/users寫法二帶斜杠location /api/ { proxy_pass http://127.0.0.1:3000/; # 帶斜杠 }請求/api/users→ 轉(zhuǎn)發(fā)到http://127.0.0.1:3000/users規(guī)則是帶/意味著 Nginx 會把 location 匹配上的那部分路徑從轉(zhuǎn)發(fā)目標(biāo)里去掉。兩種寫法差了api這幾個字符夠讓你的后端應(yīng)用收到 404 了。寫法三proxy_pass 指向一個 URIlocation /api/ { proxy_pass http://127.0.0.1:3000/v2/; # 帶路徑替換 }請求/api/users→ 轉(zhuǎn)發(fā)到http://127.0.0.1:3000/v2/usersproxy_pass后面跟了路徑等于做了一次路徑替換。這種寫法適合你的后端 API 路徑和前端路徑不一致的場景。三個 Header 為什么要設(shè)proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;這三個 Header 不是可選項是必選項。Host $host讓后端知道請求的是哪個域名。不然你后端可能有虛擬主機(jī)多個站點混布的時候就會拿錯配置。X-Real-IP把客戶端的真實 IP 傳給后端。Nginx 拿到的是客戶端 IP$remote_addr就是這個值。不然你的應(yīng)用日志里全是127.0.0.1。X-Forwarded-For如果請求已經(jīng)經(jīng)過一層代理這個 Header 記錄了整條鏈路上的 IP 鏈。$proxy_add_x_forwarded_for會在已有值后面追加當(dāng)前 IP防止 IP 偽造。如果你的后端是 Java/Spring通常還要加一個X-Forwarded-Proto來讓后端知道前端是走 https 還是 http否則你用request.getScheme()會拿到http而不是實際協(xié)議。四、location 匹配規(guī)則重點很多人配 Nginx 出問題80% 死在 location 匹配規(guī)則上。location 的匹配方式有四種每種有自己的優(yōu)先級匹配類型語法說明精確匹配location /path只匹配這一個路徑前綴匹配location ^~ /path匹配以此開頭的路徑不繼續(xù)正則匹配正則匹配location ~ /path或location ~* /path~區(qū)分大小寫~*不區(qū)分普通前綴location /path通用匹配最不優(yōu)先匹配優(yōu)先級從高到低精確匹配 前綴匹配^~ 正則匹配 普通前綴。Nginx 在匹配 location 的時候是按這個順序來遍歷的。一旦在某個優(yōu)先級上命中就不會再往下看了。舉例說明假設(shè)你配了這樣一組 locationlocation / { return 200 精確匹配 /\n; } location ^~ /static/ { return 200 前綴匹配 /static/\n; } location ~ \.php$ { return 200 正則匹配 .php$\n; } location / { return 200 普通前綴 /\n; }不同請求會命中哪個請求/→ 命中精確匹配 /請求/static/css/app.css→ 命中前綴匹配^~ /static/請求/index.php→ 命中正則匹配~ .php$正則優(yōu)先級高于普通前綴請求/about→ 命中普通前綴/這里有一個坑正則匹配是按配置文件里出現(xiàn)的順序逐個匹配的一旦匹配上就停不會再看后面的。所以正則的順序很重要。實戰(zhàn)建議能用前綴匹配就別用正則。location /api/、location /static/、location /health這些前綴匹配已經(jīng)能覆蓋大部分場景了。正則只有在需要模式匹配的時候才用比如location ~ \.(jpg|png|gif)$匹配特定后綴而且寫的時候要注意順序靠前的正則優(yōu)先。精確匹配用于特殊路徑。比如健康檢查接口/health、根路徑/這類固定路徑用精確匹配最穩(wěn)妥不會被其他規(guī)則誤傷。五、負(fù)載均衡一臺不夠就上多臺當(dāng)單臺后端扛不住流量的時候就要上負(fù)載均衡了。Nginx 通過upstream塊定義一組后端服務(wù)器然后用proxy_pass指向這個 upstream。基本配置upstream backend { server 10.0.0.2:3000 weight3; server 10.0.0.3:3000 weight2; server 10.0.0.4:3000 backup; } server { listen 80; server_name api.example.com; location / { proxy_pass http://backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }幾種負(fù)載均衡策略輪詢默認(rèn)每個請求依次分發(fā)給下一臺機(jī)器不做任何額外判斷。適合后端機(jī)器配置一致、請求處理時間相近的場景。權(quán)重weight數(shù)字越大分到的請求越多。上面配置里10.0.0.2權(quán)重 310.0.0.3權(quán)重 2所以 2 的請求量大約是 3 的 1.5 倍。適合機(jī)器配置不一致的場景配置好的機(jī)器權(quán)重設(shè)高一些。ip_hash同一個客戶端 IP 的請求永遠(yuǎn)落到同一臺后端機(jī)器。這個策略用于解決會話粘性——如果你的后端應(yīng)用沒有獨立的 session 存儲比如沒用 Redis多臺機(jī)器之間 session 不共享用戶登錄后會話就丟了。ip_hash能綁住同一個 IP 的請求。least_conn優(yōu)先把請求發(fā)給當(dāng)前連接數(shù)最少的機(jī)器適合請求處理時間差異較大的場景。自動故障轉(zhuǎn)移upstream backend { server 10.0.0.2:3000; server 10.0.0.3:3000; } server { location / { proxy_pass http://backend; proxy_next_upstream error timeout http_500 http_502 http_503; } }proxy_next_upstream的意思是當(dāng)指定的情況發(fā)生時后端報錯、連接超時、返回 5xx自動把請求轉(zhuǎn)發(fā)給 upstream 里下一臺可用的服務(wù)器。這樣做的好處是對用戶來說請求仍然可能成功不需要你手動去修后端機(jī)器。踩坑ip_hash 和后端擴(kuò)縮容這是用 ip_hash 時必須知道的問題。ip_hash 的原理是把 IP 映射到后端服務(wù)器的編號上。后端機(jī)器數(shù)量不變的時候這個映射是穩(wěn)定的。但如果你在不停機(jī)的情況下加機(jī)器或者減機(jī)器原來的哈希映射就全亂了——原來落在 A 機(jī)器的請求現(xiàn)在可能落到 B 機(jī)器而 B 機(jī)器上沒有對應(yīng)的 session用戶就掉線了。所以用 ip_hash 的時候后端擴(kuò)縮容要小心。如果你的應(yīng)用對會話要求高最好把 session 放到 Redis 等獨立存儲里然后負(fù)載均衡策略用輪詢或者 least_conn這樣無論后端怎么增減都不影響已有會話。六、HTTPS現(xiàn)在標(biāo)配HTTP 明文傳輸?shù)臅r代早就該結(jié)束了。瀏覽器現(xiàn)在會給非 HTTPS 站點標(biāo)不安全很多 API 不支持Mixed ContentSEO 也受影響。新項目直接上 HTTPS別等出問題了再補(bǔ)。Let’s Encrypt 免費證書Let’s Encrypt 提供的證書免費、自動化、三個月有效期用 certbot 工具申請非常方便# Ubuntu/Debiansudoaptinstallcertbot python3-certbot-nginxsudocertbot--nginx-dapi.example.com# 按提示操作certbot 自動申請證書并寫入 Nginx 配置certbot 會自動修改你的 Nginx 配置加好listen 443 ssl和證書路徑還會順手配好 HTTP 跳 HTTPS 的重定向。手動配 HTTPS如果不用 certbot 或者 certbot 不支持你的環(huán)境手動配也不復(fù)雜server { listen 443 ssl; server_name api.example.com; ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; ssl_prefer_server_ciphers on; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } # HTTP 強(qiáng)制跳 HTTPS server { listen 80; server_name api.example.com; return 301 https://$host$request_uri; }ssl_protocols里禁用了 TLS 1.0 和 1.1這兩個版本有已知漏洞已經(jīng)不安全了。如果你的業(yè)務(wù)不需要兼容特別老的客戶端就別開它們。踩坑證書到期忘了續(xù)Let’s Encrypt 證書有效期是 90 天如果忘了續(xù)站點直接變成不安全的。有些項目當(dāng)時用 certbot 配了但自動續(xù)期沒跑起來時間一長證書過期了才發(fā)現(xiàn)。裝完 certbot 之后確認(rèn)一下自動續(xù)期有沒有配好sudocertbot renew --dry-run--dry-run會模擬一次續(xù)期不實際執(zhí)行。如果這條命令報錯了說明自動續(xù)期沒配好。正常情況下 certbot 會裝一個 cron 任務(wù)或者 systemd timer 自動續(xù)期但你得確認(rèn)它確實在跑。七、靜態(tài)文件與緩存對于前端項目靜態(tài)資源沒必要走 Node/Python 服務(wù)讓 Nginx 直接處理更快、更省資源。server { listen 80; server_name example.com; # 靜態(tài)資源Nginx 直接服務(wù) location /static/ { root /var/www/myapp; expires 30d; # 緩存 30 天 add_header Cache-Control public, immutable; } # 上傳文件或用戶內(nèi)容 location /uploads/ { root /var/www/myapp; expires 7d; } # 剩下的走反代 location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }root指令指定文件根目錄。請求/static/css/app.css時Nginx 會去找/var/www/myapp/static/css/app.css。expires設(shè)置緩存過期時間30d表示 30 天。這個頭讓瀏覽器緩存這些文件用戶第二次訪問的時候就不發(fā)請求了頁面加載速度明顯快。踩坑location 寫得寬把靜態(tài)請求也反代了這個問題非常常見。你配了一個通用的location /做反代然后在后端應(yīng)用里發(fā)現(xiàn)某些靜態(tài)文件請求報了 404但你把文件放到服務(wù)器上就是找不到。原因就是你的location /反代把請求轉(zhuǎn)發(fā)給了后端服務(wù)但后端服務(wù)的路由里根本沒有這些路徑。解決方案是把靜態(tài)請求用更精確的 location 單獨處理放在location /之前這樣它們就不會落入反代的范圍。# 靜態(tài)資源要放在普通前綴之前匹配 location /static/ { root /var/www/myapp; } location /uploads/ { root /var/www/myapp; } location / { proxy_pass http://127.0.0.1:3000; }Nginx location 匹配是按優(yōu)先級來的普通前綴按最長匹配原則處理所以/static/會比/更長、更優(yōu)先。但如果你是用正則匹配或者其他方式順序就很重要了。八、常見故障排查Nginx 出問題的時候大部分人第一反應(yīng)是我配置是不是寫錯了然后開始一條條對著文檔看。實際上大多數(shù)問題不需要那么復(fù)雜先看日志。# 實時查看錯誤日志tail-f/var/log/nginx/error.log# 查看訪問日志tail-f/var/log/nginx/access.log錯誤日志里會記錄具體報了什么錯、哪個文件第幾行有問題。這些信息比你對著配置猜要準(zhǔn)確得多。502 Bad Gateway這是最常見的錯誤通常意味著 Nginx 成功收到了請求但轉(zhuǎn)發(fā)給后端的時候后端沒有響應(yīng)。原因排查順序后端服務(wù)沒啟動。curl http://127.0.0.1:3000試一下確認(rèn)后端在跑。upstream 地址寫錯了。檢查proxy_pass指向的地址和端口對不對。后端服務(wù)掛了或者卡住了。看后端日志看進(jìn)程狀態(tài)看 CPU 和內(nèi)存占用。后端響應(yīng)超時。proxy_connect_timeout和proxy_read_timeout默認(rèn) 60 秒如果后端處理時間太長Nginx 會放棄等待。403 ForbiddenNginx 返回 403通常是兩個原因root路徑權(quán)限不對。Nginx 的 worker 進(jìn)程用戶通常是nginx或www-data對目標(biāo)目錄沒有讀權(quán)限。檢查目錄權(quán)限ls -la /var/www/myapp確認(rèn)有 rx 權(quán)限。index 文件不存在。你配了index index.html但目錄里沒有這個文件。404404 的原因比較多逐一排查location 沒匹配到請求路徑。用nginx -T大寫 T可以把完整配置輸出到終端檢查哪個 location 命中了。proxy_pass斜杠問題。見第三節(jié)路徑拼接規(guī)則導(dǎo)致后端收到了它不認(rèn)識的路徑。后端服務(wù)沒有這個路由。確認(rèn)后端應(yīng)用里確實注冊了這個路徑。結(jié)尾Nginx 配置沒有多難。語法就那么幾種配置塊就那么幾層。但配站點能不能一次過靠的不是記住多少語法而是搞清楚location匹配優(yōu)先級和proxy_pass斜杠行為這兩個高頻坑。這篇文章里講到的那些坑我自己和周圍的同事基本都踩過proxy_pass 少個斜杠 404、正則順序?qū)懛雌ヅ溴e、reload 前沒先 -t 導(dǎo)致半夜報警。踩過一遍之后印象就深了。改配置之前nginx -t這是鐵律。記住這條你已經(jīng)比很多人少踩一半的坑了。