一、先说一个你大概率会遇到的场景
周五下午,群里弹出一条告警:「xx 接口成功率下降」。
你点开页面,白屏,中间一行 502 Bad Gateway。心跳先快了一拍,然后你做了一件几乎所有人都会做的事------打开搜索引擎,输入「502 怎么解决」。
搜索结果告诉你十几种可能:服务挂了、端口不对、超时、防火墙、SELinux、PHP-FPM 没起......于是你挨个试,最后 systemctl restart 敲下去,页面居然好了。你松了口气,在群里回一句「重启后恢复」。
问题真的解决了吗?大概率没有。你既不知道它当初为什么挂,也不知道下周会不会再挂。更麻烦的是,重启把现场抹掉了,后面想复盘都没线索。
如果你还在学校,可以把这篇当成入职前的预习;如果你已经入职,这篇能帮你少挨一次骂。
先说结论:新人排障缺的从来不是命令,是顺序。
排障的本质不是「找到一条能用的命令」,而是每一步只问一个问题,根据答案决定下一步往哪走,把范围一点点缩小。命令只是顺手的工具。下面这条线,就是我想让你记住的东西。
二、先搞懂:502 到底是谁在报错
查任何错误之前,得先知道错误可能出在哪一段。一个最普通的 Web 请求,链路长这样:
关键理解:502 是「中间人」报的错,不是干活那个人报的错。
Nginx 在这儿的角色像公司前台帮你转接电话:它替你把请求交给后面的应用,再等应用回话。如果应用没回话、回得不像话、或者压根没人接,前台只能尴尬地告诉你一句「Bad Gateway」。
把三个最容易混的状态码摆一起看,就清楚了:
| 状态码 | 谁返回的 | 大白话 | 通常往哪查 |
|---|---|---|---|
| 500 | 应用自己 | 「我干活时自己崩了」 | 应用日志里的异常堆栈 |
| 502 | 网关 | 「我找上游,没拿到合法响应」 | 上游进程、端口、连接是否被拒 |
| 504 | 网关 | 「上游接了,但迟迟不回」 | 上游为什么慢、超时时间配了多少 |
注意:502 和 504 在实际配置下经常互相串,取决于 Nginx 版本和超时参数。所以别一上来就死抠「到底是 502 还是 504」,先按下一步开始查。
这张表就是你的地图。后面四步,本质是沿着地图从上往下逐段确认「这一段还通不通」。
三、四步排查法,先记住这个顺序
text
第 1 步:确认 502 到底是谁返回的(别查错对象)
↓
第 2 步:确认上游进程还在不在、端口通不通(链路断没断)
↓
第 3 步:时间对齐,翻应用日志(活着但干不动)
↓
第 4 步:看资源和依赖(CPU / 线程 / 连接池 / 下游)
为什么必须按这个顺序?因为越靠前的步骤成本越低、排除范围越大。
第 1 步花 10 秒,就能避免你去查一台根本不相干的机器;第 2 步能一次性排除「进程没了」这一大类原因。反过来,一头扎进 JVM 线程栈,忙活半小时才发现服务根本没启动------这是新人最常吃的亏。
四、每一步具体怎么做
先把整条判断路径画出来,你照着往下走就行。每一个菱形都是一个「只问一个问题」的路口:
下面把每一步的命令和判断标准说清楚。
第 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
两个新人特别容易忽略的细节:
- 时区 。告警平台常常用 UTC,应用日志是
Asia/Shanghai,差 8 小时。对齐不上,就会得出「日志里啥也没有」的错误结论。 - 脱敏 。贴日志到群里、写进复盘文档之前,手机号、身份证、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 开发」这两件事连起来了。
先看一次请求在正常和变慢两种情况下,分别发生了什么:
注意到了吗?客户端第 3 秒就拿到错误了,但其实第 8 秒应用还在干活。 这一单活儿白干了,占用的资源却一点没少。
再往里挖一层。把 Spring Boot 内置的 Tomcat 想成一家银行:
text
请求 = 来办业务的顾客
Tomcat 线程池 = 开放的柜台窗口(默认大约 200 个)
等待队列 = 大厅里排的队(默认大约 100 个位置)
一次数据库查询 = 柜员去后台库房查资料
正常情况下,柜员查完资料就回来,窗口很快空出来。但如果某个接口被一条慢 SQL 卡住,柜员就被钉在窗口上动不了。这样的请求一多,就会走到下面这个分叉:
这张图解释了很多人困惑的一点:同样是接口慢,有时候报 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、循环里调接口、大对象序列化 | 最根本,但要先定位到具体那行 |
| 异步化 + 线程池隔离 | 代码 | 确实耗时、又必须触发的任务 | 引入异步一致性、结果回查的复杂度 |
| 限流 / 熔断 | 依赖 / 配置 | 下游不稳定,需要先保护自己 | 需要额外组件和阈值调试 |
养成一个习惯:先说清楚改哪一层、为什么改,再谈怎么改。这比上来就贴一整段重构代码靠谱得多,也更容易通过代码评审。
七、几个容易踩的坑
- 第一件事就重启。重启会抹掉线程栈、内存现场和那一刻的日志上下文。除非是为了先恢复业务,并且已经有人同步保留了日志和堆栈。
- 在生产上「顺便」改个配置试试。生产变更要走「提方案 → 人工确认 → 审批 → 执行 → 可回滚」这一套,别自己在终端里临时改。想试,去测试环境试。
- 时间没对齐就下结论。UTC 和东八区差 8 小时,是「日志里什么都没查到」最常见的原因。
- 把日志原文直接贴群。手机号、Token、客户信息先打码,没有例外。
- 只看错误码不看时间线。同一个 502,发生在发布后 2 分钟和发生在平稳运行 3 天后,根因完全不是一回事。养成先问一句「最近有什么变更」的习惯。
- 修完就结束。至少要确认:错误率是否回落、慢接口 P99 是否恢复、下游有没有被压垮、要不要补一条告警或写一份复盘。
八、想补基础,按这个顺序来
不用贪多,这三步是有先后的,别跳着学:
- HTTP 与网关常识(1~2 周):状态码是谁返回的、Header 里有什么、反向代理在做什么。能把第二节的链路图画给同学或同事讲一遍,就算过关。
- Linux 基础与日志检索 (2~4 周):
ps/ss/curl/tail/grep/top这六个用熟,再加一个「时间盒 + 关键字 + traceId 逐步收敛」的检索习惯。不用背命令手册。 - 自己服务的运行时 (1~2 个月):Tomcat 线程池、连接池、GC 基本概念,能看懂一份
jstack。这时候再回头看第五节那两张图,会有完全不同的感觉。
配套动作:给自己建一个「踩坑笔记」,每处理一个问题记四行------现象、我按什么顺序查的、根因在哪一层、下次怎么更早发现。半年后这份笔记比任何八股文都值钱。
九、最后总结
一句话收尾:502 不是「一个错误」,而是「一段链路里某一节断了」的信号。排查的价值在于顺序,不在于命令。
四步再背一遍:
- 确认是谁报的错;
- 确认上游还在不在(记得用
curl直连绕开网关); - 对齐时间翻日志;
- 看资源与依赖。
每一步都只回答一个问题,答案决定下一步往哪走。
今晚就能动手的三件小事:
- 把第六节的 Demo 在本地跑一遍,亲手对比「直连正常 / 走网关报错」,把这个感觉记住。
- 给你负责的服务画一张链路图,标出每一段的超时配置是多少,看看有没有明显不合理的地方。
- 建一个踩坑笔记文件,用「现象 / 排查顺序 / 根因层次 / 改进项」四行模板,下次遇到问题就填。
下一篇我们顺着这条线往下走一步:CPU 突然 100% 时,从告警到定位那 5 分钟该怎么过 。会用到 top -H 和 jstack,正好接上今天第 4 步没展开的部分。