[运维排障实录] 第一次遇到 502 别急着重启:按「一条线」把问题查清楚

一、先说一个你大概率会遇到的场景

周五下午,群里弹出一条告警:「xx 接口成功率下降」。

你点开页面,白屏,中间一行 502 Bad Gateway。心跳先快了一拍,然后你做了一件几乎所有人都会做的事------打开搜索引擎,输入「502 怎么解决」。

搜索结果告诉你十几种可能:服务挂了、端口不对、超时、防火墙、SELinux、PHP-FPM 没起......于是你挨个试,最后 systemctl restart 敲下去,页面居然好了。你松了口气,在群里回一句「重启后恢复」。

问题真的解决了吗?大概率没有。你既不知道它当初为什么挂,也不知道下周会不会再挂。更麻烦的是,重启把现场抹掉了,后面想复盘都没线索。

如果你还在学校,可以把这篇当成入职前的预习;如果你已经入职,这篇能帮你少挨一次骂。

先说结论:新人排障缺的从来不是命令,是顺序。

排障的本质不是「找到一条能用的命令」,而是每一步只问一个问题,根据答案决定下一步往哪走,把范围一点点缩小。命令只是顺手的工具。下面这条线,就是我想让你记住的东西。


二、先搞懂:502 到底是谁在报错

查任何错误之前,得先知道错误可能出在哪一段。一个最普通的 Web 请求,链路长这样:

flowchart LR A[&#34;浏览器&#34;] -->|&#34;① HTTP 请求&#34;| B[&#34;Nginx / 网关<br/>反向代理&#34;] B -->|&#34;② proxy_pass 转发&#34;| C[&#34;Spring Boot 应用<br/>内置 Tomcat&#34;] C -->|&#34;③ 查库 / 调缓存&#34;| D[(&#34;MySQL / Redis&#34;)] C -.->|&#34;④ 返回结果&#34;| B B -.->|&#34;⑤ 渲染页面&#34;| A

关键理解:502 是「中间人」报的错,不是干活那个人报的错。

Nginx 在这儿的角色像公司前台帮你转接电话:它替你把请求交给后面的应用,再等应用回话。如果应用没回话、回得不像话、或者压根没人接,前台只能尴尬地告诉你一句「Bad Gateway」。

把三个最容易混的状态码摆一起看,就清楚了:

状态码 谁返回的 大白话 通常往哪查
500 应用自己 「我干活时自己崩了」 应用日志里的异常堆栈
502 网关 「我找上游,没拿到合法响应」 上游进程、端口、连接是否被拒
504 网关 「上游接了,但迟迟不回」 上游为什么慢、超时时间配了多少

注意:502 和 504 在实际配置下经常互相串,取决于 Nginx 版本和超时参数。所以别一上来就死抠「到底是 502 还是 504」,先按下一步开始查。

这张表就是你的地图。后面四步,本质是沿着地图从上往下逐段确认「这一段还通不通」。


三、四步排查法,先记住这个顺序

text 复制代码
第 1 步:确认 502 到底是谁返回的(别查错对象)
   ↓
第 2 步:确认上游进程还在不在、端口通不通(链路断没断)
   ↓
第 3 步:时间对齐,翻应用日志(活着但干不动)
   ↓
第 4 步:看资源和依赖(CPU / 线程 / 连接池 / 下游)

为什么必须按这个顺序?因为越靠前的步骤成本越低、排除范围越大。

第 1 步花 10 秒,就能避免你去查一台根本不相干的机器;第 2 步能一次性排除「进程没了」这一大类原因。反过来,一头扎进 JVM 线程栈,忙活半小时才发现服务根本没启动------这是新人最常吃的亏。


四、每一步具体怎么做

先把整条判断路径画出来,你照着往下走就行。每一个菱形都是一个「只问一个问题」的路口:

flowchart TD S([&#34;页面出现 502&#34;]) --> Q1{&#34;网关日志里有<br/>这条请求吗?&#34;} Q1 -->|&#34;没有&#34;| E1[&#34;请求根本没到网关<br/>查 DNS / 负载均衡 / 客户端网络&#34;] Q1 -->|&#34;有&#34;| Q2{&#34;curl 直连应用<br/>通不通?&#34;} Q2 -->|&#34;通&#34;| E2[&#34;问题在网关到应用之间<br/>转发地址 / 端口 / 超时配置&#34;] Q2 -->|&#34;不通&#34;| Q3{&#34;同一分钟的日志里<br/>有异常堆栈吗?&#34;} Q3 -->|&#34;有&#34;| E3[&#34;根因就在这个异常<br/>OOM / 连接数超限 / 死锁&#34;] Q3 -->|&#34;没有&#34;| Q4{&#34;CPU / 线程数 / 连接池<br/>有异常吗?&#34;} Q4 -->|&#34;有&#34;| E4[&#34;优化慢操作 + 线程池隔离<br/>必要时加限流&#34;] Q4 -->|&#34;没有&#34;| E5[&#34;继续往下游查<br/>DB / Redis / 依赖服务&#34;]

下面把每一步的命令和判断标准说清楚。

第 1 步:确认 502 是谁返回的

浏览器按 F12 → Network → 点那条失败的请求 → 看 Response Headers。或者直接看 Nginx 访问日志:

bash 复制代码
# 看网关访问日志最后 20 行,找 status 为 502 的那条
# -f 表示跟随文件追加,排查完记得按 Ctrl+C 退出
tail -20f /var/log/nginx/access.log | grep ' 502 '

更直接的办法是看响应体。很多网关会在 502 页面带上自己的标记(比如 nginx 字样,或者公司网关特有的错误 JSON)。如果页面是公司统一网关的样式,说明你要查的是那一层的配置和它后面的上游,而不是自己机器上的 Nginx。

怎么判断往下走:如果连网关日志里都没有这条请求,说明请求压根没到网关------去查 DNS、负载均衡、客户端网络,别在应用上浪费时间。

第 2 步:上游进程还在不在、端口通不通

bash 复制代码
# 1) 看应用进程是否存在(把 your-app 换成你的 jar 包关键字)
ps -ef | grep your-app | grep -v grep

# 2) 看应用监听的端口(示例用 8080)是否处于 LISTEN 状态
ss -lntp | grep 8080

# 3) 绕过 Nginx,直接问应用本人 ------ 这是整套流程里最关键的一刀
curl -i --max-time 5 http://127.0.0.1:8080/health

第 3 条命令性价比最高:绕过网关直连应用。

  • 直连通了 → 应用还在,问题出在网关到应用这一段(转发地址、端口、超时配置)。
  • 直连都不通 → 问题在应用本身,进第 3 步。
  • 直连返回很慢甚至超时 → 应用活着但干不动,也进第 3 步,并重点关注第 4 步的线程和连接池。

第 3 步:时间对齐,翻应用日志

拿告警发生的分钟,去应用日志里找同一分钟的记录:

bash 复制代码
# 按时间窗口过滤(示例:14:3x 这一分钟内),只看 ERROR / WARN
grep '2026-11-12 14:3' /data/logs/app/app.log | grep -E 'ERROR|WARN' | head -50

# 如果公司有统一日志平台,用 traceId 反查整条链路更准
# 检索式示例:traceId:"a1b2****" AND level:ERROR

两个新人特别容易忽略的细节:

  1. 时区 。告警平台常常用 UTC,应用日志是 Asia/Shanghai,差 8 小时。对齐不上,就会得出「日志里啥也没有」的错误结论。
  2. 脱敏 。贴日志到群里、写进复盘文档之前,手机号、身份证、Token、客户名一律打码,比如 138****5678、Token 只留前 4 位。这不是形式主义,是保护别人也保护你自己。

怎么判断往下走 :如果同一分钟有明确的异常堆栈(比如 OutOfMemoryError、Too many connections),根因大概率就在这儿,不用再往下翻。如果日志干干净净但接口就是不响应,进第 4 步------常见于线程池打满、线程死锁这类「不报错但不动」的情况。

第 4 步:看资源和依赖

bash 复制代码
# 整体负载,主要看 CPU 使用率和 load
top -bn1 | head -20

# 进程内的线程情况,线程数暴涨往往就是线索
top -H -p <应用PID> -bn1 | head -20

# 抓一份线程快照,看线程都卡在哪(生产环境请先确认权限与影响)
jstack <应用PID> > /tmp/jstack_$(date +%H%M).txt

再顺着第二节那张地图往下游看:数据库连接池是不是满了、Redis 是不是超时、下游服务是不是正在发布。经验上,「应用没报错但不响应」有相当一部分是在等下游。

四步走完,范围基本能收敛到一个具体点上了。


五、原理:为什么「接口慢」会变成网关的 502 / 504

这一段最值得新人搞懂,因为它把「运维」和「Java 开发」这两件事连起来了。

先看一次请求在正常和变慢两种情况下,分别发生了什么:

sequenceDiagram autonumber participant U as 浏览器 participant N as Nginx participant A as Spring Boot participant D as MySQL Note over U,D: 正常情况:查询 20 毫秒就回话 U->>N: GET /api/order N->>A: 转发请求 A->>D: SELECT ... D-->>A: 返回结果 A-->>N: 200 OK N-->>U: 200 OK Note over U,D: 接口变慢:这次查询卡了 8 秒 U->>N: GET /api/order N->>A: 转发请求 A->>D: SELECT ...(卡住 8 秒) Note over N: 等满 proxy_read_timeout(3 秒) N-->>U: 504 Gateway Timeout D-->>A: 8 秒后才返回 A-->>N: 响应已经没人接收了

注意到了吗?客户端第 3 秒就拿到错误了,但其实第 8 秒应用还在干活。 这一单活儿白干了,占用的资源却一点没少。

再往里挖一层。把 Spring Boot 内置的 Tomcat 想成一家银行:

text 复制代码
请求 = 来办业务的顾客
Tomcat 线程池 = 开放的柜台窗口(默认大约 200 个)
等待队列 = 大厅里排的队(默认大约 100 个位置)
一次数据库查询 = 柜员去后台库房查资料

正常情况下,柜员查完资料就回来,窗口很快空出来。但如果某个接口被一条慢 SQL 卡住,柜员就被钉在窗口上动不了。这样的请求一多,就会走到下面这个分叉:

flowchart TD R([&#34;新请求进来&#34;]) --> T{&#34;还有空闲的<br/>工作线程吗?&#34;} T -->|&#34;有&#34;| OK[&#34;分配一个线程执行<br/>接口很快,正常返回 200&#34;] T -->|&#34;没有&#34;| Q{&#34;等待队列<br/>还有位置吗?&#34;} Q -->|&#34;有&#34;| WAIT[&#34;在队列里排队<br/>排太久 → 网关等超时,报 504&#34;] Q -->|&#34;没有&#34;| REJ[&#34;连接直接被拒绝<br/>→ 网关拿不到响应,报 502&#34;]

这张图解释了很多人困惑的一点:同样是接口慢,有时候报 502,有时候报 504。 区别只在于请求当时卡在了「排队」还是「被拒」这一步。

所以:一个接口写得慢,最后是以「网关报错」的面目暴露出来的。 这也是为什么只看 Nginx 日志永远查不出根因------它只告诉你「我没等到」,不告诉你「里面为什么慢」。


六、动手:在自己电脑上把 502 造出来

光看不练记不住。下面这个 Demo 只需要 JDK 8+、一个 Spring Boot 项目和本机 Nginx。

1. 写一个故意很慢的接口

java 复制代码
package com.example.demo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@RestController
public class SlowController {

    /**
     * 反例:故意模拟一个慢接口。
     * 真实业务里,这里可能是一次没加索引的查询、一个同步调用的第三方接口,
     * 或者一段大文件的同步处理 ------ 效果一样,都是把 Tomcat 线程长时间占住。
     */
    @GetMapping("/slow")
    public Map<String, Object> slow() throws InterruptedException {
        long start = System.currentTimeMillis();

        Thread.sleep(10_000); // 占住当前请求线程 10 秒,什么都不干

        Map<String, Object> result = new HashMap<>();
        result.put("time", LocalDateTime.now().toString());
        result.put("costMs", System.currentTimeMillis() - start);
        return result;
    }

    /** 健康检查接口:第 2 步「直连应用」就靠它验证,必须永远快 */
    @GetMapping("/health")
    public String health() {
        return "UP";
    }
}

2. 配一个「耐心值很短」的网关

nginx 复制代码
server {
    listen 80;
    server_name localhost;

    location / {
        proxy_pass http://127.0.0.1:8080;   # 把请求转给本机 Spring Boot

        # 关键三行:连接上游、发送请求、等待响应各自的超时
        # 这里故意设成 3 秒,方便复现问题
        proxy_connect_timeout 3s;
        proxy_send_timeout    3s;
        proxy_read_timeout    3s;           # 等上游回应最多 3 秒,接口却要睡 10 秒

        proxy_set_header Host $host;         # 把原始 Host 透传过去,便于排查
    }
}

3. 跑起来,观察现象

bash 复制代码
# 1) 启动应用(按你的项目实际情况替换 jar 名)
java -jar demo-0.0.1-SNAPSHOT.jar --server.port=8080

# 2) 先直连应用:会等 10 秒然后正常返回 ------ 说明应用本身没问题
curl -i --max-time 15 http://127.0.0.1:8080/slow

# 3) 再从网关走:3 秒就被掐断,拿到 504 / 502
curl -i --max-time 15 http://127.0.0.1/slow

# 4) 并发压一下,观察 Tomcat 线程被占满后的表现
for i in $(seq 1 30); do curl -s -o /dev/null http://127.0.0.1/slow & done; wait

第 2 步和第 3 步的对比,就是「绕过网关直连应用」这一刀的价值:同一个接口,直连正常、走网关报错,问题范围瞬间缩小到网关与上游之间。

4. 怎么修,四种方案的取舍

java 复制代码
// 方案一(最不推荐):只是把网关超时调大
// proxy_read_timeout 60s;
// 相当于让前台多等一会儿,柜员还是被钉在窗口上。
// 流量一大照样雪崩,属于掩耳盗铃。

// 方案二(入门常用):把耗时操作挪出 Web 线程
// 注意:生产上要用有界队列 + 明确拒绝策略的自定义线程池,
// 别直接用下面这个简化写法(默认线程池队列近乎无界,反而会堆积)
@GetMapping("/slow-async")
public String slowAsync() {
    CompletableFuture.runAsync(() -> doHeavyWork()); // 交给别的线程池执行
    return "accepted";                              // 立刻释放请求线程
}
方案 改哪一层 适合场景 代价
调大网关超时 配置 临时止血,且明确知道上游只是偶发慢 治标不治本,风险后移
优化慢操作本身 代码 / SQL 慢 SQL、循环里调接口、大对象序列化 最根本,但要先定位到具体那行
异步化 + 线程池隔离 代码 确实耗时、又必须触发的任务 引入异步一致性、结果回查的复杂度
限流 / 熔断 依赖 / 配置 下游不稳定,需要先保护自己 需要额外组件和阈值调试

养成一个习惯:先说清楚改哪一层、为什么改,再谈怎么改。这比上来就贴一整段重构代码靠谱得多,也更容易通过代码评审。


七、几个容易踩的坑

  1. 第一件事就重启。重启会抹掉线程栈、内存现场和那一刻的日志上下文。除非是为了先恢复业务,并且已经有人同步保留了日志和堆栈。
  2. 在生产上「顺便」改个配置试试。生产变更要走「提方案 → 人工确认 → 审批 → 执行 → 可回滚」这一套,别自己在终端里临时改。想试,去测试环境试。
  3. 时间没对齐就下结论。UTC 和东八区差 8 小时,是「日志里什么都没查到」最常见的原因。
  4. 把日志原文直接贴群。手机号、Token、客户信息先打码,没有例外。
  5. 只看错误码不看时间线。同一个 502,发生在发布后 2 分钟和发生在平稳运行 3 天后,根因完全不是一回事。养成先问一句「最近有什么变更」的习惯。
  6. 修完就结束。至少要确认:错误率是否回落、慢接口 P99 是否恢复、下游有没有被压垮、要不要补一条告警或写一份复盘。

八、想补基础,按这个顺序来

不用贪多,这三步是有先后的,别跳着学:

flowchart LR A[&#34;第 1~2 周<br/>HTTP 与网关常识<br/>状态码是谁返回的&#34;] --> B[&#34;第 2~4 周<br/>Linux 与日志检索<br/>ps / ss / curl / grep&#34;] B --> C[&#34;第 1~2 个月<br/>服务运行时<br/>线程池 / 连接池 / jstack&#34;]
  1. HTTP 与网关常识(1~2 周):状态码是谁返回的、Header 里有什么、反向代理在做什么。能把第二节的链路图画给同学或同事讲一遍,就算过关。
  2. Linux 基础与日志检索 (2~4 周):ps / ss / curl / tail / grep / top 这六个用熟,再加一个「时间盒 + 关键字 + traceId 逐步收敛」的检索习惯。不用背命令手册。
  3. 自己服务的运行时 (1~2 个月):Tomcat 线程池、连接池、GC 基本概念,能看懂一份 jstack。这时候再回头看第五节那两张图,会有完全不同的感觉。

配套动作:给自己建一个「踩坑笔记」,每处理一个问题记四行------现象、我按什么顺序查的、根因在哪一层、下次怎么更早发现。半年后这份笔记比任何八股文都值钱。


九、最后总结

一句话收尾:502 不是「一个错误」,而是「一段链路里某一节断了」的信号。排查的价值在于顺序,不在于命令。

四步再背一遍:

  1. 确认是谁报的错;
  2. 确认上游还在不在(记得用 curl 直连绕开网关);
  3. 对齐时间翻日志;
  4. 看资源与依赖。

每一步都只回答一个问题,答案决定下一步往哪走。

今晚就能动手的三件小事:

  • 把第六节的 Demo 在本地跑一遍,亲手对比「直连正常 / 走网关报错」,把这个感觉记住。
  • 给你负责的服务画一张链路图,标出每一段的超时配置是多少,看看有没有明显不合理的地方。
  • 建一个踩坑笔记文件,用「现象 / 排查顺序 / 根因层次 / 改进项」四行模板,下次遇到问题就填。

下一篇我们顺着这条线往下走一步:CPU 突然 100% 时,从告警到定位那 5 分钟该怎么过 。会用到 top -H 和 jstack,正好接上今天第 4 步没展开的部分。

相关推荐
学长毕设6 小时前
基于SpringBoot的景点管理系统-附源码
java·spring boot·后端
知守观7 小时前
一个方法两个触发口:反编译 XXL-Job 后,我看清了 inner/job 子包的裸奔现状
java·spring boot·微服务
瑞雪兆丰年兮7 小时前
JavaWeb-day14-SpringBootWeb原理篇
java·开发语言·spring boot·spring·bean·mybatis·javaweb
杨凯凡11 小时前
Spring Boot 大文件下载如何实现断点续传?HTTP Range、206 与 416 实战
spring boot·后端·http
nice_lcj5201 天前
【从零理解 Spring AI · 14】RAG 的四个环节:加载、切分、向量化、检索
java·spring boot·embedding·向量数据库·rag·spring ai·ai应用开发
学长毕设1 天前
基于springboot科创竞赛信息平台-附源码
java·spring boot·后端
学长毕设1 天前
基于SpringBoot公务员考试交流平台-附源码
java·spring boot·后端
AI开源软件1 天前
SpringBoot3+AI Agent 智能报销:怎样把对话接进现有财务审批系统
spring boot·ai agent·人工确认·ruoyi office·agentos·智能报销·财务审批