Nginx报错502 Bad Gateway的排查方法(connect() failed与upstream prematurely closed connection定位、proxy_read_timeout与keepalive配置)

页面一刷新就出502 Bad Gateway,绕过nginx直接curl后端接口却正常返回,这种最折腾人。502的意思是nginx作为网关没能从上游拿到一个合法响应,问题基本都在nginx与后端之间这一段,nginx自己出毛病的概率很低。排查动作的核心只有一个:把error.log里对应那次请求的记录翻出来,看清报错串到底是哪种。


error.log里那一行才是钥匙

先确定日志文件在哪。Ubuntu、Debian的包默认放/var/log/nginx/error.log,Rocky Linux装官方包也是这个路径,源码编译的常落到/usr/local/nginx/logs/error.log。拿不准就直接问nginx自己:

nginx -V 2>&1 | tr ' ' '\n' | grep error-log

vhost里单独指定的error_log会覆盖全局配置,所以顺手把运行中的配置扒出来看一眼更稳:

nginx -T | grep -n error_log

趁着还能复现,盯实时日志抖出来的那几行:

tail -f /var/log/nginx/error.log | grep --line-buffered upstream

常见的报错串有四种,指向完全不一样。

1、connect() failed (111: Connection refused) while connecting to upstream。那个端口压根没人监听,后端进程挂了、崩了、或者压根没起来。日志末尾会带上upstream: "http://127.0.0.1:8080/xxx",把这个地址抄下来。

2、upstream prematurely closed connection while reading response header from upstream。TCP连上了,但后端在吐出响应头之前就把连接断了。后端进程被kill、被OOM干掉、或者keepalive连接被后端单方面关掉,表现都是这一条。遇到过的一次是node进程滚动重启那一瞬间在刷这条,围着nginx配置改了半天,结果发现是发布脚本里用了kill -9,进程连握手告别的时间都没有。

3、upstream timed out (110: Connection timed out) while reading response。后端迟迟不给响应,客户端最后多半看到504而不是502,这是两者的分界线。

4、no live upstreams while connecting to upstream。upstream块里所有节点都被判定失败摘掉了,去看这一组上配的max_fails和fail_timeout。


确认后端还活着没有

拿到日志里的地址,手工请求一次。URI要跟日志里那条request原样一致,别只打根路径,见过太多只在某个慢接口上出现的502,打首页永远是好的:

curl -v -H "Host: www.example.com" http://127.0.0.1:8080/api/report/daily

同时看端口有没有人监听:

ss -lntp | grep 8080

进程还在、ss却看不到监听,一般是应用假死,要么就是监听地址绑错了:应用只绑了127.0.0.1,而nginx转的是内网IP。这类错在容器里尤其常见,应用配置文件里写localhost,容器外面怎么都连不通,看进程列表一切正常。

文件描述符不够也会混在这一步里报错:error.log直接刷worker_connections are not enough,或者accept() failed (24: Too many open files)。Ubuntu 22.04、24.04的nginx服务单元里LimitNOFILE常见就是1024,实测过一台四核机器在并发上来之后满屏都是这个。正经改法是加一个drop-in:

systemctl edit nginx
[Service]
LimitNOFILE=65535

然后systemctl daemon-reload加systemctl restart nginx。只在nginx.conf里配worker_rlimit_nofile也能救急,不过systemd那层不松口的话会被压回1024。


keepalive配一半等于挖坑

upstream里写了keepalive 32不代表真的在复用,下面几条配齐才算数:

upstream backend {
    server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
    keepalive 32;
}
server {
    location / {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}

少了proxy_http_version 1.1,nginx拿HTTP/1.0跟后端说话,连接复用不起来;少了那个空的Connection头,客户端发来的close会被原样透传给后端,后端随手就把连接关了,下次nginx拿这条已死的连接去发请求,日志里就是上面那条prematurely closed。这套行为从nginx 1.18.0、1.24.0、1.26.2到1.28.0,都一样,别指望升个版本能绕过去。

还有一层是后端自己的keepalive超时。Tomcat的keepAliveTimeout不显式配就取connectionTimeout的值,而nginx的keepalive_timeout默认75秒,两边对不上时,被后端掐断的空闲连接正好撞上nginx准备复用的那一刻。让两边的时间以后端为准配上,比单纯把nginx的timeout调小更合理。


超时参数别一上来就加到300秒

proxy_connect_timeout、proxy_send_timeout、proxy_read_timeout的默认值都是60秒。把proxy_read_timeout往上调确实能让一部分慢接口的502立刻消失,不过有两个地方要先想清楚。

一个是proxy_read_timeout判的是两次读之间的间隔,不是整个请求的总耗时。后端哪怕每50秒吐出一个字节,这个超时也永远触发不了。

另一个更要紧:upstream timed out之后,nginx会按proxy_next_upstream的默认规则(error、timeout)把请求重新发给下一个节点。一次表单提交被重发,下场经常是重复下单。之前碰到的就是这么回事,加完之后订单表里多出一堆内容一模一样的记录。写接口不幂等的话,把timeout从里面摘掉:

proxy_next_upstream error http_502;

至于那个真正的慢查询,调超时只是让它藏起来,后端日志里的执行时间才是该盯着的东西。


还剩两种没那么常见的

一种是后端的工作池打满。php-fpm那边日志喊server reached pm.max_children setting,Tomcat这边是线程池被占完,池子空不出来的时候nginx连过去就变成拒绝连接或者干等。这类502的特点是后端日志在叫,nginx这边只是跟着报refused。

另一种是本机临时端口耗光,压测或者nginx和后端挤在同一台机器上容易碰上,看一眼这两处:

cat /proc/sys/net/ipv4/ip_local_port_range
ss -ant | awk '{print $1}' | sort | uniq -c

TIME_WAIT排到几万、端口段又快用完的时候,error.log里跟着出现的是connect() failed (99: Cannot assign requested address)。真要治本还是回到上面那套长连接,比去调内核参数干净。


改完先跑nginx -t过一遍,再systemctl reload nginx。这时候别急着收工,盯着error.log再刷十分钟,看报错串有没有换一种。网关层这东西很会捉迷藏,把connect refused修好之后往往会变成upstream timed out,说明那次请求确实慢,只是之前连门都没敲开。