nginx 反代 502/504 的排查顺序:先分类,再动手
压测当天前端同事说"页面十个里挂三个",我看监控:502 占 6%,504 一起冒。第一反应是加机器,加完还是 6%。最后查出来是 nginx 连接数被 systemd 的 LimitNOFILE 卡在 1024,跟后端无关。这类问题靠顺序,不靠技巧。
一、先把 502 和 504 拆开
502 是 nginx 认为后端的应答不合法,或压根连不上;504 是连上了但后端不回话,等到超时。根因几乎相反:502 多为"后端死了、拒了、连接被打断",504 多为"后端活着但慢"。
一条命令归到具体哪一类:
sudo tail -n 500 /var/log/nginx/error.log \
| grep -E 'upstream (timed out|prematurely closed|sent invalid header)|Connection refused' \
| sort | uniq -c | sort -rn | head -n 10
关键字直接决定下一步查什么:
关键字 | 含义 | 下一步
upstream timed out (110: Connection timed out) | 连后端或读响应超时 | 量后端耗时,对 proxy_read_timeout
upstream prematurely closed connection | 后端把连接掐了 | 后端崩溃、被 OOM kill、keepalive 不匹配
connect() failed (111: Connection refused) | 后端没人监听 | 进程真没起来,先看后端日志
二、第二步:绕开 nginx 直连一次
这一步把"nginx 的锅"和"后端的锅"分开。upstream 走 unix socket 时无需知道地址:
curl --unix-socket /run/app/app.sock -sS -o /dev/null \
-w 'code=%{http_code} connect=%{time_connect} first=%{time_starttransfer} total=%{time_total}\n' \
http://app/healthz
重点读 first 和 total:first 小、total 大,说明后端开始回了但吐得慢,典型是大响应体或 gzip 吃 CPU;两个都大,说明连处理都排不上队,先查连接池和锁。
我那次实测 first=0.01s total=9.8s,10 秒后才吐第一个字节,而 nginx 默认 proxy_read_timeout 是 60 秒,没到超时线。真正原因是后端在等一把行锁,瓶颈是同一行数据。
三、504 的三个真实来源
1)upstream 没开 keepalive。proxy_http_version 默认 1.0,短连接一路建连,后端 accept 队列排满就表现成"慢"。四个参数要一起上,漏一个就白改:
upstream app_backend {
server unix:/run/app/app.sock;
keepalive 64;
}
location /api/ {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://app_backend;
proxy_read_timeout 30s;
proxy_next_upstream error timeout http_502 http_504;
}
我第一次改完 QPS 一点没动,就是漏了 proxy_http_version 1.1 和清空 Connection:keepalive 开了,请求头却还带着 close,连接照样一个请求断一次。
2)gzip 在 nginx 侧压大响应。gzip_comp_level 从 6 降到 2,同一批 8MB 的 JSON,worker CPU 从 78% 掉到 41%,P99 从 1.9s 回到 0.4s。JSON 压不动,2 和 6 的体积差不到 4%,多花的 CPU 全是白烧。
3)proxy_buffering 默认 on,大响应先落盘再吐,磁盘一慢全变 504。查:
df -h /var/cache/nginx /var/lib/nginx 2>/dev/null
iostat -dx 1 3 | grep -E 'Device|%util'
%util 长期 90% 以上就别怀疑配置了,先换盘,或者干脆关掉临时文件。
四、502 的三个真实来源
1)连接数卡在文件描述符上。算一下就知不够:一个客户端请求占两条连接(客户端一条、到后端一条),默认 worker_connections 512 实际只够 256 个并发。查:
pid=$(pgrep -o nginx)
ls /proc/$pid/fd | wc -l
grep -i 'open files' /proc/$pid/limits
nginx -T 2>/dev/null | grep -E 'worker_(processes|connections|rlimit_nofile)'
我那次 fd 数顶在 1024,worker_rlimit_nofile 写在 nginx.conf 里完全没用——systemd unit 里的 LimitNOFILE 优先级更高。改 unit 才生效:
[Service]
LimitNOFILE=65535
sudo systemctl daemon-reload && sudo systemctl restart nginx
pid=$(pgrep -o nginx); grep -i 'open files' /proc/$pid/limits
重启后那行必须看到 65535,看不到就是 unit 没生效。
2)后端被 OOM killer 干掉,重启那几秒正好落在压测窗口。一条命令定案:
journalctl -k --since '-2h' | grep -iE 'killed process|out of memory'
我捞到 3 条,时间点和 502 峰值完全对齐。"一阵一阵、过会自己好"就是后端在反复重启。
3)upstream 写域名,后端换地址了,而 nginx 只在启动时解析一次,之后一直往旧地址发。特征是重启 nginx 立刻好、几小时后又坏。用变量强制每次解析:
resolver <你环境里的 DNS 地址> valid=10s;
set $backend app.internal;
proxy_pass http://$backend$request_uri;
注意 proxy_pass 一旦用变量,nginx 就不自动拼 URI,必须手动补 $request_uri,否则所有请求都打到根路径——这个坑我踩过,接口层整个 404。
五、我现在的固定六步
1. grep upstream,先分到 timeout 还是 refused / prematurely
2. curl --unix-socket 直连,读 first 与 total
3. journalctl -u app -n 100 --no-pager | grep -iE 'oom|panic|timeout'
4. ls /proc/$(pgrep -o nginx)/fd | wc -l 对比 limits
5. ss -s | head -n 5,timewait 上万说明 keepalive 没生效
6. 改完必验:同一路径连打 200 次,5xx 为 0 且 P99 下降
一句话结论:502 先怀疑"连不上、连接被打断",504 先怀疑"慢"。error.log 关键字、curl 直连的 first/total、/proc 里的 fd 数,这三样到手就不用猜了。
- 点赞
- 收藏
- 关注作者
评论(0)