Nginx 与网络排查 列表全部文章
Nginx 与网络排查

502 还是 504?Nginx 给你的不是报错,是一张“尸检报告”

前端喊“接口 502”,后端说“我这儿好好的”。本文讲清 502(上游连不上/被掐断)与 504(上游超时)的区别,用 error.log 原文加自定义 access_log 三个 upstream 变量定位问题,并解释 keepalive、重启瞬间 502 与 proxy_next_upstream 的坑。

更多

那天中午十二点半,我刚端起外卖,前端同学在群里发了一张截图:浏览器一片白,中间一行灰色大字 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 分别发生在哪一段。

浏览器发起请求 Nginx 网关生成 502 / 504 Tomcat 应用10.0.1.11:8080 下游 DB/RPC可能很慢 502:连不上 / 被掐断 refused · reset · prematurely closed 504:等太久(默认 60s) timed out while reading response header 看 error.log 的 “while ...” 后半句,就知道卡在连接、发送还是读响应头

再来一张决策树式的小抄,我打印出来贴在了显示器边上:

先看 error.log 关键字 Connection refused进程挂 / 端口错 / 没监听 prematurely closed后端崩溃 / 重启 / keepalive timed out (110)504:上游慢,查线程/SQL no live upstreams全部被标记失败,查健康 再用 $upstream_addr / $upstream_response_time 判断是哪一台、慢了多久

三、动手:从日志里把上游“审”出来

第 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 限制重试次数与总时间。

五、止血与修复

止血

  1. 502 + Connection refused:先把后端拉起来 systemctl start,确认 ss -lntp 有监听;起不来就去看应用日志;
  2. 只有一台挂:在 upstream 里给这台加 down 标记,nginx -t && nginx -s reload,让流量只走好的那台,别让用户去碰运气;
  3. 504 + 集中在某个接口:给该 location 单独限流或临时返回降级页,避免慢请求占满后端线程;然后去后端 jstack 看线程卡在哪(可参考 CPU、线程池那几篇);
  4. 不要盲目调大 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。

那天我把日志截图发给前端:“不是我挂了,是我被重启了。”他回了个“?”,我又补一句:“下次发版前,麻烦先摘流量。”群里安静了三秒,然后有人默默发了个“收到”。从那以后,我们发版清单第一行就是这一句。

参考资料

— 感谢读到这里 —发现错误,或想补充?告诉我 →
TURN KNOWLEDGE INTO SOMETHING REAL

有想法,也想把它做出来?

从技术方案到上线交付,聊聊你的下一个项目。

聊聊需求

一起聊聊

搜索文章

输入关键词,按标题和摘要即时筛选。

浏览分类文章归档

阅读设置

字号
行宽
专注模式

专注模式会隐藏页头与侧栏,只留正文。设置保存在当前浏览器。

稍后阅读

保存在当前浏览器,留给下一次安静阅读。

总浏览 -- 次 · 总访客 -- 人 · 今日浏览 -- 次 · 今日访客 -- 人