跳到主要内容

故障排查与最佳实践

前面五篇讲的是「怎么配」,这一篇讲「配了之后为什么还是出问题」。线上 Nginx 的故障八成落在几个固定的错误码上:502、504、413、499,再加上 rewrite 死循环和 location 优先级误配这两类配置问题。把这几条排查路径背下来,绝大多数问题十分钟内能定位。

502 与 504​

这两个码长得很像,但指向完全不同的故障:

状态码Nginx 的日志措辞本质典型原因
502 Bad Gatewayconnect() failed (111: Connection refused) / upstream prematurely closed connection连不上或连上后被上游主动断开后端进程挂了、端口写错、上游崩溃、上游返回了非法响应
504 Gateway Timeoutupstream timed out (110: Connection timed out)连上了但等不到响应后端处理太慢、proxy_read_timeout 太小、数据库慢查询、上游死锁

一句话记:502 是「说不上话」,504 是「说了但不回」。

502 的排查路径​

按「从后往前」的顺序查,每一步都能排除一层:

# 1. 后端到底活着吗?直接打应用端口,绕开 Nginx
curl -sv http://127.0.0.1:8080/health

# 2. 端口在监听吗?upstream 里写的地址对不对
ss -lntp | grep 8080

# 3. Nginx 的 error.log 里具体报什么
sudo tail -n 100 /var/log/nginx/error.log

# 4. 从 Nginx 所在机器测连通性(跨机器时最重要)
curl -sv --connect-timeout 3 http://10.0.0.11:8080/health

# 5. 请求真的转到上游了吗?看 access.log 里的 upstream_status
sudo tail -f /var/log/nginx/access.log

error.log 里的措辞直接对应原因:

日志关键字含义处理
Connection refused目标端口没人监听后端没起、端口写错、只监听了 127.0.0.1
Connection timed out网络不通安全组/防火墙、跨网段路由
no live upstreamsupstream 里所有节点都被标记为不可用等 fail_timeout 过期,或先修后端
upstream prematurely closed connection上游没返回完整响应就断了后端进程崩溃、超时自杀、应用 OOM
upstream sent invalid header上游返回的响应头不合法后端写了非法 header(比如带换行)
SSL_do_handshake() failedNginx 到上游的 TLS 握手失败上游证书过期、协议不匹配

几个高频的真实原因:

  • 后端只监听 127.0.0.1,而 Nginx 在另一台机器上。改 server.address=0.0.0.0 或用内网 IP。
  • proxy_pass 端口写错,比如应用是 8081 写成 8080,报 Connection refused。
  • 后端启动慢(JVM 预热、依赖没就绪),Nginx 先起来了,健康检查全失败。解法是后端就绪探针 + proxy_next_upstream 重试,或者干脆等后端起来再 reload。
  • 应用自己返回 502:$upstream_status 是 502 而 $status 也是 502,说明这是后端给的,不是 Nginx 生成的,去查应用日志。

504 的排查路径​

504 一定是「慢」,先分清慢在哪:

# 看 error.log 的超时记录,注意里面的 upstream 地址和耗时
sudo grep "timed out" /var/log/nginx/error.log | tail -20

# 看 access.log 里 rt($request_time)的分布,最慢的 20 条
# grep -o 只取 rt= 后面的数字,比按列截取更稳
sudo grep -o 'rt=[0-9.]*' /var/log/nginx/access.log | cut -d= -f2 | sort -n | tail -20

常见原因和处理:

原因判断依据处理
后端真的慢(慢 SQL、外部依赖卡住)$upstream_header_time 很大优化后端,别动 Nginx
proxy_read_timeout 太小日志时间刚好等于配置值按业务最长耗时调整,别盲目调大
上游连接池耗尽 / 线程打满后端指标里线程数、连接数满扩容后端,或调 keepalive
大响应体传输慢uht 小、urt 大关 proxy_buffering 或调大缓冲
DNS 解析卡住日志里有 no resolver defined配 resolver
先加缓冲,再调超时

遇到 504,第一反应不该是「把超时从 60s 改成 300s」——那只是让用户从「等 60 秒看到 504」变成「等 5 分钟看到 504」。正确顺序是:先定位是后端慢还是传输慢 → 再决定优化后端还是调参数 → 超时值只作为兜底。

413 与 499​

413:请求体太大​

413 Request Entity Too Large

日志里的措辞是 client intended to send too large body。原因是请求体超过了 client_max_body_size,默认只有 1m。

# 全局放宽到 20m
http {
client_max_body_size 20m;
}

# 或者只给上传接口放宽
location /api/upload/ {
client_max_body_size 200m;
proxy_request_buffering off; # 大文件直接流式转发,别先落盘
proxy_pass http://app_backend;
}

两个注意点:

  • Nginx 拦下来的 413 不会到后端,$upstream_status 是 -。看到 413 且没有 upstream 记录,就是 Nginx 这一层拒的。
  • 前面还有一层 LB 时,LB 自己的 body 限制也要同步调,否则请求在 LB 就被拒了,Nginx 什么都没收到。

499:客户端提前断开​

499 Client Closed Request

这个码是 Nginx 自定义的(不是标准 HTTP 状态码),含义是:Nginx 已经把请求收下、正在处理,但客户端在响应返回之前就把连接关了。

499 本身不是 Nginx 的错,但它是个非常有价值的信号:

场景表现处理
后端太慢,用户等不及关页面499 与高 $request_time 同时出现优化后端,这是最真实的原因
客户端超时设置太短上游 LB 的 timeout 小于 Nginx 处理时间调 LB 超时,或优化响应
用户网络断/刷新页面零星 499,随机分布忽略,量小属正常
爬虫主动断开集中在特定 UA看 UA 分布决定是否限流

排查套路:把 499 和 $request_time 放一起看。499 请求的耗时常集中在某个值附近,那个值就是客户端的超时时间。如果 499 占比突然升高,先去查后端耗时,而不是查 Nginx。

rewrite 死循环与 location 误配​

rewrite 死循环​

日志里的样子:

2019/01/01 10:00:00 [error] 1234#0: *1 rewrite or internal redirection cycle while internally redirecting to "/index.html"

Nginx 对内部跳转有 10 次上限,超了就报这个错并返回 500。常见成因:

# 错例:任何请求都改写回自己,跳转 10 次后炸掉
location / {
rewrite ^ /index.html last;
}

# 正确:先判断文件存在,不存在才回退
location / {
try_files $uri $uri/ /index.html;
}

另外两个隐蔽的坑:

  • 两条 rewrite 互相指:/a 改到 /b,/b 又改回 /a。用 nginx -T 把相关 server 段拉出来,人眼走一遍跳转链。
  • try_files 的最后一个参数又命中同一个 location:比如 try_files $uri /index.html; 而 /index.html 本身不存在,且 location = /index.html 里又写了 try_files,就会循环。

排查手段:临时把 error_log 调到 info,日志里会打出每一次内部跳转的目标路径,跟着走一遍就找到环了。

location 优先级误配​

这是「配置看起来对但行为不对」的头号原因。第 1 篇讲过优先级,这里看两个真实错例。

错例一:正则抢走了静态目录

# 期望:/static/app.js 走静态目录
# 实际:被下面的正则截胡,去 /var/www/images 找,404
location /static/ {
root /var/www;
}

location ~ \.(js|css|png)$ {
root /var/www/images;
}

修法:静态目录加 ^~,命中后不再检查正则。

location ^~ /static/ {
root /var/www;
}

错例二:proxy_pass 尾斜杠导致 404

# 请求 /api/users → 上游收到 /api/users(如果后端路由是 /users,就 404)
location /api/ {
proxy_pass http://app_backend;
}

# 请求 /api/users → 上游收到 /users(尾斜杠把 /api/ 替换掉了)
location /api/ {
proxy_pass http://app_backend/;
}

判断方法:在后端 access log 里看实际收到的路径,或者临时给 Nginx 加 add_header X-Debug-URI $request_uri always; 对比。

错例三:alias 少斜杠

location /files/ {
alias /data/files; # 错:拼出 /data/filesxxx
}

location /files/ {
alias /data/files/; # 对
}
改完配置先 nginx -t 再 nginx -T

-t 只查语法,-T 会把所有 include 展开后的完整配置打出来。很多「改了不生效」的问题,用 -T 一看就发现改的文件根本没被 include,或者被后面同名的 server 覆盖了。

性能问题​

worker_connections 不够​

error.log 里的信号:

[alert] 1234#0: *5678 socket() failed (24: Too many open files) while connecting to upstream
[crit] 1234#0: accept4() failed (24: Too many open files)
[alert] 1234#0: worker_connections are not enough

三层限制要同时满足,缺一层就会出现上面的报错:

worker_rlimit_nofile 65535;      # 第 1 层:worker 进程的 fd 上限

events {
worker_connections 10240; # 第 2 层:单 worker 的连接数
}
# 第 3 层:systemd 给进程的 fd 上限,必须 >= worker_rlimit_nofile
systemctl show nginx -p LimitNOFILE

容量估算:反向代理场景每个客户端请求占 2 个连接(客户端侧 + 上游侧),所以 worker_processes × worker_connections / 2 才是能扛的客户端并发数。留 20% 余量。

文件描述符与 TIME_WAIT​

# 当前进程打开了多少 fd
sudo lsof -p $(cat /var/run/nginx.pid) | wc -l

# 系统级的连接状态分布,TIME_WAIT 特别多说明短连接太多
ss -s
ss -tan state time-wait | wc -l

TIME_WAIT 堆积的两个根因和解法:

  • Nginx 到上游是短连接:加 upstream 的 keepalive 64 + proxy_http_version 1.1 + proxy_set_header Connection "",三件套缺一不可。
  • 客户端到 Nginx 是短连接:调大 keepalive_timeout(比如 65s),让浏览器复用连接。

其他常见瓶颈​

现象可能原因排查
CPU 打满,QPS 上不去gzip 压缩级别太高、TLS 握手太频繁、正则 location 太多top -H 看线程,降 gzip_comp_level,开 session 复用
磁盘 IO 高access log 写太猛、proxy_cache 命中率低、临时文件多关不必要的 access_log,调缓存策略,日志异步写
内存持续上涨缓存 keys_zone 太大、连接泄漏看 proxy_cache 的 max_size,检查上游 keepalive
带宽跑满大文件没走 CDN、没开 gzip静态资源上 CDN,确认 gzip_types 覆盖到
压测 QPS 上不去但 CPU 不高worker 数不够、somaxconn 太小、上游连接池不足按第 4 篇的系统调优逐项核对

压测建议用 wrk,比 ab 更接近真实并发:

wrk -t4 -c400 -d60s --latency http://127.0.0.1/api/health

压测时同时观察四个数:QPS、P99、错误率、nginx_connections_active。只盯 QPS 容易得出错误结论。

最佳实践清单​

配置层面:

  • worker_processes auto,worker_connections 按容量算,worker_rlimit_nofile 与 systemd 的 LimitNOFILE 对齐。
  • sendfile on + tcp_nopush on 给静态文件,tcp_nodelay on 给动态接口。
  • server_tokens off,删掉默认站点或改成 return 444。
  • client_max_body_size 按业务设,别留默认的 1m,也别设成 0(无限制)。
  • 日志一定带耗时字段,$request_time 和 $upstream_response_time 是排障的命根子。
  • keepalive_timeout 略大于上游 LB 的空闲超时,避免连接被 LB 先断、Nginx 侧留下脏连接。
  • HTTPS 只留 TLSv1.2/1.3,证书链完整,配好续期。
  • proxy_cache 要配 X-Cache-Status 响应头,否则永远不知道缓存有没有生效。

运维层面:

  • 改配置三步走:nginx -t → nginx -s reload → curl 验证。绝不 restart。
  • 配置进 Git,部署脚本做校验和回滚,两台机器用同一份配置。
  • 升级走「备份 → 校验 → USR2 → WINCH → 观察 → QUIT」,留好回退路径。
  • 监控盯四件事:nginx_up、活跃连接水位、连接丢弃率、日志里的 5xx 和 P95。
  • 保留一份「最小可回滚配置」:出大事时能一键切回上一个已知good状态。
  • 变更前记下当前状态:nginx -T 的输出、当前 QPS、错误率,出问题时才有对比基线。
排障的通用顺序

先分层,再定位:客户端 → LB → Nginx → 上游 → 应用 → 依赖。每一层先用 curl -v 和日志确认「这一层收没收到、回没回」,别一上来就改配置。Nginx 的问题里,真正出在 Nginx 自身的比例,比想象中低得多。

小结​

  • 502 是「连不上/被断开」,504 是「等不到回应」;两者都先看 error.log 的措辞,再看 access log 的 upstream_status 和耗时字段。
  • 413 是请求体超限(client_max_body_size),Nginx 直接拒、没有 upstream 记录;499 是客户端提前断开,要和 $request_time 一起看,根因多半在后端慢。
  • rewrite 死循环有 10 次上限,报 rewrite or internal redirection cycle;location 误配优先怀疑正则抢匹配、proxy_pass 尾斜杠、alias 缺斜杠。
  • 性能瓶颈先看三层 fd 限制(worker_rlimit_nofile / worker_connections / LimitNOFILE),再看上游连接池和 TIME_WAIT。
  • 最佳实践的核心就两条:变更必须有校验和回滚,排障必须分层看日志。

六篇走完:从架构、安装、配置、部署、监控到排障,Nginx 这条链路算是闭环了。剩下的就是在自己的环境里把配置真正跑一遍——纸上得来终觉浅,日志和 curl 才是最好的老师。