一、proxy_pass 末尾 / 的差异
两段配置看起来几乎一样,转发结果完全不同:
location /api/ { proxy_pass http://backend/;}location /api/ { proxy_pass http://backend;}
简单记:带 / 是替换前缀,不带 / 是保留前缀。
如果 proxy_pass 的 URL 本身就带路径,规则又会变:
location /api/ { proxy_pass http://backend/v1/;}# /api/user → /v1/user (location 前缀被 /v1/ 替换掉)
每次改完 proxy_pass 最好用 curl -v 跑一下确认转发结果,肉眼判断容易出错。
还有个隐蔽的限制:正则 location 里的 proxy_pass 不能带 URI 部分,否则启动直接报错。
location ~ ^/api/(.*)$ { proxy_pass http://backend/v1/;}location ~ ^/api/(.*)$ { proxy_pass http://backend/v1/$1$is_args$args;}
proxy_pass 里带变量时也是同样规则:URI 部分由你自己拼,Nginx 不会再做前缀替换。
二、改完配置不生效
改配置后没看到变化,按下面顺序排查:
nginx -tnginx -s reloadps -ef | grep nginx
如果上面都正常但还是不生效,看看是不是这几种情况:
- 改错文件了:以为改的是 `nginx.conf`,其实是被 `include` 引入的子文件- `server_name` 没命中:多个 server 监听同一端口时,请求可能落到 `default_server` 或第一个 server 上- 浏览器缓存:换无痕窗口或 `Cmd/Ctrl + Shift + R`- CDN 缓存:经过 CDN 的话需要刷新 CDN- `reload` 失败但没注意:`nginx -t` 通过不代表 `reload` 一定成功,留意输出和 error.log
不确定当前实际加载的配置时,用:
nginx -T nginx -T | grep -n "server_name" nginx -T | sed -n '/server_name example\.com/,/^}/p'
nginx -T 输出的是 Nginx 实际加载并合并后的最终配置,比一层层翻 include 文件靠谱。
三、502 Bad Gateway
意思是 Nginx 能收到请求,但后端没正常响应。
排查顺序:
ps -ef | grep java ss -tlnp | grep 8080 telnet 127.0.0.1 8080curl -v http://127.0.0.1:8080/healthtail -f /var/log/nginx/error.log
常见原因和处理方式:
- **后端服务挂了**:重启服务,并加上进程存活监控- **后端只监听 `127.0.0.1`,Nginx 走的不是本机**:改后端监听地址为 `0.0.0.0`,或让 Nginx 通过 `127.0.0.1` 访问- **upstream 用域名,DNS 解析失败或缓存过期**:error.log 里能看到 `no resolver defined to resolve` 或 `host not found`,需要在 http 段配 `resolver 8.8.8.8 valid=30s;`- **防火墙 / SELinux 拦截**:`setsebool -P httpd_can_network_connect on`(Nginx 与 Apache 共用同一个布尔)- **后端响应过大,缓冲区不足**:调大 `proxy_buffer_size`、`proxy_buffers`- **upstream 节点被熔断**:检查 `max_fails` / `fail_timeout` 配置是否过于激进- **keepalive 复用了被关闭的连接**:偶发性 502 经常是这个——后端连接空闲超时比 Nginx 的 `keepalive_timeout` 还短,Nginx 复用了已被对端关闭的连接。要么把后端空闲超时调大,要么把 upstream 的 `keepalive_timeout` 调小
四、504 Gateway Timeout
后端响应太慢,Nginx 等不及主动断开。
先调大超时让请求能完成:
location / { proxy_connect_timeout 60s; proxy_send_timeout 300s; proxy_read_timeout 300s; proxy_pass http://backend;}
调大超时只是兜底,根本问题通常在后端:
- SQL 慢查询- 调用外部接口卡住- 后端线程池打满- GC 停顿过长
正确做法是定位后端慢在哪里,超时配置只是给一个上限。
几类特殊场景的超时要单独调:
- **大文件下载、导出**:`proxy_read_timeout` 调到分钟级- **SSE / 长轮询**:`proxy_read_timeout` 要大于服务端两条消息之间的最大间隔,不然中途会被断- **WebSocket**:见第八章,要专门配 `Upgrade` 头和长超时
五、413 Request Entity Too Large
上传文件常见。Nginx 默认请求体大小限制 1MB,文件上传场景肯定不够。
http { client_max_body_size 100m; }location /upload/ { client_max_body_size 500m; proxy_pass http://backend;}
如果有多层代理(CDN → 入口 Nginx → 应用 Nginx),每一层都要配,否则会被中间某一层卡住。后端框架(Spring、Express 等)自身也有限制,需要一起调。
排查时用:
curl -v -F "file=@bigfile.zip" http://example.com/upload
看响应里的 Server 头基本能判断 413 是哪一跳返回的——是入口 Nginx 还是后端框架。
六、location 匹配命中不符合预期
写了好几个 location,请求没命中你以为的那个,先看下匹配优先级(从高到低):
1. `=` 精确匹配2. `^~` 前缀匹配,命中后不再查正则3. `~` / `~*` 正则匹配(区分 / 不区分大小写)4. 无修饰符 普通前缀匹配,最长前缀优先
举例:
location = /favicon.ico { ... } location ^~ /static/ { ... } location ~* \.(jpg|png|gif)$ { ... } location /api/ { ... } location / { ... }
调试时可以在 location 里加一个响应头标记当前命中的是哪一条:
location /api/ { add_header X-Matched-Location "/api/"; proxy_pass http://backend;}
然后 curl http://xxx.com -I 看响应头。
七、后端拿到的客户端 IP 是 Nginx 的
经过反向代理后,TCP 连接的源 IP 就是 Nginx,真实 IP 需要通过 HTTP 头透传。
Nginx 端:
location / { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://backend;}
后端读取 X-Real-IP 或 X-Forwarded-For 头:
- Java:`request.getHeader("X-Real-IP")`- Node.js:`req.headers['x-real-ip']`
如果想让后端的 remote_addr 直接拿到真实 IP,可以在 Nginx 里用 realip 模块:
set_real_ip_from 10.0.0.0/8; set_real_ip_from 172.16.0.0/12; real_ip_header X-Forwarded-For;real_ip_recursive on;
注意 X-Forwarded-For 是可以伪造的,不要直接拿来做鉴权。生产环境必须配 set_real_ip_from 限制只信任内网代理,否则攻击者随便伪造一个头就能绕过 IP 黑白名单。
八、HTTPS 证书相关问题
证书这块踩坑频率不低,按现象分几种。
浏览器提示「证书无效」或「链不完整」
最常见的是证书链不全。CA 签发时通常会给三个文件:站点证书、中间证书、根证书。Nginx 要的是把站点证书 + 中间证书拼成一个文件,顺序不能反:
cat your_domain.crt intermediate.crt > fullchain.crt
ssl_certificate /etc/nginx/certs/fullchain.crt; # 注意是 fullchainssl_certificate_key /etc/nginx/certs/your_domain.key;
验证链是否完整:
openssl s_client -connect example.com:443 -servername example.com
如果只配了站点证书没拼中间证书,桌面浏览器可能能访问(因为系统缓存了常见中间证书),但 App、curl、Java HttpClient 会报错。「PC 能开、App 报错」的反馈基本就是这个原因。
多域名共享 IP,命中了错的证书
一个 IP 上挂多个 HTTPS 站点,靠 SNI 区分。如果客户端不发 SNI(很老的客户端、或者直接用 IP 访问),Nginx 会返回第一个 server 的证书,结果就是域名对不上。
# 显式定一个默认 server,避免落到别的业务站点上server { listen 443 ssl default_server; ssl_certificate /etc/nginx/certs/default.crt; ssl_certificate_key /etc/nginx/certs/default.key; return 444; # 直接断开连接,不响应}
证书过期
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \ | openssl x509 -noout -datescertbot renew --dry-run
线上证书一定要加监控,过期前两周报警。等用户截图发过来才知道就晚了。
混合内容(Mixed Content)
页面是 HTTPS,但里面引用了 HTTP 的 js/css/图片,浏览器会拦截并在控制台报错。检查页面源码里有没有 http:// 开头的资源,统一改成 // 开头或 https://。老项目改不动的可以先加一个 meta 兜底:
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
九、301 / 302 跳转死循环
浏览器报 ERR_TOO_MANY_REDIRECTS,最常见两种情况。
HTTP → HTTPS 跳转死循环
典型场景:Nginx 前面挂了 SLB / CDN,SLB 已经把 HTTPS 卸载成 HTTP 转给 Nginx,Nginx 又判断「协议不是 HTTPS」再跳一次,跳完 SLB 又卸载,循环往复。
server { listen 80; if ($scheme != "https") { return 301 https://$host$request_uri; }}
正确做法是看 SLB 透传的 X-Forwarded-Proto,而不是自己看 $scheme:
server { listen 80; if ($http_x_forwarded_proto != "https") { return 301 https://$host$request_uri; } location / { proxy_pass http://backend; }}
更稳的做法是直接在 SLB 上做 HTTP→HTTPS 跳转,Nginx 这层不掺和。
Nginx 和后端互相跳
Nginx 跳一次,后端框架(比如 Spring Security 默认开启的 HTTPS 强制跳转)又跳一次,两边互相把对方的请求当成 HTTP,于是来回打转。
排查时用 curl -IL 跟一次完整跳转链路:
看输出里每一跳的 Location: 目标地址,能直接看出是哪一层在反复跳。
十、静态资源 403 Forbidden
文件明明存在却返回 403,按这个清单排:
ps -ef | grep "nginx: worker" ls -la /data/www/index.htmlnamei -l /data/www/index.html getenforcesetenforce 0
常见情况和处理方式:
- **文件权限不够**:`chmod 644 file`- **目录没 x 权限**:`chmod 755 dir`- **Nginx 用户没读权限**:`chown nginx:nginx /data/www -R`- **SELinux 拦截**(推荐持久化方式): ```bash semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?" restorecon -Rv /data/www ``` 直接 `chcon -R -t httpd_sys_content_t /data/www` 也能临时生效,但下次系统 `restorecon` 重打标签时会被还原,不如 `semanage` 持久。- **访问的是目录但没有 index 文件**:配置 `index` 或开启 `autoindex on;`
十一、access.log 撑爆磁盘
Nginx 自身不会切割日志,需要靠 logrotate 或自定义脚本。
大多数发行版装包时会自带 /etc/logrotate.d/nginx,先看一眼默认配置:
cat /etc/logrotate.d/nginx
有的话按需调整 rotate、compress 等参数;没有的话再自己写一份。
logrotate 方案
新建 /etc/logrotate.d/nginx:
/var/log/nginx/*.log { daily rotate 7 missingok notifempty compress delaycompress sharedscripts postrotate [ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid` endscript}
kill -USR1 是给 Nginx 发「重新打开日志文件」的信号,配合 mv 完成滚动。如果不发这个信号,Nginx 会继续往改名后的旧文件里写,新文件一直是空的。
调试:
logrotate -d /etc/logrotate.d/nginx logrotate -f /etc/logrotate.d/nginx
按小时切割
高流量场景按天切割颗粒度太粗,可以写个脚本按小时切:
#!/bin/bashLOG_DIR=/var/log/nginxDATE=$(date -d "1 hour ago" +%Y%m%d%H)mv ${LOG_DIR}/access.log ${LOG_DIR}/access_${DATE}.logkill -USR1 $(cat /var/run/nginx.pid)find ${LOG_DIR} -name "access_*.log" -mtime +7 -delete
加到 crontab:
0 * * * * /usr/local/bin/cut_nginx_log.sh
减少不必要的日志
location ~* \.(jpg|png|css|js)$ { access_log off;}location = /health { access_log off; return 200;}
十二、Too many open files
高并发场景常见,error.log 里会一直刷:
accept4() failed (24: Too many open files)
Nginx 每个连接占一个文件描述符,默认 1024 远远不够。要按下面三层一起调,少调一层就不生效。
cat /proc/sys/fs/file-maxecho "fs.file-max = 1000000" >> /etc/sysctl.confsysctl -pnginx soft nofile 65535nginx hard nofile 65535worker_rlimit_nofile 65535;events { worker_connections 65535;}
改完确认是否生效:
cat /proc/$(pgrep -f "nginx: worker" | head -1)/limits | grep "open files"
如果是 systemd 启动的 Nginx,/etc/security/limits.conf 不生效,要在 service 文件里改:
[Service]LimitNOFILE=65535
然后 systemctl daemon-reload && systemctl restart nginx。
附:常用排查命令
tail -f /var/log/nginx/error.logtail -f /var/log/nginx/access.log | grep --color -E "5[0-9]{2}|$"awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10awk '{print $NF, $7}' access.log | sort -rn | head -10awk '{print $9}' access.log | sort | uniq -c | sort -rnss -s watch -n 1 "ss -ant | grep :80 | wc -l"
排查 Nginx 问题的思路其实就几条:先看 error.log,再确认请求经过哪些节点,然后对比能用和不能用的环境,最后一次只改一个变量。多数线上问题按这个顺序走基本都能定位。
该文章在 2026/5/21 8:52:29 编辑过