Nginx 502 全链路排查:从 error.log 的六种报错到 upstream 复用陷阱

举报
yd_282015185 发表于 2026/11/01 13:41:28 2026/11/01
【摘要】 线上 502 别急着重启后端。本文按 error.log 的六种关键报错分类,逐类给出验证命令与修法,并拆解 upstream keepalive 复用的三个坑,最后附一份可直接抄的兜底配置。

线上出现 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 都能在五分钟内定位。


【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。