Java

网关 504、应用超时、客户端超时,到底谁先超时?

真正把人带偏的,往往不是 504 本身,而是每一层都在用自己的秒表报错。把客户端、网关、应用到下游的超时预算摆成一条时间线,才能看清是谁先耗尽等待,谁只是最后把故障喊出来。

  • 网关
  • 504
  • 接口超时
  • 性能排查
  • Java
16 分钟阅读

19:47,一条投诉进来:用户说客户端 2 秒就报超时。几乎同时,网关面板上 504 抬头,应用里也开始出现下游 Read timed out

群里很快开始争:

  • 客户端都先超时了,肯定是前端或网络问题。
  • 网关 504 最多,先查网关。
  • 应用日志里已经有 timeout,根因肯定在应用。

那次真正有用的不是继续争“谁先报错”,而是把同一个 requestId 在三层日志里串起来:

时间客户端网关应用下游
19:47:12.001发起请求收到请求收到请求-
19:47:12.034-转发完成开始查订单、再调库存-
19:47:12.841-仍在等待第一次库存调用 read timeout(800ms)库存 RT 抬高
19:47:12.845-仍在等待触发重试,再调一次库存-
19:47:13.646-仍在等待第二次库存调用再次超时库存仍慢
19:47:13.801客户端超时返回网关仍未放弃应用还在做收尾-
19:47:13.830-网关返回 504应用 finally 结束-

这张时间线一出来,很多争论就没必要了。客户端、网关、应用看到的是同一条请求,但它们拿的是不同预算:

  • 客户端愿意等 1.8 秒。
  • 网关愿意等 1.82 秒到 2 秒。
  • 应用内部一个下游调用 800ms,还叠了一次重试。

所以“谁先报错”只是表象;真正该问的是:哪一层先把自己的等待预算花光了。

先画预算,再谈根因

很多超时现场之所以越查越乱,是因为大家默认三层在用同一把秒表。

实际往往不是。

我更愿意先拉出下面这张预算表:

层级超时配置这层实际在等谁
客户端1800ms等网关完整返回
网关1900ms等应用返回
应用入口2000ms等内部业务处理完成
应用到库存800ms,重试 1 次等库存服务

只要这张表一摆出来,很多现场就能立刻看出危险形态。

比如上面那次:应用内部一个下游调用最多就可能消耗 1600ms 左右,再加上本地处理和重试间隔,已经非常接近客户端和网关预算了。此时只要库存 RT 稍微再抬一点,客户端超时、网关 504、应用 timeout 就会像连锁反应一样一起出现。

所以别再问“504 是不是比应用 timeout 更接近根因”,要问的是:是谁先让整条链没有预算余量了。

网关 504 很响,但常常只是中继站先吹哨

504 之所以容易把人带偏,是因为它最显眼,且看起来像“网关出错”。

可在很多现场里,网关只是第一个站出来说:“我替你等到这里为止了。”

更像“504 只是外层表现”的信号有这些:

  • 同一时间应用 access log 里能看到大量长请求,而不是请求根本没到应用。
  • 应用内部线程池、下游 RT、连接池等待已经在 504 之前抬头。
  • 降低某个下游等待或关掉一次重试后,504 很快跟着掉下去。

相反,如果你看到的是:

  • 应用几乎没收到请求。
  • 网关自身 upstream connect、DNS、TLS 就已经大量失败。
  • 网关到应用之间的网络错误先抬头。

那才更像 504 真要回头查网关或中间网络层。

四种常见时间线,长得很像,处理顺序却不一样

1. 客户端先超时,网关和应用都还算安静

更像:

  • 客户端预算太短。
  • 移动端 / 前端超时阈值明显小于网关和应用。
  • 用户已经先放弃,但服务端还在继续处理。

这时要先核对客户端预算和业务可接受时延,而不是立刻说网关或应用坏了。

2. 网关 504 很多,应用 access log 里也有一批长请求

更像:

  • 请求已经进入应用。
  • 应用内部某段等待先把预算吃掉。
  • 网关只是比应用更早放弃。

这类最值得补的是应用内等待证据:线程池、连接池、下游 RT、锁等待。

3. 应用里先出现统一阈值附近的 timeout,随后网关 504 抬头

更像:

  • 下游或应用内部超时先发生。
  • 网关 504 是外层被动暴露。
  • 如果还叠了重试,网关的报错会越来越多。

这种现场尤其不能只盯网关面板。真正的起点往往已经在应用内部了。

4. 网关 504 和客户端超时都有,但应用几乎没收到请求

更像:

  • 网关到应用之间建连有问题。
  • 服务发现、DNS、TLS、目标实例状态异常。
  • 请求在到应用前就把预算耗掉了。

这时再去翻应用线程池和连接池,意义通常不大。

我在现场最想先拿到的,不是更多报错,而是这三份东西

1. 一张完整的超时预算表

没有这张表,所有“谁先超时”的讨论都是空的。

2. 一条请求的跨层时间线

最好是同一个 requestId 或 trace id,至少把客户端、网关、应用、下游四个点串起来。

3. 超时发生当时的等待证据

包括但不限于:

  • 业务线程池 active / queue
  • 连接池 active / pending
  • 下游 RT 和错误分布
  • 锁等待、事务时长、GC 停顿

这三样加在一起,才能把“谁先报错”变成“谁先花光预算”。

一个更像现场的判断方式:看预算耗在哪一段

还是上面的例子,为什么最后大家看到的是 504?因为预算是这么被吃掉的:

段落预算消耗
网关转发 + 应用入口处理30ms 左右
第一次库存调用等待800ms
重试切换与本地处理40ms
第二次库存调用等待800ms
收尾与响应写回100ms 左右

加起来已经逼近 1.8 秒到 1.9 秒。此时客户端和网关谁先叫,只差几十毫秒,根本不值得单独争。真正有价值的结论是:应用内部给下游的等待和重试,把整条链的预算余量吃没了。

别急着一起调大 timeout,先做验证动作

很多团队一看到 504、应用 timeout、客户端超时同时出现,就会把所有 timeout 一起调大。这通常只会让等待链拖得更长。

我更愿意先做两种验证:

验证 1:去掉一段最可疑的预算消耗

比如关闭一次重试、临时降低某个下游调用频率、摘掉一段非核心串行调用。看的是:客户端超时、网关 504、应用 timeout 是否按顺序一起回落。

验证 2:对齐动作后的回落顺序

如果你切掉的是前段等待,常见回落顺序是:

  1. 应用内部 timeout 先掉。
  2. 长请求数减少。
  3. 网关 504 回落。
  4. 客户端报错跟着下降。

这个顺序比“有没有变好”更能说明问题。

如果你现在要往别处接

超时链里最会误导人的,一直都不是报错文案本身,而是谁拿着自己的秒表先喊了一句“我不等了”。

把预算表和时间线画出来之后,504、客户端超时、应用 timeout 往往不再是三个问题,而只是同一条等待链在不同位置上的三次表态。