Nginx 502 全链路排查:从 error.log 的六种报错到 upstream 复用陷阱
线上出现 502,最常见的处理方式是"重启后端再说"。但只要重启过一次没生效,或者过半小时又复发,就说明没找到根因。这篇把 502 的排查拆成六个层次,每一层给出对应的日志特征、验证命令和修法,最后给一份可以直接抄的兜底配置。
## 一、先确认 502 是谁返回的
502 Bad Gateway 的定义是:作为网关或代理的服务器,从上游收到了无效响应。所以它几乎总是意味着"Nginx 到后端的这条链路出了问题",而不是客户端请求本身有问题。
有两种情况需要先排除:
第一,响应头里带 `Server: nginx` 且 error.log 里没有任何 upstream 关键字。这时 502 很可能是 Lua 脚本、第三方模块抛出的,或者是 upstream 返回了 Nginx 无法解析的响应(比如后端返回了 HTTP/0.9 格式、响应头缺少状态行)。
第二,502 只出现在特定 URL 上。这通常不是连接问题,而是该 location 单独配置了 proxy_pass 指向了错误的端口或域名。
判断依据只有一个:`error.log`。没有 error.log 的 502 排查都是猜。先确认日志路径:
```bash
nginx -t 2>&1 | head -3
ps -ef | grep nginx
# 从配置里读实际路径
grep -E "error_log|access_log" /etc/nginx/nginx.conf
```
## 二、error.log 里只有六种关键行
502 的 error.log 记录高度集中,把下面六种认全,八成问题当场就能定位。
**1. connect() failed (111: Connection refused)**
后端端口上没有进程在监听,或者监听了但不在 Nginx 能访问的地址上。这是最干脆的一种,往下看第三节。
**2. upstream timed out (110: Connection timed out) while connecting to upstream**
TCP 三次握手没完成。通常是后端 SYN 队列满、防火墙丢包、或者后端所在机器负载过高。注意这和上一条的区别:refused 是有响应但拒绝,timed out 是压根没响应。
**3. upstream timed out while reading response header from upstream**
连接建立了,请求也发了,但后端在 `proxy_read_timeout` 内没吐第一个字节出来。这是最常见的一类,对应后端卡住:GC 停顿、慢 SQL、线程池打满、同步锁、或者下游依赖超时。
**4. upstream prematurely closed connection while reading response header from upstream**
连接建立、请求发出、后端还没返回完整响应头就把连接关了。对应后端进程崩溃、被信号杀死、或者主动 close。这是最隐蔽的一类,因为后端日志里可能什么也没有。
**5. no live upstreams while connecting to upstream**
upstream 块里所有节点都被健康检查标记为不可用。看 `max_fails` 和 `fail_timeout` 的配置,很多时候是误判。
**6. SSL_do_handshake() failed**
Nginx 作为客户端去连 HTTPS 上游时握手失败。证书过期、SNI 不匹配、上游只支持 TLS 1.2 而 Nginx 配了 1.3,都会触发。
把这六条按频率排,实际生产里第 3、4 条能占到七成以上。
## 三、连接被拒:分三步确认后端是否真的在听
```bash
# 第一步:端口在不在监听,绑定的是哪个地址
ss -lntp | grep 8080
# 第二步:本机直连能不能通
curl -sv -m 3 http://127.0.0.1:8080/health
# 第三步:从 Nginx 所在机器连(如果是跨机部署)
curl -sv -m 3 http://10.0.1.12:8080/health
```
三种结果对应三个方向:
- `ss` 没有输出 → 进程没起,或者起来就退出了。看进程日志,不要看 Nginx 日志。
- `ss` 显示 `127.0.0.1:8080` 而不是 `0.0.0.0:8080` 或 `*:8080` → 后端只绑了回环地址,Nginx 在本机还能连,跨机必然 502。改监听地址。
- 本机能通、跨机不通 → 安全组或 iptables。云上先查安全组入方向规则,再查机器上的 firewalld/ufw。
还有一种容易漏的:后端用的是 Unix socket。`proxy_pass http://unix:/run/app.sock:/;` 路径写错、权限不对(Nginx worker 用户没有读写权限)、或者 socket 文件被删除后重建但 Nginx 缓存了旧 inode。这类问题的报错同样是 Connection refused,但排查方向完全不同。用 `ls -l` 看 socket 文件的属主和权限。
## 四、读超时:分清"连不上"和"连上了但没回应"
时间参数有三个,容易被混为一谈:
```nginx
proxy_connect_timeout 5s; # 建连超时
proxy_send_timeout 60s; # 向后端发送请求的超时(两次写之间的间隔)
proxy_read_timeout 60s; # 等待后端响应的超时(两次读之间的间隔)
```
read timeout 的排查重点在后端,用 curl 打点计时最快:
```bash
curl -o /dev/null -s -w "dns:%{time_namelookup} conn:%{time_connect} ttfb:%{time_starttransfer} total:%{time_total}\n" \
http://127.0.0.1:8080/api/slow
```
`ttfb` 明显偏大说明后端处理慢,`conn` 偏大说明建连慢。这两个含义不同:前者查应用,后者查网络和负载。
有个必须提醒的坑:**read timeout 触发后,Nginx 默认可能会重试下一个 upstream**(由 `proxy_next_upstream` 控制)。如果请求是非幂等的(下单、支付、写库),重试会导致重复写入。所以对写接口,建议显式关掉重试:
```nginx
proxy_next_upstream error timeout http_502;
proxy_next_upstream_tries 1; # 只试一次,不换节点
```
或者更保守,只对 GET 生效:
```nginx
proxy_next_upstream error timeout non_idempotent;
```
`non_idempotent` 的含义是"即使是非幂等方法也允许重试",默认不包含它。反过来说,默认配置已经对非幂等请求谨慎了,但很多人为了"提高成功率"手动加上,然后引发了重复下单。
## 五、prematurely closed:为什么后端日志里什么都没有
这是最有迷惑性的一类。连接建立了,请求也到了后端,但后端在返回响应头之前挂了。Nginx 只能看到 TCP 被关闭,于是记一条 prematurely closed。此时应用日志往往只写到"收到请求"就断了,看起来像是"请求丢了"。
按可能性排序,排查动作如下:
```bash
# 1) 是否被 OOM Killer 干掉
dmesg -T | grep -iE "killed process|out of memory" | tail -20
# 2) 是否收到过段错误或非法指令
dmesg -T | grep -iE "segfault|general protection" | tail -20
# 3) 进程启动时间是否与故障时间吻合(被重启过)
ps -o pid,lstart,etime,cmd -p $(pgrep -f 'your-app' | head -1)
# 4) 容器场景下看退出码
docker inspect --format '{{.State.OOMKilled}} {{.RestartCount}} {{.State.ExitCode}}' <container>
```
除了崩溃,还有两个"正常但致命"的原因:
**PHP-FPM 的 max_requests。** 子进程处理满 N 个请求后主动退出,退出瞬间正在处理的请求就会被掐断。如果 `max_requests` 设置得较小(比如 500),高峰期会稳定复现 502。对策是调大,或者接受它但要配合平滑重启。
**Node.js 的未捕获异常。** 单进程模型下一个 throw 直接退出,所有在途请求全断。这类问题的特征是 502 集中在某个时间点爆发,然后自愈(进程被 supervisor 拉起)。
容器编排环境下还要看 readiness 探针。探针失败会把 Pod 摘出,但如果摘出前流量还在打,窗口期就是 502。
## 六、upstream keepalive:三个必踩的坑
长连接能显著降低建连开销,但配置不完整反而会制造 502。三个坑按踩到的概率排序:
**坑一:upstream 块里没写 keepalive 指令。** 只在 `location` 里写 `proxy_http_version 1.1` 是无效的,必须同时有:
```nginx
upstream backend {
server 10.0.1.12:8080;
keepalive 64; # 每个 worker 维持的空闲连接数
keepalive_requests 1000;
keepalive_timeout 60s;
}
```
**坑二:Nginx 侧 keepalive 比后端长。** Nginx 认为连接可用,取出来用的时候后端刚好把这条连接关了,结果就是响应头读到一半连接被断——正好对应第五条那个 prematurely closed。所以 Nginx 的 `keepalive_timeout` 必须**小于**后端的空闲超时,经验值是后端的一半。
**坑三:只设了 `proxy_http_version 1.1` 但漏了 `Connection` 头。** 某些上游(尤其是 Java 应用)看到 `Connection: close` 会主动关连接:
```nginx
location / {
proxy_http_version 1.1;
proxy_set_header Connection "";
}
```
`Connection ""` 这个空值写法是必须的,写 `keep-alive` 反而不对,因为 Nginx 会用 `Connection` 头控制自己到后端的连接复用,显式设置会干扰。
如果改完仍然间歇性 502,把 upstream 的 keepalive 临时去掉做对比实验。去掉后 502 消失,就说明问题一定在长连接复用上,再逐个校准超时参数。
## 七、proxy_pass 写域名:只在启动时解析一次
```nginx
# 危险写法
proxy_pass http://api.internal.example.com;
```
Nginx 的 DNS 解析发生在配置加载时,之后 IP 变了不会自动更新。后端做了滚动发布、换了 IP,或者 K8s Service 重建,Nginx 还在往老 IP 打,表现就是成片 502,而 error.log 里是 connect() failed。
正确写法是利用变量强制运行时解析:
```nginx
resolver 10.0.0.2 valid=10s ipv6=off;
location /api/ {
set $backend "api.internal.example.com";
proxy_pass http://$backend;
}
```
注意两个副作用:使用变量后,URI 不会自动带上 location 前缀,需要显式写 `proxy_pass http://$backend$request_uri;`;`valid=10s` 太小会增加 DNS 压力,内网一般 30s 到 60s 足够。
## 八、连接数耗尽与 backlog 溢出
这两种情况的表现不是"连不上",而是"排队排到超时",日志特征不同:
```bash
# worker 连接数是否打满
grep "worker_connections are not enough" /var/log/nginx/error.log | tail -5
# 当前连接数与状态分布
curl -s http://127.0.0.1/nginx_status
```
`worker_connections` 的最大并发能力是 `worker_processes × worker_connections`,但每个代理请求会同时占用一条客户端连接和一条后端连接,也就是**一次代理消耗两个连接槽**。所以理论上限要除以 2。
backlog 溢出时,内核会直接丢弃 SYN,客户端侧表现为连接超时,Nginx 侧可能是 connect timeout。调优:
```nginx
events {
worker_connections 10240;
multi_accept on;
}
```
```bash
sysctl -w net.core.somaxconn=32768
sysctl -w net.ipv4.tcp_max_syn_backlog=8192
```
## 九、一张决策表
| error.log 特征 | 首要怀疑 | 第一条验证命令 |
|---|---|---|
| Connection refused | 后端未监听或地址不对 | `ss -lntp \| grep 端口` |
| timed out while connecting | 网络不通或 SYN 队列满 | `curl -m 3` 直连后端 |
| timed out while reading header | 后端处理慢 | `curl -w ttfb` 打点 |
| prematurely closed | 后端崩溃或被重启 | `dmesg -T \| grep -i killed` |
| no live upstreams | 健康检查误判 | 检查 max_fails/fail_timeout |
| SSL_do_handshake failed | 证书或 TLS 版本 | `openssl s_client -connect` |
| 无任何 upstream 记录 | 模块或响应格式 | 检查 Lua/第三方模块 |
## 十、可以直接抄的兜底配置
```nginx
upstream backend {
server 10.0.1.12:8080 max_fails=3 fail_timeout=10s;
server 10.0.1.13:8080 max_fails=3 fail_timeout=10s;
keepalive 64;
keepalive_requests 1000;
keepalive_timeout 30s; # 小于后端空闲超时
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
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;
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# 只对幂等请求重试,避免重复写入
proxy_next_upstream error timeout http_502;
proxy_next_upstream_tries 2;
# 后端返回 502 时给出可识别的响应头,便于区分"Nginx 的 502"和"后端的 502"
proxy_intercept_errors off;
add_header X-Upstream-Status $upstream_status always;
}
}
```
最后那个 `X-Upstream-Status` 是个实用技巧:正常时它是 `200`,后端自己返回 502 时也能看到具体的 upstream 状态码,能快速区分故障发生在哪一跳。
## 结语
502 的排查顺序应该是:先读 error.log 定类别,再按类别验证假设,最后才动配置。跳过第一步直接重启,等于把可复现的现场销毁了。
真正的难点从来不是"怎么修",而是"怎么在改配置之前确认是哪一类"。上面六种日志特征对应六个完全不同的方向,认清楚它们,大部分 502 都能在五分钟内定位。
- 点赞
- 收藏
- 关注作者
评论(0)