問題背景
Nginx 是互聯(lián)網(wǎng)生產(chǎn)環(huán)境中使用最廣泛的反向代理和 Web 服務(wù)器之一。不管是做靜態(tài)資源服務(wù)、API 網(wǎng)關(guān),還是負(fù)載均衡器,Nginx 幾乎是標(biāo)準(zhǔn)配置。很多運(yùn)維工程師日常和它打交道,但真正能把配置細(xì)節(jié)說清楚的人不多——不是因?yàn)?Nginx 復(fù)雜,而是因?yàn)樗暮芏嘈袨椴环现庇X,配置項(xiàng)之間的相互作用容易被忽略。
實(shí)際生產(chǎn)環(huán)境里,大量線上故障的直接原因就是 Nginx 配置不當(dāng):有人把請求代理到了已經(jīng)宕機(jī)的 upstream 導(dǎo)致 502,有人因?yàn)?location 匹配優(yōu)先級(jí)問題導(dǎo)致靜態(tài)資源 404,有人文件上傳一直報(bào) 413 查了半天是 client_max_body_size 沒配,有人升級(jí) TLS 證書后瀏覽器報(bào)不安全是因?yàn)橹虚g證書沒一起部署。每一個(gè)坑都是真實(shí)踩出來的。
本文從一線運(yùn)維工程師的視角,系統(tǒng)梳理 Nginx 配置中最容易出問題的十個(gè)場景。每個(gè)坑都按照「現(xiàn)象描述 → 根因分析 → 正確配置 → 驗(yàn)證方法 → 風(fēng)險(xiǎn)提醒」的閉環(huán)結(jié)構(gòu)來講,讓你不僅知道怎么修,還知道為什么這樣修,以及修完之后怎么確認(rèn)生效。
適用場景
日常運(yùn)維:接手新服務(wù)器、變更配置前檢查、線上故障排查
上線變更:每次 Nginx 配置變更后的驗(yàn)證
面試準(zhǔn)備:理解 Nginx 核心配置原理
性能優(yōu)化:定位 Nginx 導(dǎo)致的響應(yīng)慢或資源消耗異常
通用排查思路
在逐個(gè)拆解具體踩坑點(diǎn)之前,先建立一個(gè)通用的排查框架。每次遇到 Nginx 配置問題,按這個(gè)順序走一遍,能排除大部分問題。
第一步:驗(yàn)證配置語法
Nginx 提供了一個(gè)內(nèi)置的配置檢查命令,任何修改配置之后、上線之前,必須先跑這一條:
nginx -t # 典型輸出: # nginx: the configuration file /etc/nginx/nginx.conf syntax is ok # nginx: configuration file /etc/nginx/nginx.conf test is successful
如果輸出報(bào)錯(cuò),它會(huì)指出具體文件和行號(hào)。常見錯(cuò)誤比如拼寫錯(cuò)誤、分號(hào)缺失、括號(hào)不匹配等。
但要注意:-t只檢查語法和基本結(jié)構(gòu),不檢查邏輯錯(cuò)誤。比如你把 proxy_pass 指向了一個(gè)不存在的 upstream,-t依然會(huì)通過,但實(shí)際跑起來會(huì) 502。所以-t通過只是必要條件,不是充分條件。
第二步:查看 error_log
error_log 是排查 Nginx 問題的第一手資料。很多工程師配置了日志卻從來不看,等到出問題了才想起來去翻。
# 查看 error_log 位置(默認(rèn)在 nginx.conf 里配置) grep -r"error_log"/etc/nginx/ # 實(shí)時(shí)跟蹤最新錯(cuò)誤 tail -f /var/log/nginx/error.log # 按時(shí)間過濾最近 5 分鐘的日志 find /var/log/nginx/ -name"error.log"-mmin -5 | xargs tail -n 50
error_log 的級(jí)別可以在配置里指定,常見的有 debug、info、notice、warn、error、crit。建議生產(chǎn)環(huán)境用 warn 或 error,太細(xì)的級(jí)別會(huì)產(chǎn)生大量 IO 開銷。
第三步:檢查進(jìn)程和連接狀態(tài)
# 看 Nginx master 和 worker 進(jìn)程 ps aux | grep nginx # 看 Nginx 監(jiān)聽的端口 ss -tlnp | grep nginx # 看當(dāng)前連接數(shù)狀態(tài) netstat -an | awk'/:80s/ {s[$NF]++} END {for(k in s) print k, s[k]}' # 或用 ss(更現(xiàn)代) ss -s # 看每個(gè) worker 的連接數(shù)(看負(fù)載是否均衡) ps -o pid,ppid,comm,%cpu,%mem --no-headers -e | grep nginx
如果 worker 進(jìn)程 CPU 或內(nèi)存異常高,往往意味著配置里有性能問題,比如 regex location 匹配或過于頻繁的日志寫操作。
第四步:reload 而非 restart
Nginx 支持不停機(jī) reload 配置,這應(yīng)該是線上變更的標(biāo)準(zhǔn)操作:
nginx -s reload # 或者向 master 進(jìn)程發(fā)信號(hào) kill-HUP $(cat /var/run/nginx.pid)
reload 的邏輯是:master 進(jìn)程加載新配置,啟動(dòng)新的 worker 進(jìn)程處理新請求,舊的 worker 優(yōu)雅退出(處理完現(xiàn)有請求后自動(dòng)關(guān)閉)。這個(gè)過程不會(huì)中斷現(xiàn)有連接。
但要注意:有些配置變更不支持 reload,必須 restart,比如綁定到新端口、修改 SSL 證書路徑等。這類操作需要提前申請維護(hù)窗口。
第五步:建立配置變更管理機(jī)制
生產(chǎn)環(huán)境的 Nginx 配置變更是高風(fēng)險(xiǎn)操作,建議遵循以下原則:
所有配置變更走代碼倉庫(Git),禁止直接在生產(chǎn)環(huán)境手工改
變更前先在測試環(huán)境驗(yàn)證nginx -t通過
變更時(shí)先 push 配置文件,再用 ansible/salt 或者直接 cp 部署
部署后立即nginx -t && nginx -s reload,不要離開,等觀察幾分鐘確認(rèn)正常再離開
準(zhǔn)備好回滾腳本,保留上一版配置文件的備份
踩坑點(diǎn)一:location 匹配優(yōu)先級(jí)混亂
現(xiàn)象
用戶訪問/api/users返回 404,或者請求本該走/api卻走到了/的處理邏輯,導(dǎo)致返回了 HTML 頁面而不是 JSON。這種問題在團(tuán)隊(duì)多人維護(hù) Nginx 配置時(shí)特別常見,某人在 http 段新增了一個(gè) catch-all location,導(dǎo)致原有的精細(xì)化匹配全部失效。
根因
Nginx 的 location 匹配規(guī)則是按優(yōu)先級(jí)遞進(jìn)匹配的,不是按配置順序,很多工程師以為「寫在后面的 location 覆蓋前面的」,這是一個(gè)根本性誤解。Nginx location 匹配優(yōu)先級(jí)從高到低如下:
location = /path—— 精確匹配,優(yōu)先級(jí)最高
location ^~ /path—— 前綴匹配,找到最長匹配后不再做正則匹配
location ~ /path或location ~* /path—— 正則匹配(~區(qū)分大小寫,~*不區(qū)分大小寫),按配置順序匹配,第一個(gè)命中的就停止
location /path—— 普通前綴匹配,最長匹配原則
典型錯(cuò)誤配置:
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html;
}
location /api {
proxy_pass http://127.0.0.1:8080;
}
# 下面的 catch-all 會(huì)導(dǎo)致 /api 也走這個(gè)分支,因?yàn)闆]有加 ^~
location ~* .php$ {
proxy_pass http://127.0.0.1:9000;
}
}
正確配置
根據(jù)實(shí)際需求選擇正確的匹配類型:
server {
listen 80;
# 精確匹配,首頁
location = / {
root /usr/share/nginx/html;
index index.html;
}
# 前綴匹配,不允許正則覆蓋,用于靜態(tài)資源目錄
location ^~ /static/ {
root /data/www;
expires 30d;
add_header Cache-Control "public, immutable";
}
# 前綴匹配,用于 API 代理
location /api/ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# 正則匹配,按順序排在普通前綴匹配之后
location ~* .(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
root /data/www;
expires 7d;
access_log off;
}
# 默認(rèn) catch-all
location / {
root /usr/share/nginx/html;
index index.html;
}
}
關(guān)鍵原則:正則 location(~或~*)會(huì)按配置文件中的順序匹配,一旦匹配就停止。普通前綴 location(/path)采用最長前綴匹配原則,即選擇匹配路徑最長的那個(gè)。如果某個(gè)前綴 location 不想被后續(xù)的正則匹配覆蓋,需要加^~修飾符。
驗(yàn)證方法
用curl模擬請求,觀察返回的響應(yīng)頭和狀態(tài)碼:
# 測試精確匹配 curl -I http://localhost/ # 期望:返回 index.html,狀態(tài)碼 200 # 測試 /api 路徑 curl -I http://localhost/api/users # 期望:代理到 8080,返回 API 響應(yīng),不是 404 # 測試靜態(tài)資源(確認(rèn)走緩存路徑) curl -I http://localhost/static/logo.png # 期望:返回 200,且有 Cache-Control 頭 # 用 -v 查看詳細(xì)處理過程(需要開啟 rewrite log) curl -v http://localhost/api/test2>&1 | head -30
如果配置了rewrite_log on和notice級(jí)別的 error_log,可以在 error.log 中看到詳細(xì)的 location 匹配過程:
error_log /var/log/nginx/error.log notice; rewrite_log on;
風(fēng)險(xiǎn)提醒
修改 location 匹配規(guī)則是高風(fēng)險(xiǎn)操作,因?yàn)榫€上 API 路徑、靜態(tài)資源路徑可能在代碼層有依賴。變更前需要:
確認(rèn)所有請求路徑的用途
在測試環(huán)境全量回歸
保留舊配置文件備份
變更后密切監(jiān)控 404 和 502 錯(cuò)誤率
踩坑點(diǎn)二:proxy_pass 末尾帶不帶 / 的區(qū)別
現(xiàn)象
配置了proxy_pass http://127.0.0.1:8080;和proxy_pass http://127.0.0.1:8080/;,表面看起來一樣,但實(shí)際代理出去的請求路徑完全不同。
具體來說:如果原始請求是GET /api/users HTTP/1.1,訪問目標(biāo)是http://your-domain.com/api/users:
proxy_pass http://127.0.0.1:8080;(無尾部斜杠):代理后請求變成GET /api/users HTTP/1.1(完整路徑保留)
proxy_pass http://127.0.0.1:8080/;(有尾部斜杠):代理后請求變成GET /users HTTP/1.1(/api 部分被替換掉了)
這個(gè)差異會(huì)導(dǎo)致后端收到完全不同的路徑,引發(fā) 404 或業(yè)務(wù)邏輯錯(cuò)誤。
根因
這是 Nginx 中最反直覺的配置之一。Nginx 在處理 proxy_pass 時(shí),會(huì)根據(jù)是否指定了 URI(路徑部分)來決定是否做路徑替換:
proxy_pass http://127.0.0.1:8080;—— 沒有指定 URI,Nginx 將原始請求的完整路徑傳遞給 upstream
proxy_pass http://127.0.0.1:8080/;—— 指定了 URI(根路徑),Nginx 會(huì)把匹配 location 時(shí)使用的前綴部分從請求路徑中移除,剩下的部分拼接到新的 URI 上
用具體例子說明:
# 場景:原始請求 /api/users -> proxy_pass http://127.0.0.1:8080/
# 寫法一:不帶 /
location /api {
proxy_pass http://127.0.0.1:8080;
# 代理后:GET /api/users -> upstream 收到 /api/users
}
# 寫法二:帶 /
location /api {
proxy_pass http://127.0.0.1:8080/;
# 代理后:GET /api/users -> upstream 收到 /users
}
正確配置
根據(jù)業(yè)務(wù)需求選擇正確的寫法,并配合rewrite或proxy_redirect做路徑調(diào)整:
# 場景一:后端路徑與前端路徑一致,不需要替換
location /api {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# 場景二:后端路徑有前綴,且 location 使用了精確前綴匹配,需要替換
location /api/ {
proxy_pass http://127.0.0.1:8080/;
# /api/users -> /users
# /api/login -> /login
}
# 場景三:需要保留部分路徑,或路徑映射關(guān)系復(fù)雜
location /app/v1/ {
rewrite ^/app/v1/(.*) /$1 break;
proxy_pass http://127.0.0.1:8080;
# /app/v1/users -> /users
# /app/v1/orders -> /orders
}
# 場景四:后端要求 Host 頭為特定值
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host "backend.example.com";
}
驗(yàn)證方法
在后端服務(wù)上開啟請求日志,觀察收到的請求路徑:
# 在 upstream 服務(wù)的 access log 中查看 # 如果是 Node.js/Express: console.log(req.method, req.url); # 如果是 Python/Flask: print(request.method, request.path, request.url) # 用 curl 本地測試(同時(shí)在前端和后端抓包) curl -v http://localhost/api/users 2>&1 # 看 Host 頭、X-Real-IP 頭、以及實(shí)際的 URL 是什么
一個(gè)完整的調(diào)試流程:
# 1. 檢查 proxy_pass 寫法 grep -n"proxy_pass"/etc/nginx/conf.d/*.conf # 2. 開啟調(diào)試日志查看 rewrite 過程 tail -f /var/log/nginx/error.log # 3. 用 nc/strace 在 upstream 端口抓包 # nc -l 8080 # 監(jiān)聽本地 8080,看收到的請求行 # 4. 確認(rèn) upstream 服務(wù)日志中的路徑 # 這是最直接的方式——upstream 日志里的 path 是什么,就是 Nginx 實(shí)際傳過去的路徑
風(fēng)險(xiǎn)提醒
修改 proxy_pass 的 URI 寫法會(huì)影響所有匹配該 location 的請求路徑。如果后端有多個(gè)微服務(wù)共用一個(gè) upstream 塊,尤其要注意。建議:
先在測試環(huán)境用完整請求路徑做回歸
確認(rèn)后端 API 是否做了路徑校驗(yàn)(比如 Spring Cloud Gateway 會(huì)嚴(yán)格校驗(yàn)路由前綴)
配合監(jiān)控,觀察上線后 404 錯(cuò)誤率變化
踩坑點(diǎn)三:try_files 使用不當(dāng)導(dǎo)致死循環(huán)
現(xiàn)象
訪問某些 URL 時(shí)瀏覽器報(bào) "Too many redirects"(重定向次數(shù)過多),或者直接返回 500 Internal Server Error。用戶在瀏覽器里看到的是一片空白或者錯(cuò)誤頁面。
根因
try_files是 Nginx 用來檢查文件是否存在的重要指令,但它和alias、rewrite、rewrite ^/(.*) $1 last等指令組合時(shí),行為往往出乎意料。
最常見的死循環(huán)配置:
location / {
root /data/www;
try_files $uri $uri/ /index.html;
}
這個(gè)配置的意圖是:先嘗試找對應(yīng)的文件,再找同名目錄下的 index.html,最后 fallback 到根目錄的 index.html??雌饋頉]問題。
但如果同時(shí)在另一個(gè) location 里用rewrite把請求重定向回去:
location / {
try_files $uri $uri/ /index.html;
}
location = /index.html {
root /data/www;
rewrite ^ / permanent; # 這行導(dǎo)致 /index.html 永久重定向到 /,死循環(huán)
}
另一種常見錯(cuò)誤是 try_files 和 alias 混用時(shí)的路徑問題:
location /static/ {
alias /data/static/;
try_files $uri /static/404.html;
# 當(dāng)文件不存在時(shí),try_files 會(huì)追加 /static/ 前綴去找 /static/404.html
# 但 alias 指令已經(jīng)映射了路徑,導(dǎo)致路徑拼接混亂
}
正確配置
try_files 的正確用法,關(guān)鍵是理解它按順序嘗試每個(gè)參數(shù),最后一個(gè)參數(shù)是 fallback URI:
# 基礎(chǔ)用法:先找文件,再找目錄 index,再 fallback
location / {
root /data/www;
index index.html index.htm;
try_files $uri $uri/ /fallback.html;
}
# PHP FastCGI 場景:找文件 -> 找目錄 -> 傳給 PHP
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ .php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# 帶命名的 fallback
location / {
try_files $uri @backend;
}
location @backend {
proxy_pass http://127.0.0.1:8080;
}
與 alias 配合時(shí)需要注意路徑不要重疊:
# alias 會(huì)改變 root 的映射關(guān)系,try_files 的 $uri 是基于 alias 后的路徑
location /static/ {
alias /data/static/;
try_files $uri =404;
# 不要寫成 try_files $uri /static/404.html,
# 因?yàn)?/static/ 這個(gè)前綴會(huì)被 alias 再次處理
}
驗(yàn)證方法
用 curl 模擬各種文件存在/不存在的場景:
# 測試文件存在的情況 curl -I http://localhost/static/exists.png # 期望:200 OK,有 Content-Type 頭 # 測試文件不存在的情況(看 fallback 是否生效) curl -I http://localhost/nonexistent/path # 期望:返回 fallback 的響應(yīng),狀態(tài)碼可能是 200 或 302 # 測試重定向循環(huán)(如果配置有誤,會(huì)在 curl 輸出中看到循環(huán)) curl -v http://localhost/ 2>&1 | grep -E"(< HTTP|< Location)" # 如果看到連續(xù)多個(gè) 302 -> / 的重定向,就是死循環(huán)了 # 在錯(cuò)誤日志中搜索 redirect 循環(huán) grep -i"redirect"/var/log/nginx/error.log | tail -20
風(fēng)險(xiǎn)提醒
try_files 的死循環(huán)問題在上線初期可能不會(huì)觸發(fā)——只有當(dāng)某些特定文件不存在、且 fallback 路徑又依賴于被 try_files 檢查的路徑時(shí)才會(huì)暴露。建議在上線前用不存在的路徑做一次全量測試:
# 批量測試不存在的路徑是否都有合理的 fallback
forpathin/api/noexist /static/noexist/file.js /images/noexist.png;do
status=$(curl -s -o /dev/null -w"%{http_code}"http://localhost${path})
echo"${path}->${status}"
done
# 期望:所有路徑都返回非 500、非 301 循環(huán)的有效響應(yīng)
踩坑點(diǎn)四:upstream keepalive 配置不足
現(xiàn)象
業(yè)務(wù)高峰期大量用戶反映接口響應(yīng)慢,甚至出現(xiàn) 502 Bad Gateway。但后端 Java/Python 服務(wù)的 CPU 和內(nèi)存使用率都不高,數(shù)據(jù)庫連接池也沒滿。用netstat查看網(wǎng)絡(luò)連接狀態(tài),發(fā)現(xiàn)服務(wù)器有大量 TIME_WAIT 連接:
netstat -an | awk'/:80s/ {print $NF}'| sort | uniq -c | sort -rn
# 輸出中 TIME_WAIT 數(shù)量成千上萬
同時(shí)后端 upstream 服務(wù)報(bào)錯(cuò)日志里出現(xiàn)了類似 "too many connections" 或 "connection refused" 的信息。
根因
這是 Nginx 與 upstream 之間使用短連接導(dǎo)致的問題。在沒有配置 keepalive 的情況下,Nginx 每轉(zhuǎn)發(fā)一個(gè)請求都會(huì)新建一個(gè) TCP 連接到 upstream,請求結(jié)束后連接立即關(guān)閉。這會(huì)產(chǎn)生兩個(gè)問題:
TIME_WAIT 堆積:大量短連接關(guān)閉后,端口會(huì)進(jìn)入 TIME_WAIT 狀態(tài)(默認(rèn)持續(xù) 60 秒),消耗文件描述符和端口號(hào)。在高并發(fā)場景下,如果 upstream 端口復(fù)用太快,可能出現(xiàn)端口耗盡。
后端連接數(shù)暴漲:Nginx worker 進(jìn)程數(shù) × 每個(gè) worker 的活躍請求數(shù) = 實(shí)際需要的 upstream 連接數(shù)。如果每個(gè)請求都新建連接,upstream 服務(wù)的連接池會(huì)很快耗盡。
典型錯(cuò)誤配置:
upstream backend {
server 127.0.0.1:8080;
# 沒有 keepalive 配置,每個(gè)請求都是新建連接
}
server {
location / {
proxy_pass http://backend;
# 沒有配置 proxy_http_version 1.1,也沒有 proxy_set_header Connection ""
}
}
正確配置
Nginx upstream keepalive 配置包含三個(gè)關(guān)鍵部分:
upstream backend {
server 127.0.0.1:8080 weight=5 max_fails=3 fail_timeout=30s;
# weight: 權(quán)重
# max_fails: 失敗多少次后認(rèn)為該 server 不可用
# fail_timeout: 失敗后多少秒內(nèi)不再向該 server 發(fā)請求
# 關(guān)鍵:開啟 keepalive 連接池
# keepalive 連接數(shù)建議設(shè)置為 worker_connections 的 10%~20%
keepalive 32;
keepalive_requests 1000;
keepalive_timeout 60s;
}
server {
listen 80;
# 必須使用 HTTP/1.1 才能支持 keepalive
proxy_http_version 1.1;
location / {
proxy_pass http://backend;
# 關(guān)鍵:清除 Connection 頭,讓連接保持 alive
proxy_set_header Connection "";
# 其他常用頭
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 超時(shí)配置
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
}
}
keepalive 32表示 Nginx 會(huì)為這個(gè) upstream 保持 32 個(gè)空閑長連接。當(dāng)請求進(jìn)來時(shí),Nginx 優(yōu)先使用空閑連接,只有當(dāng)空閑連接不夠用時(shí)才新建連接。如果配置了keepalive 0或不配置 keepalive,則每個(gè)請求都走新建的短連接。
驗(yàn)證方法
查看 upstream 連接狀態(tài),確認(rèn)連接被復(fù)用:
# 方法一:在 upstream 服務(wù)端查看連接數(shù)(如果 upstream 是 Node.js/Java 等,可以查看其連接池狀態(tài))
# 以 Java Spring Boot 為例,看日志中 HikariCP 的連接池狀態(tài)
# 方法二:在 Nginx 端查看連接狀態(tài)
ss -tn | grep :8080 | awk'{print $4}'| sort | uniq -c
# keepalive 正常時(shí),8080 端口的連接數(shù)會(huì)比較穩(wěn)定,不會(huì)隨 QPS 線性增長
# 方法三:查看 error_log 中是否有 upstream 報(bào)錯(cuò)
grep -i"upstream|connection"/var/log/nginx/error.log | tail -50
# 方法四:對比修改前后的 TIME_WAIT 數(shù)量
# 修改前:大量 TIME_WAIT
netstat -an | grep TIME_WAIT | wc -l
# 修改后:TIME_WAIT 數(shù)量大幅下降
一個(gè)簡單的壓測對比(用 ab 或 wrk):
# 用 ab 壓測,觀察連接建立情況 ab -n 1000 -c 100 http://localhost/api/test # 如果沒有 ab wrk -t10 -c100 -d30s http://localhost/api/test # 觀察結(jié)果中的 Time taken for tests、Requests per second # keepalive 生效后,QPS 應(yīng)該明顯提升,延遲明顯下降
風(fēng)險(xiǎn)提醒
keepalive 配置如果設(shè)置過大,會(huì)占用過多 Nginx 內(nèi)存,因?yàn)槊總€(gè) keepalive 連接都會(huì)占用一個(gè) socket 緩存。一般建議 keepalive 數(shù)量設(shè)為 worker_connections 的 10%~20%。另外,如果 upstream 服務(wù)不支持 HTTP/1.1(比如一些老的 RPC 服務(wù)),keepalive 不會(huì)生效。
踩坑點(diǎn)五:client_max_body_size 未設(shè)置或設(shè)置過小
現(xiàn)象
用戶上傳文件時(shí),Nginx 直接返回 413 Request Entity Too Large,但后端服務(wù)的文件上傳限制其實(shí)很大。運(yùn)維工程師去查后端配置,發(fā)現(xiàn)后端沒有做任何限制,困惑不已。
這是 Nginx 默認(rèn)的請求體大小限制在作祟:client_max_body_size 默認(rèn)值是 1MB。任何超過這個(gè)大小的請求,Nginx 會(huì)在讀取請求體的階段就直接拒絕,根本不會(huì)向后端轉(zhuǎn)發(fā)。
根因
Nginx 在接收請求體的階段就會(huì)檢查大小限制,如果超過 client_max_body_size,直接返回 413,不會(huì)到 proxy_pass 或 fastcgi_pass 那一層。這個(gè)限制是在 Nginx 層面硬攔截的。
常見錯(cuò)誤場景:
根本沒有配置 client_max_body_size,用的是默認(rèn)值 1MB,上傳圖片或文檔立刻超限
配置了 client_max_body_size 但位置不對——放在了 http 段而不是 server 段或 location 段
配置了但值設(shè)置得過小,沒有預(yù)估業(yè)務(wù)增長
正確配置
根據(jù)業(yè)務(wù)需求設(shè)置合理的請求體大小限制:
http {
# 全局默認(rèn)限制,可以設(shè)置得相對保守
client_max_body_size 10m;
server {
listen 80;
server_name example.com;
# 對特定業(yè)務(wù)設(shè)置更大的限制
location /upload/ {
# 文件上傳業(yè)務(wù),需要較大限制
client_max_body_size 100m;
# 同時(shí)調(diào)整超時(shí),因?yàn)榇笪募蟼骱臅r(shí)長
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_connect_timeout 75s;
proxy_pass http://upload-backend;
}
location /api/ {
# 普通 API 請求,1m 就夠了
client_max_body_size 1m;
proxy_pass http://api-backend;
}
# 也可以在 error_page 中自定義 413 錯(cuò)誤頁面
error_page 413 = /413.html;
location = /413.html {
root /data/www/errors;
internal;
}
}
}
驗(yàn)證方法
用 dd 生成指定大小的測試文件,然后上傳:
# 生成一個(gè) 2MB 的測試文件
ddif=/dev/zero of=/tmp/test_2mb.bin bs=1M count=2
# 生成一個(gè) 15MB 的測試文件
ddif=/dev/zero of=/tmp/test_15mb.bin bs=1M count=15
# 測試上傳 2MB 文件(應(yīng)該成功)
curl -X POST -F"file=@/tmp/test_2mb.bin"http://localhost/upload/ -w"
HTTP Status: %{http_code}
"
# 測試上傳 15MB 文件(如果限制是 10m,應(yīng)該返回 413)
curl -X POST -F"file=@/tmp/test_15mb.bin"http://localhost/upload/ -w"
HTTP Status: %{http_code}
"
# 觀察 error_log 中的 413 記錄
grep"client intended to send too large body"/var/log/nginx/error.log | tail -10
風(fēng)險(xiǎn)提醒
設(shè)置過大的 client_max_body_size 會(huì)帶來安全風(fēng)險(xiǎn)——攻擊者可能通過上傳超大文件來耗盡服務(wù)器磁盤或內(nèi)存。最佳實(shí)踐是:
根據(jù)實(shí)際業(yè)務(wù)需求設(shè)置,寧可保守也不要太大
配合后端做二次校驗(yàn),前端限制不可信
上傳目錄單獨(dú)掛載,限制磁盤配額
配合 rate limiting 防止頻繁上傳
# 配合 limit_rate 限制上傳速度,防止惡意占用帶寬
location /upload/ {
client_max_body_size 100m;
limit_rate 1m; # 限制上傳速度 1MB/s
proxy_pass http://upload-backend;
}
踩坑點(diǎn)六:gzip 壓縮配置不當(dāng)
現(xiàn)象
服務(wù)器帶寬使用率很高,CPU 負(fù)載也上去了,但用戶反映頁面加載還是很慢。排查發(fā)現(xiàn)雖然配置了 gzip,但 response header 里沒有 Content-Encoding: gzip。用瀏覽器的開發(fā)者工具看 network 面板,發(fā)現(xiàn)傳輸?shù)奈募w積很大,沒有被壓縮。
或者反過來,gzip_comp_level 設(shè)置得太高,CPU 占用率直接拉滿,壓縮效果卻沒提升多少。
根因
gzip 是 Nginx 中提升傳輸效率的重要手段,但默認(rèn) gzip 是關(guān)閉的,而且 gzip_types 默認(rèn)只包含 text/html。如果只加了gzip on;就以為萬事大吉了,但實(shí)際上大部分請求的 MIME 類型沒有被加入壓縮列表。
另一個(gè)常見問題是 gzip_vary 頭沒有開啟,導(dǎo)致代理緩存(如 CDN、Varnish)給不同客戶端返回了不正確的壓縮版本。
正確配置
一個(gè)完整的 gzip 配置:
http {
# 開啟 gzip
gzip on;
# 壓縮級(jí)別:1(最快,壓縮率低)到 9(最慢,壓縮率高),默認(rèn) 1
# 生產(chǎn)環(huán)境建議 4~5,平衡 CPU 開銷和壓縮率
gzip_comp_level 5;
# gzip_buffers 定義壓縮時(shí)使用的緩存大小
gzip_buffers 16 8k;
# gzip_http_version 指定支持的 HTTP 版本
gzip_http_version 1.1;
# 開啟 Vary 頭,代理緩存要識(shí)別 User-Agent
gzip_vary on;
# 限制最小壓縮長度,太小的內(nèi)容壓縮反而增加開銷
gzip_min_length 1024;
# 對指定 MIME 類型啟用壓縮
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml
application/xml+rss
application/x-javascript
application/octet-stream
image/svg+xml;
# gzip 對部分代理緩存的兼容性配置
# 確保代理層能正確處理壓縮和非壓縮響應(yīng)
server {
listen 80;
location /static/ {
# 靜態(tài)資源在響應(yīng)頭里加 Expires,瀏覽器會(huì)緩存
expires 7d;
add_header Cache-Control "public, no-transform";
# 關(guān)閉 access_log 減少 IO
access_log off;
}
location /api/ {
proxy_pass http://backend;
}
}
}
gzip 壓縮的效果通常非常顯著——JSON 響應(yīng)可以壓縮到原來的 20%~30%,CSS/JS 可以壓縮到 30%~40%。但注意:
圖片(PNG、JPG、WebP)和視頻、音頻不要壓縮——這些格式本身就是壓縮格式,gzip 反而徒增 CPU
太小的文件(< 1KB)不值得壓縮,壓縮開銷可能大于節(jié)省的傳輸量
不要對已壓縮的內(nèi)容二次壓縮
驗(yàn)證方法
# 用 curl 的 range 頭或 Accept-Encoding 測試壓縮 curl -I -H"Accept-Encoding: gzip"http://localhost/api/data 2>/dev/null # 看響應(yīng)頭是否有: # Content-Encoding: gzip # Vary: Accept-Encoding # 看原始響應(yīng)大小 vs 壓縮后大小 # 先獲取原始大小 curl -s http://localhost/api/data | wc -c # 獲取 gzip 后大小 curl -s -H"Accept-Encoding: gzip"http://localhost/api/data | wc -c # 用 ab 壓測,對比開啟/關(guān)閉 gzip 的 QPS ab -n 1000 -c 10 -H"Accept-Encoding: gzip"http://localhost/api/data # 查看 gzip 統(tǒng)計(jì)(需要編譯時(shí)帶了 --with-http_gzip_static_module) # 在 error_log 中會(huì)有相關(guān)統(tǒng)計(jì)輸出
風(fēng)險(xiǎn)提醒
gzip_comp_level 超過 6 之后,壓縮率提升非常有限,但 CPU 消耗成倍增加。生產(chǎn)環(huán)境慎用高壓縮級(jí)別。
有些老版本 CDN 或代理服務(wù)不理解 gzip Vary 頭,可能導(dǎo)致緩存錯(cuò)亂。如果使用了 CDN,需要確認(rèn) CDN 是否支持 Vary: Accept-Encoding。
gzip 對 CPU 的消耗在高并發(fā)場景下不可忽視。如果服務(wù)器 CPU 本來就很緊張,可以適當(dāng)降低 gzip_comp_level 或減少 gzip_types。
踩坑點(diǎn)七:SSL/TLS 配置不完整
現(xiàn)象
用戶在瀏覽器訪問網(wǎng)站時(shí),看到「您的連接不是私密連接」或者證書錯(cuò)誤頁面。用curl -v https://example.com顯示證書鏈不完整,或者只配了證書文件沒有配中間證書(chain cert)。又或者雖然配置了 SSL,但用的加密套件是 TLS 1.0,已被主流瀏覽器標(biāo)記為不安全。
根因
HTTPS 配置看似簡單,但坑很多。常見的錯(cuò)誤包括:
證書鏈不完整:只配了公鑰證書(server.crt),沒有配中間證書(chain.cert)。瀏覽器在驗(yàn)證證書鏈時(shí)找不到中間 CA,就會(huì)報(bào)錯(cuò)。
私鑰和證書不匹配:證書是用另一個(gè)私鑰簽發(fā)的。
使用了不安全的協(xié)議版本:啟用了 SSLv3、TLS 1.0、TLS 1.1,這些已被廢棄。
使用了不安全的加密套件:比如 RC4、3DES,或者允許 NULL 加密。
證書文件路徑不存在:Let's Encrypt 證書續(xù)期后路徑變了,但 Nginx 配置沒更新。
正確配置
一個(gè)安全且完整的 HTTPS 配置:
server {
listen 443 ssl http2;
server_name example.com;
# 證書文件(公鑰 + 中間證書要合并在一個(gè)文件里,或者分別指定)
ssl_certificate /etc/nginx/ssl/example.com.fullchain.pem;
# 私鑰文件
ssl_certificate_key /etc/nginx/ssl/example.com.key;
# TLS 版本控制,禁用不安全的舊版本
ssl_protocols TLSv1.2 TLSv1.3;
# 安全加密套件配置(推薦使用 Mozilla 的現(xiàn)代套件)
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256ECDHE-ECDSA-AES256-GCM-SHA384ECDHE-ECDSA-CHACHA20-POLY1305DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers on;
# 開啟 OCSP Stapling,加快證書驗(yàn)證速度
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# 證書有效期提醒(可選項(xiàng),Nginx 1.19.10+ 支持)
# ssl_conf_command CertificateSpkiCheck on;
# HSTS 頭(嚴(yán)格傳輸安全,啟用前確保全站 HTTPS)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# 其他安全響應(yīng)頭
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
location / {
root /data/www;
index index.html;
}
}
# HTTP 到 HTTPS 的強(qiáng)制跳轉(zhuǎn)
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
關(guān)于證書鏈的合并:
# 正確的順序:服務(wù)器證書 -> 中間證書 -> 根證書(根證書可選) # Let's Encrypt 證書合并方式: cat /etc/letsencrypt/live/example.com/fullchain.pem > /etc/nginx/ssl/example.com.fullchain.pem cat /etc/letsencrypt/live/example.com/privkey.pem > /etc/nginx/ssl/example.com.key.pem # 驗(yàn)證證書鏈?zhǔn)欠裢暾?openssl s_client -connect example.com:443 -servername example.com # 看 "Certificate chain" 部分是否完整
驗(yàn)證方法
# 檢查證書信息 openssl s_client -connect localhost:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates -subject -issuer # 檢查證書鏈 openssl s_client -connect localhost:443 -showcerts /dev/null | grep -E"subject=|issuer=" # 使用 curl 驗(yàn)證(curl 會(huì)檢查證書鏈完整性) curl -v https://localhost/api/ 2>&1 | grep -E"SSL|HTTP|Server: # 使用 testssl.sh(全量 TLS 安全檢測工具)檢查所有加密套件和協(xié)議版本 testssl.sh --protocols --ciphers --headers https://example.com # 瀏覽器開發(fā)者工具 -> Security 面板 -> 查看證書詳情 # 在 Chrome 中訪問 chrome://flags 搜索 "Certificate" 看有沒有相關(guān)警告 # 檢查 SSL Labs 評(píng)分(在線工具) # 訪問 https://www.ssllabs.com/ssltest/ 輸入域名查看評(píng)級(jí)
風(fēng)險(xiǎn)提醒
購買或續(xù)期證書后,一定要測試openssl s_client -connect確認(rèn)證書鏈完整再上線
啟用 HSTS(Strict-Transport-Security)后,用戶瀏覽器會(huì)強(qiáng)制 HTTPS,且 max-age 設(shè)置很長。如果證書有問題,影響范圍會(huì)很大。建議先用較短的 max-age 測試,確認(rèn)無誤后再放大。
TLS 1.3 在 Nginx 1.13+ 支持,但如果 client 是舊版 Android 或 Java 6/7,可能不支持。如果業(yè)務(wù)用戶群體中有這種情況,需要保留 TLS 1.2。
踩坑點(diǎn)八:worker_processes 與 worker_connections 配合錯(cuò)誤
現(xiàn)象
Nginx 配置里worker_connections設(shè)置得很大,比如 65535,但實(shí)際并發(fā)量稍微高一點(diǎn)就開始 502。用netstat或ss查看連接數(shù),明明遠(yuǎn)遠(yuǎn)沒到 65535,Nginx 卻報(bào) "too many connections"。這是系統(tǒng)層的文件描述符限制沒有同步調(diào)整導(dǎo)致的。
根因
Nginx 每個(gè) worker 進(jìn)程能夠處理的最大連接數(shù)由worker_connections控制(默認(rèn) 512)。但這個(gè)值受限于操作系統(tǒng)的文件描述符上限(ulimit -n)。如果系統(tǒng)的文件描述符上限只有 1024,而 Nginx 的 worker_connections 設(shè)置的是 65535,Nginx 實(shí)際能打開的連接數(shù)也只有 1024 左右。
同時(shí),Nginx 處理連接時(shí),每個(gè)連接除了占用一個(gè)文件描述符,還需要分配一定的內(nèi)存。如果內(nèi)存不足,大并發(fā)也會(huì)出問題。
正確配置
分兩層配置:
Nginx 配置層:
# /etc/nginx/nginx.conf
worker_processes auto; # 自動(dòng)等于 CPU 核心數(shù)
worker_rlimit_nofile 65535; # 允許每個(gè) worker 打開的最大文件描述符數(shù)
events {
# 每個(gè) worker 的最大連接數(shù),受限于系統(tǒng) ulimit -n
worker_connections 65535;
# 使用 epoll(Linux)提高并發(fā)處理效率,F(xiàn)reeBSD 用 kqueue
use epoll;
# 允許一次接受多個(gè)連接
multi_accept on;
}
http {
# 打開文件緩存,減少磁盤 IO
open_file_cache max=65535 inactive=60s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
# 客戶端keepalive超時(shí)
keepalive_timeout 65;
# 單客戶端最大請求數(shù)(防止單個(gè)客戶端占用過多連接)
keepalive_requests 1000;
}
系統(tǒng)配置層:
# 查看當(dāng)前文件描述符限制 ulimit-n # 臨時(shí)修改(立即生效,但重啟后失效) ulimit-n 65535 # 永久修改(編輯 /etc/security/limits.conf) # 在文件末尾添加: # * soft nofile 65535 # * hard nofile 65535 # root soft nofile 65535 # root hard nofile 65535 # 編輯 /etc/sysctl.conf(修改網(wǎng)絡(luò)參數(shù)) echo"fs.file-max = 1000000">> /etc/sysctl.conf sysctl -p # 編輯 /etc/pam.d/common-session(確保 PAM 讀取 limits.conf) # 確認(rèn)有:session required pam_limits.so # 修改 nginx systemd 服務(wù)文件(如果用 systemd 管理) # /lib/systemd/system/nginx.service # 在 [Service] 段添加: # LimitNOFILE=65535 # 然后執(zhí)行:systemctl daemon-reload && systemctl restart nginx
系統(tǒng)層面的完整調(diào)優(yōu):
# /etc/sysctl.conf 網(wǎng)絡(luò)相關(guān)參數(shù) cat >> /etc/sysctl.conf <'EOF' # 網(wǎng)絡(luò)連接追蹤 net.netfilter.nf_conntrack_max = 1048576 net.nf_conntrack_max = 1048576 # TIME_WAIT 復(fù)用 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_timestamps = 1 # 監(jiān)聽隊(duì)列長度 net.core.somaxconn = 65535 net.core.netdev_max_backlog = 65535 # 本地端口范圍 net.ipv4.ip_local_port_range = 1024 65535 # 內(nèi)存優(yōu)化(視實(shí)際情況) net.ipv4.tcp_mem = 786432 1048576 1572864 net.ipv4.tcp_rmem = 4096 87380 16777216 net.ipv4.tcp_wmem = 4096 65536 16777216 EOF sysctl -p
驗(yàn)證方法
# 查看 Nginx worker 進(jìn)程的文件描述符使用情況 ps -p $(pgrep nginx | head -1) -o pid,comm,nlwp,drs # nlwp = number of light-weight processes/threads # 查看實(shí)際打開的文件描述符數(shù)量 ls /proc/$(pgrep -f"nginx: worker"| head -1)/fd | wc -l # 查看系統(tǒng)級(jí)別的文件描述符限制 cat /proc/sys/fs/file-max ulimit-n # 壓測驗(yàn)證配置生效 # 用 ss 觀察并發(fā)連接數(shù)上限 ss -s # 或 ab 壓測 ab -n 10000 -c 5000 http://localhost/api/test # 觀察 error_log 中是否有 "too many connections" 錯(cuò)誤 grep"too many connections"/var/log/nginx/error.log
風(fēng)險(xiǎn)提醒
文件描述符設(shè)置過大可能導(dǎo)致系統(tǒng)內(nèi)存泄漏或耗盡。每個(gè)文件描述符在 Linux 內(nèi)核中都有對應(yīng)的 struct file 結(jié)構(gòu),約占 2KB 內(nèi)存。設(shè)置 65535 個(gè)約需 130MB 物理內(nèi)存。
somaxconn 設(shè)置過大可能在遭受 SYN Flood 攻擊時(shí)放大攻擊效果。根據(jù)實(shí)際業(yè)務(wù)調(diào)整。
系統(tǒng)層面的修改需要 root 權(quán)限,修改前務(wù)必記錄原值,修改后立即驗(yàn)證,異常時(shí)能快速回滾。
踩坑點(diǎn)九:日志配置不當(dāng)導(dǎo)致磁盤爆滿
現(xiàn)象
服務(wù)器突然變得很慢,SSH 登錄后敲命令卡頓。用df -h一看,根分區(qū) 100% 滿了。進(jìn)一步排查發(fā)現(xiàn)/var/log/nginx/下的 access.log 或 error.log 達(dá)到了幾十 GB。上一次 logrotate 執(zhí)行不知道什么時(shí)候,或者是 logrotate 配置根本沒有生效。
根因
Nginx 的 access_log 默認(rèn)對每個(gè)請求都寫一行,高并發(fā)場景下日志量增長極快。如果 access_log 沒有配置合理的輪轉(zhuǎn)策略,磁盤空間遲早會(huì)被耗盡。
常見問題:
access_log 記錄了太多無用信息(特別是啟用了詳細(xì)的 log_format 包含大量 header)
error_log 級(jí)別設(shè)置為 debug,產(chǎn)生了巨量調(diào)試日志
logrotate 配置了但頻率太低(每天一次,而日志每小時(shí)就能寫滿磁盤)
日志被寫到了系統(tǒng)根分區(qū)而不是獨(dú)立掛載的日志分區(qū)
壓縮后的舊日志沒有被刪除
正確配置
Nginx 日志配置:
http {
# 定義日志格式(不要記錄過長的 header 內(nèi)容)
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" urt="$upstream_response_time"';
# 訪問日志(生產(chǎn)環(huán)境建議按虛擬主機(jī)拆分)
access_log /var/log/nginx/access.log main buffer=16k flush=2m;
# 錯(cuò)誤日志(生產(chǎn)環(huán)境不要用 debug,會(huì)寫大量日志)
error_log /var/log/nginx/error.log warn;
# Gzip 壓縮日志(減少日志體積)
# 需要在 http 或 server 段開啟
# gzip off; # 如果不需要 gzip 日志可以關(guān)閉
}
server {
server_name example.com;
access_log /var/log/nginx/example.com.access.log main;
error_log /var/log/nginx/example.com.error.log;
# 關(guān)閉不需要的日志,減少 IO
location /health {
access_log off;
return 200 "OK";
}
# 靜態(tài)資源可以關(guān)閉 access_log
location /static/ {
root /data/www;
access_log off;
expires 7d;
}
}
logrotate 配置:
# /etc/logrotate.d/nginx
/var/log/nginx/*.log{
daily # 每天輪轉(zhuǎn)一次
missingok # 日志不存在也不報(bào)錯(cuò)
rotate 14 # 保留 14 份舊日志
compress # 壓縮舊日志(gzip)
delaycompress # 延遲壓縮,等下一次輪轉(zhuǎn)再壓縮
notifempty # 空日志不輪轉(zhuǎn)
create 0640 nginx nginx # 輪轉(zhuǎn)后創(chuàng)建新文件的權(quán)限
sharedscripts # 所有日志輪轉(zhuǎn)完再執(zhí)行一次 postrotate
# 向 master 進(jìn)程發(fā) reload 信號(hào),而不是 restart
postrotate
if[ -f /var/run/nginx.pid ];then
kill-USR1 $(cat /var/run/nginx.pid)
fi
endscript
}
定時(shí)清理過大的日志文件(緊急處理用):
# 如果磁盤已經(jīng)滿了,先緊急清理日志釋放空間 # 1. 先找到最大的日志文件 du -sh /var/log/nginx/*.log| sort -rh | head -10 # 2. 截?cái)嗳罩疚募ú皇莿h除,避免 Nginx 還在寫這個(gè) inode) # 危險(xiǎn)操作,先確認(rèn)你要截?cái)嗟氖悄膫€(gè)文件 truncate -s 0 /var/log/nginx/access.log # 3. 如果 Nginx 還在寫,用 kill -USR1 通知它重新打開日志 kill-USR1 $(cat /var/run/nginx.pid) # 4. 確認(rèn)磁盤空間釋放 df -h /var/log
驗(yàn)證方法
# 檢查日志輪轉(zhuǎn)是否正常工作
logrotate -d /etc/logrotate.d/nginx # 模擬執(zhí)行(不實(shí)際輪轉(zhuǎn))
# 檢查日志大小
ls -lh /var/log/nginx/
# 檢查磁盤使用情況(按大小排序)
du -ah /var/log/ | sort -rh | head -20
# 監(jiān)控系統(tǒng)日志目錄的大小增長
watch -n 5"df -h /var/log; ls -lhS /var/log/nginx/*.log"
# 開啟日志后定期檢查是否有異常(比如某個(gè) IP 頻繁請求導(dǎo)致日志暴增)
awk'{print $1}'/var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
# 統(tǒng)計(jì)訪問量最大的 IP,如果某個(gè) IP 請求量異常大,可能是被爬或被攻擊
風(fēng)險(xiǎn)提醒
絕對不要rm刪除正在被 Nginx 寫入的日志文件——?jiǎng)h除后 Nginx 會(huì)繼續(xù)寫這個(gè)已被刪除的 inode,導(dǎo)致磁盤空間持續(xù)消耗且無法通過刪除文件釋放。正確做法是truncate -s 0或> /var/log/nginx/access.log。
設(shè)置 logrotate 時(shí)確認(rèn)postrotate里發(fā)的是-USR1信號(hào)而非-HUP或 restart。-USR1讓 Nginx 重新打開日志文件,不中斷現(xiàn)有連接;-HUP會(huì)重載配置,可能觸發(fā) worker 進(jìn)程重啟。
access_log 關(guān)掉后無法做流量分析和異常排查,建議只關(guān)靜態(tài)資源的日志,保留 API 和核心業(yè)務(wù)的 access_log。
踩坑點(diǎn)十:隱藏 server 塊版本號(hào)未生效
現(xiàn)象
用curl -I http://example.com或?yàn)g覽器開發(fā)者工具查看響應(yīng)頭,發(fā)現(xiàn)Server: nginx/1.18.0(或具體版本號(hào))。滲透測試報(bào)告里指出這是信息泄露漏洞——攻擊者可以據(jù)此判斷 Nginx 版本,從而查找對應(yīng)的 CVE 漏洞。
你明明在 nginx.conf 里配置了server_tokens off;,但版本號(hào)還在顯示。
根因
server_tokens off;的作用范圍是有限的,它只關(guān)閉 Nginx 在 error_page 響應(yīng)和 Location 響應(yīng)頭中的版本號(hào)顯示,但如果你通過include /etc/nginx/conf.d/*.conf加載了其他 server 塊文件,或者使用了第三方模塊(如 OpenResty),配置可能不生效。
另一個(gè)常見問題是:server_tokens off 在 http 段配置了,但 server 段里又重寫了 error_page,或者 upstream 響應(yīng)頭里還帶著版本信息。
正確配置
基礎(chǔ)配置:
http {
# 關(guān)閉版本號(hào)顯示(在 error 頁面和響應(yīng)頭中都生效)
server_tokens off;
# 如果需要完全自定義 Server 頭,可以用這個(gè)(需要 ngx_http_headers_more 模塊)
# more_set_headers 'Server: MyServer';
server {
listen 80;
server_name example.com;
# 自定義錯(cuò)誤頁面,同時(shí)隱藏版本
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
location / {
root /data/www;
}
# 自定義錯(cuò)誤頁面內(nèi)容,避免暴露 Nginx 版本
location = /50x.html {
root /data/www/errors;
}
}
}
完全隱藏 Nginx 標(biāo)識(shí)(需要編譯第三方模塊):
Nginx 默認(rèn)的 Server 頭由 HttpHeadersMore 模塊控制,但開源版 Nginx 本身不提供關(guān)閉 Server 頭的方法。如果需要完全隱藏,可以:
使用 OpenResty 的 header 電子:
# 需要安裝 OpenResty 或 ngx_http_headers_more 模塊
header_filter_by_lua_block {
ngx.header.server = "MyServer"
}
或者在代碼層面——后端服務(wù)在響應(yīng)頭里覆蓋:
# 后端服務(wù)返回的響應(yīng)頭會(huì)帶上自定義 Server proxy_set_header Server "MyServer";
編譯時(shí)修改:
# 編譯 Nginx 時(shí)指定默認(rèn) Server 頭
./configure --with-http_realip_module
--with-http_stub_status_module
--http-client-body-temp-path=/var/lib/nginx/client-body
--http-proxy-temp-path=/var/lib/nginx/proxy
--with-http_gzip_static_module
--add-module=# 第三方模塊路徑
# 編譯后修改 Objs/nginx.c 中的默認(rèn)字符串
# 查找 "ngx_http_server_string[]" 并修改 "nginx/" 為自定義值
驗(yàn)證方法
# 查看響應(yīng)頭中的 Server 字段 curl -I http://localhost/ 2>/dev/null | grep -i server # 期望輸出只有 Server: nginx,沒有版本號(hào) # 測試 error_page 響應(yīng)是否還帶版本號(hào) curl -s http://localhost/nonexistent_path | grep -i"server|nginx" # 期望:不包含 nginx 版本號(hào) # 檢查是否有其他地方泄露了版本信息 # 比如 upstream 返回的響應(yīng)頭 curl -I http://localhost/api/ 2>/dev/null | grep -i server curl -s http://localhost/api/ -H"Accept: text/html"| grep -i"nginx|server" # 用 nikto 掃描(Nginx 指紋識(shí)別工具) nikto -h http://localhost -Friendly
風(fēng)險(xiǎn)提醒
完全隱藏 Server 頭在實(shí)際安全收益上有限——有經(jīng)驗(yàn)的攻擊者通過 SSL 證書、對特定路徑的響應(yīng)特征等方式依然可以識(shí)別出 Nginx。但如果合規(guī)要求(如等保)要求隱藏版本號(hào),這是必要的配置。
如果使用 OpenResty 或第三方模塊,確保該模塊來源可信,避免引入新的安全風(fēng)險(xiǎn)。
修改 Server 頭后,如果業(yè)務(wù)依賴第三方安全掃描工具判斷 Web 服務(wù)器類型,可能導(dǎo)致掃描工具誤報(bào)。記得同步更新資產(chǎn)清單。
綜合排查路徑:502/504 故障排查流程
當(dāng) Nginx 返回 502 Bad Gateway 或 504 Gateway Timeout 時(shí),按以下路徑逐層排查:
排查路徑圖
Nginx 502/504 ├── 1. 檢查 upstream 是否存活 │ ├── curl 本地測試后端端口是否響應(yīng) │ │ curl http://127.0.0.1:8080/health │ └── 檢查 upstream 進(jìn)程狀態(tài)和端口監(jiān)聽 │ ps aux | grep backend │ ss -tlnp | grep 8080 │ ├── 2. 檢查 Nginx 與 upstream 之間的網(wǎng)絡(luò)連通性 │ ├── telnet 127.0.0.1 8080 │ ├── ping 127.0.0.1 │ └── iptables/selinux 是否攔截 │ ├── 3. 檢查 upstream 連接數(shù)和超時(shí)配置 │ ├── upstream keepalive 是否足夠 │ ├── proxy_connect_timeout 是否太小 │ ├── proxy_read_timeout 是否太小 │ └── upstream 服務(wù)的連接池是否耗盡 │ ├── 4. 檢查 upstream 進(jìn)程/容器狀態(tài) │ ├── 進(jìn)程是否 OOM 被殺 │ │ dmesg | grep -i oom │ │ journalctl -u backend --since"10 minutes ago" │ ├── 容器是否被重啟 │ │ docker ps -a | grep backend │ └── Kubernetes: pod 狀態(tài) │ kubectl get pods -n default | grep backend │ ├── 5. 檢查 error_log 中的具體錯(cuò)誤信息 │ ├──"connect() failed" │ ├──"Connection refused" │ ├──"Connection timed out" │ └──"no live upstreams" │ └── 6. 檢查 Nginx upstream 配置是否正確 ├── upstream 塊中的 server 地址是否正確 ├── proxy_pass 是否指向了正確的 upstream └── upstream 是否所有 server 都 down 了
常用診斷命令匯總
# 一分鐘內(nèi)快速診斷 502 問題
echo"=== 1. Nginx error_log ==="&& tail -100 /var/log/nginx/error.log | grep -i"502|upstream|connect"
echo"=== 2. Nginx upstream 進(jìn)程狀態(tài) ==="&& ps aux | grep -E"java|node|python|php"| grep -v grep
echo"=== 3. 端口監(jiān)聽狀態(tài) ==="&& ss -tlnp | grep -E"8080|3000|5000|9000|3306"
echo"=== 4. 本地 upstream 健康檢查 ==="&& curl -s -o /dev/null -w"%{http_code}"http://127.0.0.1:8080/health
echo"=== 5. 系統(tǒng)資源 ==="&& free -h && df -h / && uptime
echo"=== 6. 近期系統(tǒng)日志(OOM 等)==="&& dmesg | tail -50 | grep -iE"oom|killed|nginx|java|node"
特殊狀態(tài)碼處理
| 狀態(tài)碼 | 含義 | 常見原因 | 排查命令 |
|---|---|---|---|
| 400 | Bad Request | 請求頭過大、語法錯(cuò)誤 | grep "400" /var/log/nginx/error.log |
| 413 | Request Entity Too Large | client_max_body_size 超限 | 增大 client_max_body_size |
| 444 | Nginx 特有,連接直接關(guān)閉 | 被配置攔截,未返回響應(yīng) | 常見于配置了if (condition) { return 444; } |
| 499 | Client Closed Request | 客戶端主動(dòng)斷開,upstream 處理過慢 | 增加 proxy_read_timeout |
| 502 | Bad Gateway | upstream 進(jìn)程掛了或無響應(yīng) | 逐層檢查 upstream 進(jìn)程和端口 |
| 503 | Service Temporarily Unavailable | upstream 達(dá)到 max_fails 暫時(shí)不可用 | 等 30s 自動(dòng)恢復(fù)或檢查 fail_timeout |
| 504 | Gateway Timeout | upstream 處理超時(shí) | 增加 proxy_read_timeout / proxy_send_timeout |
生產(chǎn)環(huán)境變更 checklist
每次 Nginx 配置變更前,必須完成以下檢查項(xiàng):
變更前
# 1. 備份當(dāng)前配置 cp -r /etc/nginx /etc/nginx.bak.$(date +%Y%m%d%H%M%S) # 2. 在測試環(huán)境驗(yàn)證語法 nginx -t -c /etc/nginx/nginx.conf # 3. 確認(rèn)變更影響的站點(diǎn)和路徑 grep -n"proxy_pass|upstream|listen"/etc/nginx/conf.d/*.conf # 4. 確認(rèn)新配置中的 IP、端口、路徑?jīng)]有錯(cuò)誤 # 特別是 upstream 的 server 地址和端口 # 5. 準(zhǔn)備回滾腳本 cp /etc/nginx/nginx.conf /tmp/nginx.conf.backup
變更中
# 1. 部署新配置文件 cp /path/to/new/nginx.conf /etc/nginx/nginx.conf # 2. 驗(yàn)證語法 nginx -t # 3. reload 而非 restart nginx -s reload # 4. 確認(rèn) master 進(jìn)程還在,worker 進(jìn)程正常 ps aux | grep nginx # 5. 立即觀察 error_log tail -f /var/log/nginx/error.log
變更后
# 1. 全量健康檢查(測試所有站點(diǎn))
curl -s -o /dev/null -w"%{http_code} %{url_effective}
"
http://localhost/
http://localhost/api/
http://localhost/static/
# 2. 監(jiān)控 502/504 錯(cuò)誤率(變更后 5 分鐘內(nèi)每分鐘檢查一次)
foriin{1..5};do
echo"=== Check$i==="
grep -c"502|504"/var/log/nginx/access.log
sleep 60
done
# 3. 確認(rèn)新舊 worker 進(jìn)程平滑切換(舊 worker 優(yōu)雅退出)
ps aux | grep"nginx: worker"| wc -l
# 確認(rèn) worker 數(shù)量等于配置中的 worker_processes * worker_connections 的 worker 數(shù)
# 4. 如果有問題,立即回滾
cp /tmp/nginx.conf.backup /etc/nginx/nginx.conf
nginx -t && nginx -s reload
回滾方案
#!/bin/bash # rollback_nginx.sh - 緊急回滾腳本 BACKUP_FILE="/tmp/nginx.conf.backup" NGINX_CONF="/etc/nginx/nginx.conf" if[ ! -f"$BACKUP_FILE"];then echo"ERROR: Backup file not found:$BACKUP_FILE" exit1 fi echo"Rolling back Nginx configuration..." cp"$BACKUP_FILE""$NGINX_CONF" ifnginx -t 2>&1 | grep -q"syntax is ok";then nginx -s reload echo"Rollback successful. Nginx reloaded." else echo"ERROR: Rollback config failed syntax check. Manual intervention required." exit1 fi
總結(jié)
Nginx 配置的十個(gè)踩坑點(diǎn),總結(jié)起來有一個(gè)共同規(guī)律:Nginx 的配置項(xiàng)之間不是孤立的,而是相互影響、相互制約的。proxy_pass 的尾部斜杠影響路徑傳遞,location 的修飾符決定匹配順序和正則行為,worker_processes 和系統(tǒng) ulimit 共同決定實(shí)際并發(fā)上限,gzip 壓縮和 CPU 消耗是一對矛盾體。
一個(gè)合格的運(yùn)維工程師,不僅要記住每個(gè)配置項(xiàng)怎么寫,更要理解配置項(xiàng)之間的協(xié)作關(guān)系。以下幾點(diǎn)核心原則,貫穿了所有的踩坑場景:
原則一:先測試后上線。nginx -t能排除大部分語法錯(cuò)誤,但不是所有。上線前在測試環(huán)境完整跑一遍業(yè)務(wù)流程,用 curl 覆蓋所有關(guān)鍵路徑。
原則二:小步快走,留好回滾。每次變更只改一個(gè)配置項(xiàng),改完后立即驗(yàn)證。多個(gè)配置項(xiàng)一起改,出問題都不知道是哪一項(xiàng)導(dǎo)致的。
原則三:關(guān)注資源邊界,不只看配置值。worker_connections 再大,系統(tǒng) ulimit 不夠也用不上;client_max_body_size 再大,磁盤滿了也存不了。系統(tǒng)層和 Nginx 配置層要一起調(diào)。
原則四:日志是運(yùn)維的眼睛。access_log 和 error_log 不只是出問題時(shí)才看,日常要看基線、觀趨勢,發(fā)現(xiàn)異常苗頭及時(shí)處理。
原則五:安全配置要完整。HTTPS 證書鏈、TLS 版本、加密套件、版本隱藏,這些不只是安全合規(guī)要求,也是運(yùn)維的基本功。
Nginx 的配置不算復(fù)雜,指令也就幾十個(gè),但用對、用好、用穩(wěn),需要在實(shí)踐中不斷積累經(jīng)驗(yàn)。每一個(gè)踩過的坑,都是真實(shí)生產(chǎn)環(huán)境里的教訓(xùn)。希望這十個(gè)場景能幫你少走彎路,遇到問題時(shí)能快速定位、精準(zhǔn)修復(fù)。
-
Web
+關(guān)注
關(guān)注
2文章
1311瀏覽量
75089 -
服務(wù)器
+關(guān)注
關(guān)注
14文章
10399瀏覽量
91801 -
nginx
+關(guān)注
關(guān)注
0文章
199瀏覽量
13235
原文標(biāo)題:線上頻發(fā)故障:Nginx 典型配置錯(cuò)誤復(fù)盤與優(yōu)化
文章出處:【微信號(hào):magedu-Linux,微信公眾號(hào):馬哥Linux運(yùn)維】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評(píng)論請先 登錄
Linux運(yùn)維Nginx軟件優(yōu)化之安全優(yōu)化
Linux運(yùn)維Nginx軟件優(yōu)化之Nginx性能優(yōu)化
Linux運(yùn)維Nginx軟件優(yōu)化之日志優(yōu)化
nginx錯(cuò)誤頁面配置
主要學(xué)習(xí)下nginx的安裝配置
運(yùn)行nginx所需的最低配置
每個(gè)人都要會(huì)的復(fù)盤知識(shí)
Nginx常用的配置和基本功能講解
nginx負(fù)載均衡配置介紹
如何通過優(yōu)化Nginx配置來提高網(wǎng)絡(luò)環(huán)境的安全性
Nginx配置終極指南
Nginx典型配置錯(cuò)誤復(fù)盤與優(yōu)化
評(píng)論