1. 从一次“抓狂”的排查开始说起
如果你接手过一个经常报Gateway Timeout 504的服务,你大概能体会那种感觉:用户说“网站打不开了”,你按 F5 刷新了一下,页面转了几圈,然后浏览器里跳出刺眼的一行字504 Gateway Timeout。那一刻你会下意识地先骂一句网关,然后开始想:到底是后端挂了?还是代理转发超时了?还是负载均衡被拖垮了?
入行越久越会发现,网关超时并不是一个明确的单一问题,而是一种“症状”。它就像一个人发烧,病因可能是一堆乱七八糟的事:病毒感冒、急性阑尾炎、甚至只是被子盖多了。如果你一看到 504 就只会重启服务或者拉长超时时间,那多半治标不治本,过一阵它还会卷土重来。
这篇文章,我打算把网关超时从表象到本质拆开揉碎,结合我实际运维和排障过程中踩过的坑,给出一套既能在 10 分钟内应急止血、又能从架构层面彻底解决问题的思路。不管是还在学 Nginx 配置的小白,还是已经在维护微服务集群的老手,应该都能从中找到自己能直接用的方案。
2. 500、502、503、504,别再傻傻分不清
先把基础打牢。很多人把 502 和 504 混为一谈,出问题的时候连报障都说不清楚,这对排查方向影响很大。它们虽然都跟网关层有关,但含义完全不同。
2.1 状态码背后的“分工逻辑”
要理解这几个状态码,得先搞清楚一次请求在典型架构里经过了哪些关卡。以最常见的浏览器到 Nginx 再到应用服务器为例:
浏览器 -> 反向代理(Nginx) -> 应用服务器(Tomcat / PHP-FPM) -> 数据库 / 外部API- 浏览器把请求发给 Nginx。
- Nginx 根据配置把请求转发给上游的应用服务器。
- 应用服务器处理业务,可能还要访问数据库。
- 应用服务器把结果返回给 Nginx。
- Nginx 再把结果返回给浏览器。
在这个过程里,Nginx 同时承担了反向代理的角色,也就是“网关”。当某个环节出了问题,网关就会根据情况返回不同的状态码,相当于它站在中间替你向浏览器说“我不舒服”。
2.2 状态码对比速查表
| 状态码 | 含义 | 本质问题 | 一句话类比 |
|---|---|---|---|
| 500 | 服务器内部错误 | 应用代码抛异常,但没被捕获 | 后厨炒菜炒糊了 |
| 502 | 网关收到上游无效响应 | 上游进程崩了、端口没监听、或响应格式不对 | 后厨空无一人,没人做菜 |
| 503 | 服务不可用 | 服务器过载或正在维护,主动拒绝服务 | 餐厅门口挂了“暂停营业” |
| 504 | 网关超时 | 网关发请求后,上游在限定时间内没返回内容 | 菜做了很久还没端上来,实在等不了 |
| 499 | 客户端关闭连接(Nginx特有) | 客户端等不及主动断开,Nginx记录到日志 | 顾客等烦了,直接走了 |
所以你看,504 的关键词是“时间”。它不是完全没响应,而是响应来得太慢,慢到超出了网关耐心等待的时间上限。而这个时间上限,往往就是 Nginx 里的proxy_read_timeout、proxy_connect_timeout这类参数决定的。
2.3 为什么“网关”自己不直接报错?
有一种常见误解是“网关出问题了,所以返回 504”。其实大多数时候,网关本身非常健康,CPU 占用很低,内存也没爆,它只是实在等不到上游的响应,才只好给浏览器发一个 504。
这就好比你是前台接待,顾客问你“我们订的包间准备好了吗?”你打电话问后厨,结果后厨一直没接电话,你干等了 60 秒,最后只能跟顾客说“不好意思,我们这边还没准备好。”真正出问题的,其实是在“后厨”。
所以请记住这句话:处理 504 的目标,不是让网关闭嘴不报错,而是让后厨别再掉链子。
3. 实战排查:收到 504 后的第一波操作
遇到 504,别慌,也别急着改配置。按照下面的顺序一步步来,90% 的问题能在半小时内定位到大致方向。
3.1 第一步:区分是“偶发”还是“持续”
- 如果只是某个页面偶尔 504,刷一下又好了,大概率是瞬间并发飙高,后端某个接口偶发变慢。
- 如果是所有请求都 504,且持续不断,那要考虑后端进程是否已经完全无响应,或者网络链路是否被切断。
- 如果特指某个接口 504,而其他接口正常,问题就锁定在这个接口背后的逻辑里:是不是在查一个慢 SQL?是不是调了一个很慢的外部服务?
这一步看起来简单,但很多人省了它,直接去查 Nginx 日志,结果在茫茫日志里浪费几个小时。
3.2 第二步:看日志,找证据
日志是你和服务器之间沟通的唯一语言,用法如下。
| 日志位置 | 内容 | 关键字段 |
|---|---|---|
Nginx 错误日志 (error.log) | 上游连接失败、超时记录 | upstream timed out、connect() failed |
Nginx 访问日志 (access.log) | 每个请求的耗时、状态码、上游响应时间 | request_time、upstream_response_time、status=504 |
| 应用日志(如 Tomcat / Spring Boot) | 接口执行耗时、异常堆栈 | SLOW SQL、ConnectTimeoutException、OutOfMemoryError |
系统日志 (dmesg//var/log/messages) | 内核是否杀进程、磁盘是否报错 | oom-killer、I/O error |
重点提一下 Nginx 访问日志里的两个时间字段,这俩是诊所里的体温计:
request_time:从 Nginx 收到请求到把响应返回给客户端的总时间。upstream_response_time:Nginx 与上游服务器通信的耗时。
如果request_time很大而upstream_response_time很小,说明时间花在“客户端和 Nginx 之间”;如果upstream_response_time本身就很大,那问题就在“应用服务器处理请求”这一环。
很多发行版默认没有在日志格式里加上upstream_response_time,建议你提前把它加进去。在http块里定义一个日志格式:
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for" ' 'rt=$request_time urt=$upstream_response_time'; access_log /var/log/nginx/access.log main;这样配置之后,以后排障的效率会高很多。
3.3 第三步:用命令探一探后端的“体温”
如果日志显示超时发生在 Nginx 与上游之间,那就要马上确认上游还能不能扛住。
# 看端口监听状态 netstat -ntlp | grep 8080 ss -tln | grep 8080 # 看进程状态 ps aux | grep java top -Hp <PID> # 实时看线程状态 # 模拟请求,测试响应时间 curl -v -m 5 http://127.0.0.1:8080/health 2>&1 # 看连接数是否被打满 netstat -an | grep 8080 | wc -l曾经我排查过一个 504 案例,top一看,CPU 只有 3%,内存也正常,但curl就是卡住不返回。最后发现是数据库连接池被耗尽,所有线程都在等数据库连接。这种问题光看网关日志是看不出来的,必须在应用这一侧才能看到真相。
3.4 第四步:静态页面 vs 动态接口,分流判断
可以在 Nginx 里配置一个不经过后端的静态路由来测试。比如放一个test.html,如果直接访问这个文件非常快,而访问动态接口 504,那至少说明网络链路和 Nginx 本身是健康的,问题在后端服务。
同理,也可以在后端服务器本机curl一下它的接口。如果本机访问很慢,那就是后端应用自身的问题,和 Nginx 毫无关系;如果本机很快,但从 Nginx 转发就很慢,那就要检查两台机器之间的网络是否有延迟、丢包,或者是 Nginx worker 与上游之间的 keepalive 连接是不是快耗尽。
4. 定位到问题的这几种“临床分型”
排障经验多了,你会发现在 504 这个诊断结果下面,通常藏着几种固定的病理。我总结成几大类,方便你对照。
4.1 类型A:后端口算慢查询 / 同步调外部接口
这是最常见的一种:某个接口内部逻辑很重,比如在一个循环里查 N 次数据库,或者直接同步调用第三方 API,而第三方 API 响应要 10 秒。Nginx 默认的proxy_read_timeout是 60 秒,按理说 10 秒不至于超时。
但问题在于,如果数据库连接池只有 50 个连接,而这个接口的并发量是 100,那后 50 个请求就得排队等连接。排队时间加上第三方接口的慢响应,很容易把时间拖到超时线以上。
症状特征:
- 偶发出现,高并发时间段更容易出现。
- 应用日志里有大量“连接获取等待超时”之类的错误。
- 数据库慢查询日志里有执行时间特别长的 SQL。
4.2 类型B:后端进程假死或线程阻塞
进程还活着,但已经不干活了。这种情况最吓人,因为从监控上看 CPU 不高、内存不高、端口也在监听,但它就是不返回任何东西。
典型的场景是:
- 某个接口死锁,所有线程互相等着对方释放锁。
- 磁盘 I/O 卡死,日志都写不进去。
- 后端服务被某个大任务占用了所有 worker 进程(比如 PHP-FPM 的
pm.max_children设置太小)。
症状特征:
- 持续 504,重启后能短暂撑一会儿,之后又慢慢卡死。
- 应用日志停在某个时间点,不再更新。
- 各种请求都超时,不只是特定接口。
4.3 类型C:网关配置不当或连接池耗尽
后端其实还活着,但是网关这边自己先撑不住了。
场景一:Nginx 的worker_connections太小,连接被瞬间打满,新的请求根本没法转发,直接在网关层就超时。
场景二:Nginx 与上游之间开了 keepalive,但你忘记配置keepalive参数,导致连接反复建连、断连,握手过程消耗了大量时间。
场景三:如果你前面还有一层负载均衡(比如云服务上的 SLB,或者自建的 HAProxy),每一层都有自己的超时时间,某一层的超时配置比别的层小,它就会先掐断。
4.4 类型D:网络层面的黑洞或丢包
这一点最容易被忽略。有时候后端很健康,Nginx 也很健康,但两台机器之间的网络链路有问题。特别是跨机房跨地域部署的时候,偶尔的丢包会导致 TCP 重传,重传过程中时间被拉长,最终触发超时。
用mtr或ping -f可以简单测一下丢包率,但发作比较随机的话,最好在 Nginx 日志里看看upstream_response_time的分布。如果它经常横跳,从几十毫秒突然跳到几十秒,网络抖动嫌疑很大。
5. Nginx 超时配置到底怎么调才不算乱调
很多教程一上来就教你“把 timeout 调到 600 秒”,说实话,这是我最反对的做法。无脑调大超时时间,本质上是在掩盖问题,而不是解决问题。但它确实是一个合理的“应急止血”手段,不能完全否定。
5.1 搞清楚 Nginx 的 6 个超时参数
| 参数 | 默认值 | 作用 | 常见调整场景 |
|---|---|---|---|
client_header_timeout | 60s | 读取客户端请求头超时 | 客户端网络差时不需要调 |
client_body_timeout | 60s | 读取客户端请求体超时 | 上传大文件时留意 |
proxy_connect_timeout | 60s | 与上游建立连接超时 | 上游偶尔启动慢可适当调高 |
proxy_send_timeout | 60s | 把请求发送给上游的超时 | 大请求体转发时留意 |
proxy_read_timeout | 60s | 等待上游返回响应的超时 | 高耗时接口需要重点关注 |
keepalive_timeout | 75s | 与客户端的连接保持时间 | 连接复用场景一般默认就够 |
注意,proxy_send_timeout和proxy_read_timeout的计时逻辑并不是“总时间”,而是两次网络读写操作之间的间隔时间。也就是说,只要在 60 秒内有一次数据传输,连接就不会被掐断。这个细节对理解超时问题非常重要,很多人误以为它是“接口最长时间”,其实不是。
5.2 合理的调整思路
假如你的一个接口确实需要长时间计算,比如生成一份复杂的报表,耗时可能要 90 秒。这时候你确实得调大proxy_read_timeout,但你应该遵守几个原则:
- 只在那个接口的
location块里调,尽量不要全局调大。 - 把超时时间设置成“接口 P99 耗时”的两倍左右。
- 同时把浏览器层面能接收超时的时间也考虑进去,因为即使 Nginx 等到了 90 秒,浏览器那边的
read_timeout如果只有 60 秒,用户还是报错。
我自己常用的折中方案是:给耗时接口开一个专用的代理路径,只在该路径上调大超时时间,其他普通接口保持默认。举个例子:
location /api/slow-report { proxy_pass http://backend_server; proxy_read_timeout 300s; proxy_connect_timeout 10s; proxy_send_timeout 30s; } location /api/ { proxy_pass http://backend_server; proxy_read_timeout 60s; }这样既不影响整体稳定性,又能解决特定业务需求。
5.3 千万别忽略“一层超时”的关系链
如果你在 Nginx 前面还有一层云负载均衡,或者 CDN,那就是一条完整的超时链,每一层的耐心值都不同。
比如:
- 云负载均衡的响应超时是 120 秒。
- Nginx 的
proxy_read_timeout是 300 秒。 - 后端接口真实耗时是 150 秒。
这种情况下,用户拿到 504 其实是云负载均衡报的,跟你的 Nginx 配置没关系。你以为改了 Nginx 就好,结果改了等于白改。所以调整超时之前,最好把我上面这种链路图理清楚,每一层的超时时间都要满足:上层 >= 下层,否则上层一定会成为最先不耐烦斩断连接的那个。
5.4 一个值得尝试的小技巧:逐步放行而不是一次性超时
有一种常见做法,是在 Nginx 里用limit_req或upstream的max_conns参数,限制转发给后端的并发数,让请求排队在 Nginx 这一层,而不是一股脑涌进后端。
upstream backend { server 10.0.0.2:8080 max_conns=200; keepalive 32; }这样做的思路是:让快请求不要排队排在慢请求后面,减少整体阻塞。如果后端确实扛不住并发,与其在应用层雪崩,不如在网关层做有限度的蓄洪。
6. 多场景解决方案:从 PHP 到 Java,逐个击破
不同技术栈的后端,出现 504 时处理方式会有很大差别。下面我来分场景说说。
6.1 PHP-FPM 场景:worker 被打光的经典案例
PHP-FPM 默认的模式是dynamic,几个关键参数:
pm = dynamic pm.max_children = 50 pm.start_servers = 10 pm.min_spare_servers = 5 pm.max_spare_servers = 20 pm.max_requests = 500pm.max_children是你这台机器能同时处理的 PHP 请求数量。如果设置的 50 但实际并发有 80,那只能有 50 个请求被处理,剩下 30 个排队在 FPM 里面等空闲 worker。
症状就是:Nginxerror.log里面会疯狂刷connect() to unix:/tmp/php-cgi.sock failed。
解决方法分两步走。
第一步,估一个合理值。经验公式是:
max_children = 可用内存 / 单个PHP进程平均内存占用比如机器内存 8GB,每个 PHP 进程平均占用 80MB,那么max_children大概可以设在 80 左右,留一部分内存给系统和其他进程。
第二步,给 FPM 加一个超时控制。很多人会忽略request_terminate_timeout,这个参数可以让执行时间过长的请求被强制掐断。比如你设置成 60 秒,超时之后 FPM 会把 worker 杀掉并回收,防止一个卡死的请求无限占用 worker。
request_terminate_timeout = 60但注意这个设置的“手感”:它会给用户返回一个 500 而不是 504,但至少你的服务不会被一个死循环拖垮。对业务而言,及时止损比“永不超时”重要得多。
6.2 Java 服务场景:连接池与线程池的综合治理
Java 后端的 504 往往跟线程池和数据库连接池关系紧密。Spring Boot 里典型的配置:
server: tomcat: threads: max: 200 min-spare: 20 max-connections: 10000 accept-count: 200max=200表示 Tomcat 同时处理请求的线程数。如果请求进入太快,超过 200 就会放到accept-count指定的队列里等待,但如果队列也满,连接就会被拒绝或等待到超时。
另外,对外调用其他服务的超时也要单独配置,比如用 RestTemplate 或 OpenFeign,务必设好连接和读取超时:
feign: client: config: default: connect-timeout: 5000 read-timeout: 5000这里最想提醒的一点是:千万记得给每个外部调用都设置超时,不要让调用无限期地占着 Tomcat 线程。一个没有超时的第三方调用,在对方服务假死的时候,能把你整个线程池拖到全部排队,最终所有请求延迟上升,网关一路超时到用户端。
6.3 高并发短平快接口的场景:连接复用和协议选择
如果你的业务大多是查询接口,每次只要几十毫秒,但并发量巨大,那么链路握手成本会占比很高。
建议:
- 启用 Nginx 到后端的 keepalive。
- 启用浏览器到 Nginx 的 HTTP/2(如果前端支持)。
- 在同一个 location 里尽量保持后端的连接复用,减少 TCP 三次握手。
upstream backend { server 127.0.0.1:8080; keepalive 100; } server { listen 443 ssl http2; location /api/ { proxy_http_version 1.1; proxy_set_header Connection ""; proxy_pass http://backend; } }注意proxy_http_version 1.1和清空Connectionheader 这两行不能漏,不然 keepalive 完全不生效。
6.4 数据库慢查询导致的连锁 504
如果应用逻辑本身没有问题,但数据库响应很慢,那应用线程会卡在数据库等待上,效果等同于接口变慢。
排查手段是开启慢查询日志:
SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1;看到慢查询后,用EXPLAIN分析执行计划,看看是否缺索引、是否扫描行数过多。我之前碰到过一个典型问题:一条 SQL 要查 1000 万行的表,加了索引之后从 15 秒缩到 0.2 秒,整个接口的 504 瞬间消失。
这种根因型修复,性价比远高于任何网关层的超时调参。
7. 一套可落地的监控与预防方案
解决了眼前的问题,不代表以后不会复发。下面这套东西,建议你在项目里逐步加上。
7.1 核心指标:P99 响应时间与 5xx 比例
不要只看接口平均耗时。平均值会被极值带偏,让你对系统真实表现毫无感觉。一定要看 P99、P95 的值。
如果某个接口的 P99 耗时接近甚至超过超时时间阈值,说明它已经在危险边缘反复横跳。这时候不是等它超时才去处理,而是要提前优化或提升超时容忍度。
7.2 健康检查与自动摘除节点
如果你有多台后端节点,可以在 Nginx 配置里加上主动或被动健康检查。
被动健康检查靠 Nginx 自带行为实现:
upstream backend { server 10.0.0.2:8080 max_fails=3 fail_timeout=30s; server 10.0.0.3:8080 max_fails=3 fail_timeout=30s; }如果 30 秒内有 3 次失败,Nginx 会将该节点标记为不可用,后续请求不再转发给它。这能避免一个假死的节点继续拖垮整体服务。
但要注意,这只对 Nginx 这一层有效。如果你的架构是 LVS/云负载均衡 -> Nginx -> 后端,那 Nginx 层挂了,负载均衡也不知道,所以还需要在负载均衡上配置 HTTP 健康检查。
7.3 超时间单调优清单
给你一个我自己的检查清单,发布、上线大改之后,照着跑一遍:
- [ ] 浏览器 → 云负载均衡 → Nginx → 后端,每一层超时是否满足“上层 >= 下层”?
- [ ] 所有对外部服务的调用是否都设置了连接超时和读取超时?
- [ ] 数据库连接池上限是否 >= 应用线程池上限?
- [ ] 高耗时接口是否走独立的代理配置?
- [ ] Nginx keepalive 配置是否完整(版本、header、参数三者配套)?
- [ ] 后端进程是否有 OOM、线程死锁等异常日志?
- [ ] 是否有定时任务和大数据任务与业务高峰期撞车?
7.4 预防胜于救火:容量规划
最后一个朴素但有用的建议:如果你经常在高峰期收到 504,请做一个简单的容量估算。
假设:
- 单节点最大 QPS = 500
- 单接口 P99 延迟 = 300ms
- 业务高峰期实际 QPS = 2000
那么你至少需要 4 到 5 个节点才够撑住高峰期,并且要规划好如果一个节点挂掉,剩余节点仍然能扛住流量。这个估算看起来很简单,但很多团队是等到 504 大爆发之后,才慌忙扩容,早估一步就能少流一身冷汗。
8. 常见问题速查表与一手体验
最后,我把这些年遇到过的典型 504 场景整理成一张速查表,方便你直接对照处理。
| 症状表现 | 可能原因 | 首选排查动作 | 临时方案 | 根治方案 |
|---|---|---|---|---|
| 偶发 504,集中在某功能 | 接口查询慢 / 第三方调用慢 | 查慢查询日志、链路追踪 | 局部调大超时 | SQL 优化 / 异步化 |
| 全站 504,重启后端后短暂恢复 | 线程池耗尽 / PHP worker 卡死 | top看线程、FPM 状态页 | 调大 worker 数量 | 定位死锁/死循环 |
| 特定机器上面的请求全 504 | 单节点假死 | 看该节点资源、日志 | 从 LB 摘除该节点 | 修复该节点的问题 |
| 请求 504,但后端访问正常 | 网络链路问题 / Nginx 配置过小 | mtr看丢包、查 Nginx 日志 | 调大超时(临时) | 专线优化 / 配置调优 |
| 上传大文件时 504 | body 超时 / 后端处理太慢 | 查client_body_timeout | 临时调大 | 改异步上传/分片上传 |
| 504 只在压测时出现 | 连接数打满 / 线程池满 | 查连接数监控 | 加节点 | 容量规划、限流 |
8.1 压测时的 504 和线上真实 504,处理方式完全不同
压测时出 504,往往是因为压测工具产生的并发超过了系统承载上限,这时候可以顺手验证系统的极限,为容量规划提供数据。但线上真实的 504,背后通常有真实用户受害,处理优先级完全不同。
我的建议是,把压测当成一种主动体检,把线上 504 当成紧急事故来处理。“压测时 504 了怎么办”和“线上 504 了怎么办”,答案的第一步都是先定位,但压测可以花 30 分钟慢慢看,线上只能争分夺秒。
8.2 从个人角度说几句掏心窝的话
处理 504 这几年,我最大的体会是:不要迷信“调大超时”这个万能药。它就像止痛药,治标立竿见影,但长期用的话,身体会被拖垮。总有一天,后端会因为某个请求卡在那儿,把连接池占光,然后把整个系统的所有请求都拖进超时深渊。
另外,如果你能在每次修复之后,花 10 分钟记录一下“原因 + 怎么定位 + 怎么解决 + 怎么预防”,久而久之你手里会有一份属于自己的排障手册。下次不管哪台机器报 504,你都能第一时间判断个大概方向,而不是像第一次遇到时那样手忙脚乱地查东查西。
再分享一个小经验:调超时参数一定要记录变更。有一次我在一台服务器上调了proxy_read_timeout到 300 秒,过了两周忘了这回事,后来该接口出问题,我看配置还以为是别人改的,绕了很大弯路才想起是自己之前临时调整留下的“遗迹”。修改配置之前先备份、修改之后做注释、生产环境配上变更记录,这个习惯能救回你很多时间。
9. 后续还可以怎么扩展
504 只能算网关超时问题里的冰山一角。你再往前推一步,会发现还可以做很多事:
- 在代码里给所有外部调用加上耗时打点,形成调用链。
- 在后端接口里做优雅降级,慢调用直接返回缓存结果而不是硬等。
- 把超时策略改成渐进式,第一次等 3 秒拿不到就返回,供用户重试。
- 在网关层加一个统一的重试机制,注意必须限制重试次数,不能把放大流量打进后端。
这些方向做好了,配合上面说的定位手段,你会发现 504 这个现象对你的团队来说,已经不再是一句吓人的报警,而只是一条普通的、可以快速定位的系统日志问题。
如果你手头现在正有一个服务在报 504,别急着复制网上的配置,先按我这条思路走一遍:分清现象类型,去看日志,找到真正超时的环节,再对症下药。或许这一次,你就能把它根治掉。