故障排查与最佳实践
前面五篇讲的是「怎么配」,这一篇讲「配了之后为什么还是出问题」。线上 Nginx 的故障八成落在几个固定的错误码上:502、504、413、499,再加上 rewrite 死循环和 location 优先级误配这两类配置问题。把这几条排查路径背下来,绝大多数问题十分钟内能定位。
502 与 504
这两个码长得很像,但指向完全不同的故障:
| 状态码 | Nginx 的日志措辞 | 本质 | 典型原因 |
|---|---|---|---|
| 502 Bad Gateway | connect() failed (111: Connection refused) / upstream prematurely closed connection | 连不上或连上后被上游主动断开 | 后端进程挂了、端口写错、上游崩溃、上游返回了非法响应 |
| 504 Gateway Timeout | upstream 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 upstreams | upstream 里所有节点都被标记为不可用 | 等 fail_timeout 过期,或先修后端 |
upstream prematurely closed connection | 上游没返回完整响应就断了 | 后端进程崩溃、超时自杀、应用 OOM |
upstream sent invalid header | 上游返回的响应头不合法 | 后端写了非法 header(比如带换行) |
SSL_do_handshake() failed | Nginx 到上游的 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 才是最好的老师。