下午刚处理完一个线上告警,群里运维发来一张截图:客户端登录直接报错 login auth failed, errormsg: response is fail. errorcode: 504。用户那边反馈就是“登录转圈然后失败”,监控面板上一排红色的 Gateway Timeout 504。如果你是后端开发或者运维,这玩意儿肯定不陌生,它不像是 500 那种代码 bug 一样能立刻定位,也不像 404 那么直白,504 意味着整个链路里有人“没接住球”,而且超时了。
我最早遇到 504 还是刚转后端那会儿,当时的反应就是“哦,超时了,那把 Nginx 超时时间调大不就行了”,结果调完更糟——用户等得更久,服务照样超时,只是错误发生得更晚了而已。这篇文章把这几年跟 504 缠斗的经验整理一下,从错误本身开始讲,一步步说清楚怎么排查、怎么解决、怎么避免以后再犯。
1. 先搞清楚504到底卡在哪一环
1.1 一次请求的“接力赛”模型
要理解 504,先得理解任何一次 WEB 请求都要经过的一条链路。客户端发起请求后,请求会依次经过 DNS 解析、进入负载均衡层、到达网关层(Nginx、HAProxy、云厂商 SLB 等)、然后转发给上游应用服务器、应用服务器再去查数据库或者调第三方接口,最终把结果原路返回给客户端。
这就像一场接力赛。网关是第二棒选手,上游应用服务器是第三棒。客户端把“接力棒”(HTTP 请求)交到网关手里,网关必须把它递到第三棒手里,然后再把第三棒的“回应”(HTTP 响应)拿回来交还给客户端。如果第三棒迟迟不回应,或者根本接不到棒子,网关就不能无限等下去,等到了自己设定的上限,就直接向客户端宣告:我不等了。这个“宣告”就是 504 Gateway Timeout。
网关报 504 的时候,它其实是在说一句话:我这边没问题,但你让我转交的对象没有在约定时间内把结果给我。这一点非常关键,因为很多人一看到 504 就以为是网关挂了,其实通常恰恰相反,网关是那个“等得不耐烦”的人,真正的问题往往在更后面的环节。
1.2 502、503、504,三个常见错误别再混为一谈
排查的时候经常有人把这几兄弟弄混,先花一分钟分清它们,能省很多弯路:
| 状态码 | 含义 | 通俗解释 | 首要排查方向 |
|---|---|---|---|
| 502 Bad Gateway | 网关收到了上游的无效响应 | 第三棒接到了棒子,但递回来的东西是坏的(连接被重置、响应残缺) | 上游服务是否崩溃、进程是否退出、端口是否在监听 |
| 503 Service Unavailable | 服务暂时不可用 | 第二棒自己这边出了问题,比如过载、正在重启、熔断 | 网关自身负载、限流策略、服务注册与发现 |
| 504 Gateway Timeout | 网关等待上游响应超时 | 第二棒把棒子递出去了,但第三棒迟迟没回应 | 上游应用处理耗时、数据库查询、第三方调用、网络延迟 |
从这张表能看出,504 的核心矛盾是“时间”。而时间花在哪,才是我们排查的关键。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 排查504的思路:永远先找“响应慢的人”
2.1 三层定位法
我排查 504 这么多年,总结了一套自己的固定套路,叫“三层定位法”。不管入口是 Nginx 还是云负载均衡,都按这个顺序来,效率最高。
第一层:确认网关日志里上游是谁、超时时间是多少。Nginx 的错误日志里会明确记录 upstream timed out 以及对应的 upstream 地址,这一行能直接告诉你“第三棒是谁”。同时看访问日志里的 upstream_response_time 字段,这个字段记录的是 Nginx 等待上游返回总共花了多少毫秒。如果这个值稳定在接近超时阈值附近,那基本可以确认是上游响应慢。
第二层:去上游应用服务器上看日志。应用日志里有没有对应时间点的请求记录?如果有,看这条请求内部处理了多久、卡在哪个调用上。如果没有,说明请求压根没到应用服务器,那问题可能出在中间的网络层、防火墙、或者应用进程已经假死(端口还在,但线程池满了,新请求进不来)。
第三层:往下游依赖查。数据库慢查询日志、Redis 响应时间、第三方接口调用耗时,逐个确认。这一步往往是最终根因所在,我遇到过太多“应用日志显示处理了 3 秒,其中 2.5 秒都在等数据库”的情况。
这个顺序是自外而内的,每一层都能排除一批可能性。最忌讳的是上来就怀疑代码,花半天看业务逻辑,结果最后发现是数据库连接池被耗尽。
2.2 必备的排查命令和工具
直接把我常用的命令列出来,照着用就行:
bash复制# 1. 看Nginx错误日志,找upstream timed out那一行
tail -n 200 /var/log/nginx/error.log | grep "upstream timed out"
# 2. 看访问日志里响应时间最慢的TOP 20请求
awk '{print $NF, $0}' /var/log/nginx/access.log | sort -rn | head -20
# 3. 直接curl测试上游,限制最长等待时间
curl -v -m 10 http://127.0.0.1:8080/api/login
# -v 显示详细过程,-m 10 表示最多等10秒,如果10秒内没返回,curl自己会退出并报错
# 4. 确认上游进程还活着,端口还在监听
ss -lntp | grep 8080
ps aux | grep java
# 5. 看系统负载
top -b -n 1 | head -20
vmstat 1 5
iostat -x 1 5
这里有个小技巧,光看 top 不够,要看 vmstat 里的 r(运行队列)和 wa(IO 等待)这两列。如果 r 持续大于 CPU 核数,说明系统在做超负荷运算;如果 wa 很高,说明磁盘 IO 成了瓶颈,进程在等磁盘响应,这种慢在网络层完全看不出来。
还有一点要注意:curl -v -m 10 测出来了不代表生产环境就一定没问题。因为 curl 是直连上游,绕过了网关,但生产环境里网关和上游之间可能隔着防火墙、安全组、或者其他网络设备。如果 curl 正常但通过网关访问就 504,那就得重点查网关和上游之间的网络连通性,比如安全组配置、路由规则之类。
3. 解决504的方案:从配置到架构,一层层拆解
3.1 网关层:超时参数的合理配置
Nginx 最常见的三个超时参数,很多教程都写过,但真正理解每个参数含义的人不多。
nginx复制location /api/ {
proxy_connect_timeout 5s; # 与上游建立TCP连接的超时时间
proxy_send_timeout 30s; # 向上游发送请求数据的超时时间
proxy_read_timeout 60s; # 等待上游返回响应的超时时间
}
proxy_connect_timeout 是建立连接的超时,这个值没必要设大,正常内网环境下几百毫秒就够连上了,我一般设置 3 到 5 秒。如果频繁报 connect timeout,说明上游服务可能没起来或者网络不通,调大这个值没有意义,只会让用户等更久。
proxy_send_timeout 和 proxy_read_timeout 是两个“数据等待”超时。前者是 Nginx 向后端发送请求数据时,如果中间出现两次写操作间隔超过这个时间就断开;后者是 Nginx 等待后端响应数据时,两次读操作间隔超过这个时间就断开。注意,proxy_read_timeout 不是“总时长限制”,而是“读间隔限制”。也就是说,只要上游每 60 秒内能吐一点数据(比如下载大文件、SSE 流式推送),一直不结束也不会触发超时。这个特性在做日志流、聊天推送这类长连接场景时很实用。
调整超时参数时要记住一个原则:先搞清楚上游到底需要多久,再设置合理的超时值。如果上游正常情况下要 100 秒才返回结果,你把 proxy_read_timeout 设成 60 秒,那必然稳定超时;反过来,如果上游正常只要 200 毫秒,某个时刻突然变成 60 秒,那说明上游已经出问题了,这时候把超时调成 120 秒也只是把问题掩盖了,用户该失败还是失败,只是从“快速失败”变成了“慢速失败”。正确的做法是让超时略大于正常 P99 响应时间,同时配合监控和告警,异常情况能第一时间发现。
另外一个常用配置是 proxy_next_upstream,当某个上游节点超时时,自动把请求转发给下一个节点,实现故障转移:
nginx复制location /api/ {
proxy_next_upstream http_502 http_504 error timeout;
proxy_next_upstream_tries 2;
}
这个配置建议配合多节点 upstream 使用,能显著减少单点故障导致的 504。但要注意 proxy_next_upstream_tries 不要太激进,设成 2 就够了,否则一次请求会在多个上游之间反复尝试,反而拖慢了整体响应。
3.2 应用层:从业务逻辑和性能层面解决根因
网关层的超时时间只是“缓冲”,真正解决问题还得看应用本身为什么不快。
如果应用处理请求本身就需要很长时间(比如导出大量数据、复杂报表计算),有几种处理方式。第一种是优化业务逻辑,把慢查询拆成多个快查询、给数据库加索引、减少循环里的远程调用。第二种是加缓存,把热点数据放到 Redis,减少重复计算。第三种是把耗时操作异步化——客户端发起请求后立即返回“已受理”,后台任务跑完后通过消息队列通知或者回调,这样客户端根本不用等那么久,也就不会触发网关超时。
如果应用本身响应很快,但出现 504 是因为并发太高导致线程池被占满,那需要看两个层面。JVM 层面(拿 Java 举例)看线程池配置是否合理,Tomcat 默认线程池是 200,每个线程都在处理请求,如果某个请求卡住了(比如调第三方没设超时),200 个线程很快就会被占满,后面的请求全排队,排队时间超过网关超时就报 504。这时候光调大线程池可能适得其反,因为更多线程意味着更大的并发压力,数据库和下游系统压力也会更大,最终整台机器资源耗尽。
实际上,线程池被占满往往是因为有个别请求“卡死”了。排查这种问题要找“卡住的线程在干什么”,用 jstack 抓线程快照:
bash复制jstack <pid> > thread_dump.txt
grep -A 20 "http-nio-8080-exec-" thread_dump.txt | grep -B 2 -A 10 "java.lang.Thread.State"
重点看 WAITING 或者 TIMED_WAITING 状态的线程,如果发现大批线程都在等同一个锁、同一个数据库连接、或者同一个第三方接口响应,那根因就找到了——通常是某个共享资源的获取没设超时。
3.3 下游依赖:数据库和第三方接口的响应瓶颈
数据库是最常见的“拖后腿”角色。慢查询日志能告诉我们哪些 SQL 执行时间过长:
sql复制-- MySQL开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 超过1秒的SQL记入慢查询日志
-- 然后去日志里看具体是哪条SQL
找到慢 SQL 后,用 EXPLAIN 看执行计划,确认是不是没有走索引、是不是扫描行数过大、需不需要优化表结构。这些是老生常谈,但 90% 的数据库慢问题就是索引没建对。
数据库连接池也很关键。如果连接池最大连接数设置得太小,高并发时大量请求在“等数据库连接”这一步排队,getConnection 等待时间超过数据库驱动超时阈值就会抛异常,反映到外层的表现就是应用处理时间变长,最终以 504 收场。HikariCP 的配置里有一个 connection-timeout,默认 30 秒,我个人建议改到 3 秒,宁可快速失败也不让请求长时间挂着:
properties复制# HikariCP
spring.datasource.hikari.connection-timeout=3000
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
第三方接口调用也同样重要。我见过太多线上事故都是因为内部服务调用第三方接口没设超时,第三方某次抖动,请求就全部挂在等响应的状态,线程池被耗尽,整个服务不可用,入口网关疯狂报 504。正确的做法是给所有外部调用设置连接超时和读取超时,并配合熔断降级:
java复制// Java httpclient 设置超时
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(3))
.build();
HttpRequest request = HttpRequest.newBuilder(uri)
.timeout(Duration.ofSeconds(5)) // 读取超时
.GET()
.build();
Python 请求也是同样的道理,requests 库必须显式传 timeout,否则它会无限等下去:
python复制import requests
# 第二个值是读取超时,必须显式设置
resp = requests.get("https://api.thirdparty.com/auth", timeout=(3, 5))
道理都是相通的:每个向外部的调用都要有明确的超时上限,绝不能让调用方无限等待。
4. 实战复盘:登录接口报 login auth failed errorcode 504 的处理过程
4.1 现象描述与第一轮排查
回到开头那个线上告警。客户端登录时返回 login auth failed, errormsg: response is fail. errorcode: 504,这个报错信息本身是业务侧对网关错误码的透传,也就是说客户端请求打到了网关,网关返回了 504,业务网关把错误码包装成 JSON 透传给客户端。
第一件事是看 Nginx 错误日志,结果非常明确:
code复制2025/01/18 14:32:17 [error] 22345#0: *98765 upstream timed out (110: Connection timed out) while reading response header from upstream, client: x.x.x.x, request: "POST /api/v1/login HTTP/1.1", upstream: "http://10.0.8.15:8080", host: "api.example.com"
upstream timed out while reading response header from upstream,upstream 是 10.0.8.15:8080——这是认证服务的地址。也就是说 Nginx 已经把请求转发给了认证服务,但认证服务迟迟没有返回响应头。同时,查看 Nginx access log,upstream_response_time 平均在 5 秒左右,而 Nginx 的 proxy_read_timeout 配置的是 5 秒,刚好卡在临界点,部分请求超过 5 秒就触发了 504。
4.2 根因定位:线程池被第三方调用拖死
第二层去认证服务上看日志。认证服务的访问日志显示请求确实进来了,但处理时间分布非常不均匀,快的 30 毫秒,慢的超过 5 秒。再用 jstack 抓线程快照,发现大量线程阻塞在同一个位置:调用第三方统一登录平台校验 token 时,在等待 HTTP 响应。
顺着代码往下查,发现认证服务调用统一登录平台的 HTTP 客户端没有设置读取超时,用的是默认值。正常情况下这个第三方接口 200 毫秒就能返回,但当时正好赶上第三方平台一个机房网络抖动,部分请求的响应时间突然暴增到 4 到 5 秒甚至更久。由于没设超时,这些请求就一直占着线程池的线程不放,新的登录请求进来后没有线程可用,只能排队。排队的请求迟迟得不到处理,Nginx 等不到响应头就报了 504。
第三层验证:直接 curl 测试第三方登录平台接口,果然响应时间忽快忽慢,确认是第三方服务异常。这个根因链条非常典型:第三方慢 → 未设超时就一直占线程 → 线程池耗尽 → 后续请求排队 → 网关等不及 → 504。
4.3 方案落地:超时、熔断、缓存三管齐下
问题定位清楚了,修复方案分了三步走。
第一步,给所有外部 HTTP 调用显式设置超时——连接超时 3 秒,读取超时 5 秒。这样即使第三方继续慢,单个请求最多 5 秒就会失败,不会无限占用线程。同时设置超额重试,但重试只允许一次,避免雪崩。
第二步,加熔断器。这里用的是 Resilience4j 的配置思路:
yaml复制resilience4j:
circuitbreaker:
instances:
thirdparty-auth:
failureRateThreshold: 50
slowCallRateThreshold: 50
slowCallDurationThreshold: 3000
minimumNumberOfCalls: 10
slidingWindowSize: 20
waitDurationInOpenState: 10s
简单解释这几个参数:在一个滑动窗口(20 次调用)内,如果失败率超过 50%,或者慢调用(超过 3 秒)占比超过 50%,熔断器打开,接下来 10 秒内所有请求直接快速失败,不再真正调用第三方。这样既保护了线程池,也给第三方一个恢复的喘息时间。
第三步,加验证结果缓存。登录 token 校验本身是幂等操作(验证一个 token 是否有效),完全可以缓存一段时间。用一个本地 Caffeine 缓存来缓存 token 的校验结果,TTL 设置在第三方正常响应时间的 10 倍以上(比如 2 分钟)。这样即使第三方抖动,大部分校验请求也能从缓存命中,不会打到第三方。同时要考虑安全问题,token 注销时需要绕过缓存,这里通过校验结果里带一个 version 字段来控制缓存失效。
网关层的超时时间也做了微调,从 5 秒调到 10 秒。注意,这不是盲目调大,而是在应用层已经把根因修复之后,给正常业务逻辑留出合理的余量。如果先调大网关超时再改应用,那期间用户会一直转圈等待,体验更差。
上线之后观察了一整天,认证服务的线程池占用率从接近 100% 回落到稳定的 20% 左右,日志里没有再出现 login auth failed 的 504 报错。第三方平台后续又抖了两次,但因为有熔断和缓存,入口完全无感知。
4.4 这次故障的启发
复盘这件事,最大的教训不是第三方不稳,而是我们的代码对第三方的不稳定没有任何防御。504 只是最终暴露出来的结果,真正的病灶在应用层的超时和熔断策略缺失。给所有外部依赖设置超时、加熔断,应该是写入团队代码规范的红线,而不是出一次事故才补一次。
另外,排查过程暴露了一个监控盲区:我们原来只监控了 Nginx 的 5xx 数量,没有监控 upstream_response_time 的分位数。这次故障从第三方开始抖动到用户大面积报错,中间有二十分钟的窗口,如果当时有 P99 响应时间的告警,完全可以在用户感知之前介入。现在已经补上了这个监控:每台 Nginx 采集 $upstream_response_time 的 P50、P90、P99,有任何一端上涨超过阈值就触发告警。
5. 常见问题速查与避坑清单
5.1 常见问题速查表
| 现象 | 可能原因 | 排查建议 |
|---|---|---|
| 所有请求都 504 | 上游服务挂了、或进程假死 | 检查端口监听、进程状态、连接数 |
部分请求 504,且 upstream_response_time 忽高忽低 |
上游线程池/连接池耗尽 | 看线程快照找阻塞点,检查连接池配置 |
| 特定接口稳定 504 | 该接口业务逻辑慢、慢SQL | 优化 SQL、加缓存、考虑异步化 |
| 通过网关 504,curl 直连正常 | 网关与上游网络问题、防火墙/安全组 | 检查网络连通性、安全组规则 |
| 504 出现在晚上高峰期 | 并发超载 | 扩展节点、限流、优化性能 |
upstream timed out while reading response header |
Nginx 已连上上游但迟迟拿不到响应头 | 重点查应用线程池、慢第三方、慢数据库 |
upstream timed out while connecting |
Nginx 连不上上游 | 查上游服务是否启动、端口是否正确 |
upstream timed out while sending request |
Nginx 向上游发送数据超时 | 常见于上传大文件,检查 proxy_send_timeout 和客户端上传速度 |
5.2 几个容易踩的坑
先说超时时间设置。有人可能会觉得“既然 504 是超时导致的,那把超时设成 600 秒不就永不超时了”。这是个很危险的思路。超时本质上是一种“失败快速化”机制,它的意义在于让调用方尽快知道“这次调用大概率失败了”,从而可以走降级、重试或者提示用户。如果把超时设得无限大,用户只能一直转圈等待,体验和结果都比直接报错差得多。而且长超时会让大量请求挂在资源上,线程池、连接池、内存都会被慢慢拖垮,最终引发更严重的雪崩。
第二个坑是只调网关不调应用。这属于治标不治本,相当于消防员只把火场外围的警戒线拉大了,火本身还在烧。改完网关超时后过几天可能又会出问题,只是问题变成了“请求在应用内部堆积”而已。正确姿势永远是先定位根因,再决定要不要调网关。
第三个坑是重试机制滥用。很多团队会在网关层加上 proxy_next_upstream 或者在代码层做重试,但重试要限制次数,并且要保证接口的幂等性。登录接口还好,如果是一个创建订单的接口,一次请求失败后重试两次,恰好第一次其实已经成功了(只是响应超时),那就会重复下单。所以非幂等接口要么不重试,要么用全局唯一请求 ID 做幂等控制。
第四个容易被忽略的是 keepalive 配置。Nginx 和 upstream 之间如果每次请求都重新建 TCP 连接,在高并发下会有大量 TIME_WAIT 连接,连接建立本身也会增加响应时间。配置 keepalive 能复用连接,减少握手开销:
nginx复制upstream auth_service {
server 10.0.8.15:8080;
keepalive 32; # 默认建议在16-64之间
}
location /api/ {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://auth_service;
}
这个配置很多时候能在高并发场景下降低十几个百分点的响应耗时,对缓解 504 有直接帮助。
最后一个提醒是健康检查。如果你有多个上游节点,一定要配置主动健康检查,而不是只依赖 Nginx 的被动检查(失败几次后才摘除节点)。主动健康检查能周期性地探测节点健康状态,有问题的节点能提前摘掉,用户请求就不会被转发到已经濒临崩溃的节点上。云上一般用负载均衡器的健康检查;自建 Nginx 可以用 nginx_upstream_check_module(社区版需要单独编译)或者直接用阿里云、腾讯云这类商业负载均衡来兜底。
说实话,504 这个错误本身并不难解决,难的是“找到那个没接住球的人”。多数情况下它不会自己好,也不会因为你把超时调大就消失,它只是暂时被掩盖了。我个人处理这种问题的习惯是:先看日志确认上游是谁,再确认上游是不是真的慢、慢在哪个环节,最后才是改配置。把每一步都落实到数据和日志上,而不是凭感觉猜,排查效率会高很多。
最后再分享一个实用的小技巧:在生产环境保留最近一段时间(至少一周)的完整访问日志,并且把 $upstream_response_time 和 $request_time 这两个字段都记录下来。遇到 504 的时候,按时间点去查这两个字段,能很快看出是网络层慢、网关慢还是上游慢,省去很多重复排查的时间。
