那天中午十二点半,我刚端起外卖,前端同学在群里发了一张截图:浏览器一片白,中间一行灰色大字 502 Bad Gateway。紧接着他补了一句:“是不是你们后端挂了?”我说我没挂。他说:“那为什么是 502?”我说:“……你先别急,我看看是 502 还是 504。”他回了个问号:“这俩有区别吗?”
有,而且区别大到排查方向完全相反。502 通常是“网关连不上上游,或者上游把连接掐了”,往往是后端进程/端口/网络的问题;504 是“网关在等上游,等到不耐烦了”,往往是后端慢的问题。前者去看进程活没活,后者去看谁在慢。这篇是我把两个状态码的分辨方法、日志原文、命令和几个经典坑整理成的备忘录。
一、现象:同一个网关,两种“不开心”
先说一下架构:浏览器 → Nginx(网关,对外 80/443)→ Spring Boot 应用(Tomcat,内网 10.0.1.11:8080 与 10.0.1.12:8080 两台)。Nginx 只做反向代理。这时候用户看到的 502 / 504 都是 Nginx 自己生成的,不是业务代码返回的。所以第一句话就该问:这个状态码是谁生成的?如果 Nginx 的 access.log 里有这条请求、状态码 502,那就是 Nginx 自己判了“上游有问题”。
我的第一个动作永远是:先看日志,不要先重启任何东西。重启 Nginx 或后端会让现场消失,而且 502 这种问题经常是“后端刚好重启了一下”造成的,你一重启,等于再制造一次。
$ tail -n 5 /var/log/nginx/access.log
10.0.0.21 - - [12/Sep/2026:12:31:07 +0800] "POST /api/order/create HTTP/1.1" 502 157 "-" "Mozilla/5.0"
10.0.0.21 - - [12/Sep/2026:12:31:09 +0800] "POST /api/order/create HTTP/1.1" 502 157 "-" "Mozilla/5.0"
10.0.0.34 - - [12/Sep/2026:12:31:12 +0800] "GET /api/report/export HTTP/1.1" 504 167 "-" "Mozilla/5.0"
10.0.0.34 - - [12/Sep/2026:12:31:15 +0800] "GET /api/user/info HTTP/1.1" 200 412 "-" "Mozilla/5.0"默认的 combined 格式只告诉你“有 502、有 504”,不告诉你上游是谁、花了多久。这就是为什么我后面要给 access_log 加三个变量。先看 error.log,它才是 502/504 的“尸检报告”。
二、排查路线图
先上第一张图:一个请求经过 Nginx 时,502 和 504 分别发生在哪一段。
再来一张决策树式的小抄,我打印出来贴在了显示器边上:
三、动手:从日志里把上游“审”出来
第 1 步:error.log,认清几种“死法”
$ grep -E 'upstream|connect\(\)' /var/log/nginx/error.log | tail -n 8
2026/09/12 12:31:07 [error] 1203#1203: *48811 connect() failed (111: Connection refused) while connecting to upstream, client: 10.0.0.21, server: demo.example.com, request: "POST /api/order/create HTTP/1.1", upstream: "http://10.0.1.11:8080/api/order/create", host: "demo.example.com"
2026/09/12 12:31:08 [error] 1203#1203: *48815 upstream prematurely closed connection while reading response header from upstream, client: 10.0.0.22, server: demo.example.com, request: "GET /api/user/info HTTP/1.1", upstream: "http://10.0.1.12:8080/api/user/info", host: "demo.example.com"
2026/09/12 12:31:12 [error] 1203#1203: *48830 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 10.0.0.34, server: demo.example.com, request: "GET /api/report/export HTTP/1.1", upstream: "http://10.0.1.11:8080/api/report/export", host: "demo.example.com"
2026/09/12 12:31:20 [error] 1203#1203: *48851 recv() failed (104: Connection reset by peer) while reading response header from upstream, client: 10.0.0.23, ...
2026/09/12 12:31:31 [error] 1203#1203: *48902 no live upstreams while connecting to upstream, client: 10.0.0.24, ...这五行值得逐个背,每一种是不同的“死因”:
connect() failed (111: Connection refused) while connecting to upstream:TCP 握手直接被拒。那台机器在,但那个端口没人监听:Java 进程死了、还没起来、或者配置里端口写错了。→ 502。upstream prematurely closed connection while reading response header from upstream:连接是通的,请求也发出去了,但后端在给出响应头之前就把连接关了。典型:后端进程崩溃/被 OOM Killer 杀、正好在重启、或者复用了一个后端已经关闭的 keepalive 连接。→ 502。upstream timed out (110: Connection timed out) while reading response header from upstream:连接通、请求发出,但proxy_read_timeout(默认 60s)内没收到任何数据。→ 504。注意:如果是 while connecting to upstream,说明是连接超时(proxy_connect_timeout,默认 60s,实际通常不超过 75s)。recv() failed (104: Connection reset by peer):后端发了 RST,比如进程被强杀、或中间的防火墙/负载均衡器丢了连接状态。→ 502。no live upstreams while connecting to upstream:upstream 组里的所有 server 都被 Nginx 暂时标记为不可用(连续失败超过max_fails,默认 1 次,fail_timeout默认 10s 内),于是干脆不试了,直接 502。这就是为什么“后端明明恢复了,还要再 502 一会儿”。
还有一个我踩过的:upstream sent too big header while reading response header from upstream,后端返回的响应头(比如一个巨大的 Set-Cookie)超过 proxy_buffer_size,也是 502。改大 proxy_buffer_size 即可。
第 2 步:给 access_log 加上“上游三件套”
默认日志格式最大的缺点是:看不出这次请求打到了哪台后端、后端花了多久。在 http 块里加一个自定义格式:
log_format upstream_log '$remote_addr [$time_local] "$request" $status '
'rt=$request_time urt=$upstream_response_time '
'uct=$upstream_connect_time uht=$upstream_header_time '
'addr=$upstream_addr ustatus=$upstream_status';
access_log /var/log/nginx/access.log upstream_log;各变量含义(来自 Nginx 官方文档):
$request_time:从读到客户端第一个字节,到把响应最后一个字节发给客户端的总耗时,秒,毫秒精度;$upstream_response_time:从上游收响应用的时间;如果有多次尝试,用逗号分隔(如0.001, 30.002);$upstream_addr:实际连接的上游 IP:端口,多次重试同样用逗号分隔;$upstream_status:上游返回的状态码;Nginx 选不出上游时为 502;$upstream_connect_time/$upstream_header_time:建连耗时 / 收到响应头耗时,用来区分“连不上”与“连上了但慢”。
nginx -t 检查语法,然后平滑加载。永远先 -t 再 reload,否则语法错了 reload 会失败,你还以为改好了:
$ nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
$ nginx -s reload过几分钟再看日志,形状就完全不同了:
$ grep -E ' (502|504) ' /var/log/nginx/access.log | tail -n 4
10.0.0.21 [12/Sep/2026:12:40:07 +0800] "POST /api/order/create HTTP/1.1" 502 rt=0.000 urt=0.000 uct=- uht=- addr=10.0.1.11:8080 ustatus=502
10.0.0.22 [12/Sep/2026:12:40:08 +0800] "GET /api/user/info HTTP/1.1" 502 rt=0.003 urt=0.003, 0.001 uct=0.000, 0.000 uht=- addr=10.0.1.12:8080, 10.0.1.11:8080 ustatus=502, 502
10.0.0.34 [12/Sep/2026:12:40:12 +0800] "GET /api/report/export HTTP/1.1" 504 rt=60.001 urt=60.001 uct=0.000 uht=- addr=10.0.1.11:8080 ustatus=504读法:
- 第一行:
rt=0.000,连接瞬间失败、uct=-没建成连接——典型 connection refused,11 这台后端根本没在监听; - 第二行:两个地址、两个 502,说明 Nginx 试了 12 又试了 11,两台都挂了(或者刚好在滚动发布);
- 第三行:
rt=60.001,精确等于 60 秒,是proxy_read_timeout的默认值——504 的标准指纹。看到耗时恰好是整数秒(60 / 30 / 120),基本可以断定是某个超时配置在起作用。
第 3 步:确认后端到底活没活
$ ss -lntp | grep 8080 # 后端机器上:8080 有没有监听
LISTEN 0 100 *:8080 *:* users:(("java",pid=8231,fd=45))
$ curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" http://10.0.1.11:8080/actuator/health
200 0.012s
$ journalctl -u demo-order --since "12:25" | grep -E 'Started|Stopping|Killed'
Sep 12 12:30:58 app-01 systemd[1]: Stopping demo-order...
Sep 12 12:31:19 app-01 systemd[1]: Started demo-order.最后一条日志破案了:12:30:58 到 12:31:19,后端被重启,中间 21 秒 8080 端口没人听,Nginx 当然 502。原来是同事在午饭前“顺手”发了个版,没有摘流量。这是 502 最常见的死法。
四、根因:几个经典机制
1. 502 = 上游不可达或被中途掐断
Nginx 作为代理,向上游发起 TCP 连接、转发请求、读取响应头。任何一步出现“对方主动拒绝/重置/提前关闭”,都会返回 502。具体原因常见的有:后端进程挂了或正在重启;端口/域名配错(比如 proxy_pass 写成了 8081);后端 Tomcat 线程池和 accept 队列打满被拒绝;容器内服务监听了 127.0.0.1 而不是 0.0.0.0,跨容器访问被拒;SELinux 拦截 Nginx 访问网络端口(connect() to ... failed (13: Permission denied))。
2. 504 = 等上游超过了超时时间
官方文档对 proxy_read_timeout 的定义很重要:“超时只设置在两次连续读操作之间,而不是整个响应的传输时间”。默认 60s。所以一个接口慢 70 秒但中途一直有数据流出(流式响应),不会触发 504;而一个接口 61 秒都不吐一个字节,就会 504。此外还有 proxy_connect_timeout(建连)和 proxy_send_timeout(发送请求)。
关键认知:Nginx 504 之后,后端那个请求通常还在继续跑。Nginx 关闭了到上游的连接,但 Tomcat 不一定会中断正在执行的业务线程。所以 504 之后,你还要去后端查“谁还在慢慢算”,否则线程会被慢请求一个个占满,进而引发更多 504——这是个正反馈。
3. keepalive:偶发 502 的隐形杀手
Nginx 默认(在较老版本里)以 HTTP/1.0 访问上游,并带 Connection: close,每个请求新建一个 TCP 连接,性能差但“不会出现复用过期连接”的问题。很多人为了提性能加了 upstream keepalive:
upstream demo_backend {
server 10.0.1.11:8080 max_fails=2 fail_timeout=10s;
server 10.0.1.12:8080 max_fails=2 fail_timeout=10s;
keepalive 32; # 每个 worker 缓存最多 32 个空闲长连接
}
server {
location /api/ {
proxy_pass http://demo_backend;
proxy_http_version 1.1; # 长连接需要 HTTP/1.1
proxy_set_header Connection ""; # 清掉默认的 Connection: close
}
}官方文档明确说:想让上游 keepalive 生效,要把 proxy_http_version 设成 1.1 并清空 Connection 头(注意:官方文档提到较新的版本已经把这些默认值调整了,所以先 nginx -V 看版本,再对照对应版本的文档,旧版本必须显式写)。
但开了长连接后会有一个经典坑:Tomcat 的 keepAliveTimeout 默认 20 秒(Spring Boot 内嵌 Tomcat 使用 connectionTimeout 的值,默认 20s),而 Nginx 那边的空闲连接可能存活更久。Tomcat 在某个瞬间关闭了一条空闲连接,而 Nginx 恰好在同一瞬间拿这条连接发请求——请求发到了一个已经关闭的连接上,结果是 upstream prematurely closed connection 或 Connection reset by peer,用户看到偶发的 502。特点是:概率很低(每万个请求几个)、没有规律、重试就好。修法:让 Nginx 侧的空闲连接寿命短于后端(较新的 Nginx 有 keepalive_timeout(upstream 内)指令,默认 60s,可设为略小于 Tomcat 的值),或者接受它并依赖下面的 proxy_next_upstream 重试。
4. 后端重启瞬间的 502 与 no live upstreams
如前所述,滚动发布时如果没有先把流量摘掉,哪怕后端重启只要 20 秒,这 20 秒里 Nginx 收到的所有请求都会 502。更隐蔽的是:max_fails 默认为 1、fail_timeout 默认 10s,Nginx 只要连不上一次,就会把这台 server 标记为“不可用”10 秒,如果 upstream 里只有一台,就直接 no live upstreams。也就是后端好了,你还要再 502 十秒。单台 upstream 的话,可以把 max_fails=0 关掉失败判定,或者加多一台。
5. proxy_next_upstream:重试是把双刃剑
默认 proxy_next_upstream error timeout;:出现连接错误或超时,就换下一台。这对 502 有缓冲作用:一台后端在重启,另一台还活着,请求会自动被重试到另一台,用户无感。但要注意两点:
- 非幂等请求(POST/PATCH/LOCK)默认不会被重试,除非你显式加了
non_idempotent。所以上面日志里POST /api/order/create直接 502,没有重试,这是正确的保护——你不想同一个下单请求被打两次; - 如果配了
proxy_next_upstream error timeout http_500 http_502 http_503;,一个慢请求超时后会被再发到另一台,一个 60 秒的慢查询会变成两个 60 秒的慢查询,同时把两台后端都拖垮,还多耗一倍时间。可以再用proxy_next_upstream_tries 2与proxy_next_upstream_timeout 10s限制重试次数与总时间。
五、止血与修复
止血
- 502 + Connection refused:先把后端拉起来
systemctl start,确认ss -lntp有监听;起不来就去看应用日志; - 只有一台挂:在 upstream 里给这台加
down标记,nginx -t && nginx -s reload,让流量只走好的那台,别让用户去碰运气; - 504 + 集中在某个接口:给该 location 单独限流或临时返回降级页,避免慢请求占满后端线程;然后去后端
jstack看线程卡在哪(可参考 CPU、线程池那几篇); - 不要盲目调大
proxy_read_timeout来“解决”504——这是把警报关了,而不是灭火,后端该慢还是慢。
根治
# 1) 给长耗时接口(导出/报表)单独放宽,而不是全局放宽
location /api/report/export {
proxy_pass http://demo_backend;
proxy_read_timeout 300s;
}
# 2) 常规接口保持较短超时,快速失败
location /api/ {
proxy_pass http://demo_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_connect_timeout 3s; # 连接超时别用默认 60s,3~5s 足够
proxy_read_timeout 30s;
proxy_next_upstream error timeout; # 默认值,写出来是为了显式;不要轻易加 http_500
proxy_next_upstream_tries 2;
}发布流程也要改:先摘流量,再停后端,起好并健康检查通过,再挂回去。对 Nginx 开源版可以用 down + reload,或用配置中心/注册中心做网关;Spring Boot 可以开优雅停机 server.shutdown=graceful,让正在处理的请求跑完再退出。
六、复盘
| 做法 | 为什么 |
|---|---|
| 看到 502/504 先看 error.log 的 “while ...” 半句 | 它直接说明卡在连接、发送还是读响应头,比猜靠谱得多 |
access_log 常备 $upstream_addr $upstream_response_time $request_time $upstream_status | 能一眼区分“哪台后端、慢了多久、重试了几次”,耗时正好整数秒就是超时配置 |
upstream 长连接必须 proxy_http_version 1.1 + Connection "" | 否则 keepalive 不生效;生效后又要留意后端空闲超时短于 Nginx 引发的偶发 502 |
| 发布先摘流量,配合优雅停机 | 重启瞬间的 502 是自己造的,完全可避免 |
不要把 http_500、non_idempotent 随手加进 proxy_next_upstream | 会放大流量、重复下单,慢请求变两倍慢 |
改配置先 nginx -t 再 nginx -s reload | 语法错误 reload 会失败,别以为已经生效 |
| 监控 5xx 比例与 upstream 耗时 P99 | 让告警比用户先发现问题 |
一句话备忘:502 看进程与连接(refused / reset / prematurely closed),504 看后端慢(proxy_read_timeout 默认 60s);日志加
$upstream_addr+$upstream_response_time,耗时恰好整数秒就是超时;改完nginx -t再 reload。
那天我把日志截图发给前端:“不是我挂了,是我被重启了。”他回了个“?”,我又补一句:“下次发版前,麻烦先摘流量。”群里安静了三秒,然后有人默默发了个“收到”。从那以后,我们发版清单第一行就是这一句。
参考资料
- Nginx 官方文档:ngx_http_proxy_module——proxy_read_timeout、proxy_connect_timeout、proxy_next_upstream、proxy_http_version、proxy_request_buffering 等指令的默认值与语义。
- Nginx 官方文档:ngx_http_upstream_module——upstream keepalive 的推荐配置,以及 $upstream_addr、$upstream_status、$upstream_response_time 等变量含义。
- Nginx 官方文档:WebSocket proxying——Upgrade/Connection 头与默认 60 秒超时(下一篇会展开)。
一起聊聊