某业务线反馈,凌晨流量高峰期 Nginx 反向代理频繁返回 502 Bad Gateway,重启 Nginx 后短暂恢复,但半小时内问题再次出现。后端 Java 服务监控显示 CPU、内存、连接数均正常,错误日志也没有抛出任何异常。更奇怪的是,Nginx 的 error.log 中只有零星 upstream timed out (110: Connection timed out) 记录,似乎暗示后端服务无响应。
但这个“无响应”是伪像——通过 telnet 直接测试后端端口,每次都能立即建立 TCP 连接并得到响应。那么,问题一定出在 Nginx 与 upstream 之间的连接管理上。经过几轮排查,最终定位到 keepalive_timeout 与 upstream 长连接池配置不当导致的 连接泄露。
现象还原:502 的假象
配置前的检查
先看配置。Nginx 作为反向代理,upstream 块里启用了 keepalive 长连接池:
upstream backend {
server 10.0.0.1:8080;
keepalive 32;
}
server {
listen 80;
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
这里 keepalive 32 指定了每个 worker 进程与 upstream 服务器之间最多保持 32 个空闲长连接。Nginx 使用的是 事件驱动模型,每个 worker 进程独立维护自己的连接池。当请求到达时,会从连接池中取出一个空闲连接发给后端;如果池中没有空闲连接,则新建 TCP 连接。请求结束后,连接会被放回池中等待复用。
问题就藏在这个“取与还”的逻辑里。当后端处理请求速度很快时,连接复用率高,池中的连接数会动态变化。但如果 Nginx 侧的 keepalive_timeout 设置不当,就会导致连接无法及时归还,甚至永久丢失。
补充参考:此处可内链到“Nginx故障排查实例”。
想继续深入:此处可内链到“Nginx优化清单”文章。
排查过程:从表象到根因
第一步:缩小范围,确认是连接问题
在故障高峰期抓包。用 tcpdump 在 Nginx 服务器上监听与后端的通信:
tcpdump -i eth0 host 10.0.0.1 and port 8080 -nn -c 10000 -w dump.pcap
分析抓包文件发现,Nginx 发起的连接越来越多,但 主动关闭的连接极少。许多连接在建立后长时间处于 ESTABLISHED 状态,既不发送数据,也不被回收。而 Nginx 的 error.log 中频繁出现 no live upstreams 或 upstream timed out 错误,但后端明明还在正常响应。
这意味着 Nginx 认为自己没有可用的 upstream 连接了,实际却是因为连接池被“无用”的连接占满,新请求无法获取空闲连接。
第二步:检查 keepalive_timeout 配置
Nginx 有两个 keepalive_timeout:
- http 块中的 keepalive_timeout:控制客户端(浏览器)与 Nginx 之间的长连接超时。
- server 块或 location 块中的 keepalive_timeout:一般不常用。
但这里核心问题出在 upstream 侧的 keepalive_timeout,它其实由 Nginx 的 proxy_http_version 1.1 和 proxy_set_header Connection "" 配合 upstream 的 keepalive 指令管理。然而 Nginx 自身在代理模式下,并不会对 upstream 侧的空闲连接设置超时时间——这一点容易被忽略。
查看当前配置:
keepalive_timeout 65;
这个 65 秒是给客户端用的,对 upstream 的长连接池无效。实际上,upstream 空闲连接的默认行为是 永不过期,直到被 worker 进程主动回收。但回收条件是什么?
第三步:分析连接泄露的触发条件
查阅 Nginx 源码和官方文档,upstream 的 keepalive 连接池在以下情况会释放连接:
- worker 进程主动调用
ngx_http_upstream_free_keepalive_peer且池中连接数超过keepalive设定值时,会关闭最不活跃的连接。 - 连接上报错(例如超时、后端主动断开、TCP RST)。
- worker 进程退出。
但在高并发场景下,如果后端响应极快,请求结束后连接立即被放回池中,池子很快达到 32。此时新请求进来,Nginx 会从池头部取一个连接(LRU 策略?实际是LIFO),看似没问题。然而,一旦出现某些 非正常情况,比如后端偶尔返回一个 Connection: close 响应,或者 Nginx 与后端之间发生 TCP 半关闭,连接就会“卡”在池中无法复用。
我们的检测发现:后端服务在某些场景下会发送 FIN 包主动关闭连接,但 Nginx 侧没有正确处理这个关闭事件,导致该连接虽然 FIN_WAIT2 或 CLOSE_WAIT,但仍然被保留在连接池中,占用一个槽位。随着时间推移,池中越来越多的连接处于半死状态,最终耗尽 32 个槽位。新请求无法获取空闲连接,只能新建 TCP 连接,但新建连接的速度远低于请求到达速度,导致连接队列积压,最终超时返回 502。
第四步:用 stub_status 确认连接数
Nginx 的 stub_status 模块可以查看当前活跃连接数。启用后:
location /nginx_status {
stub_status on;
allow 127.0.0.1;
deny all;
}
在故障时查看:
Active connections: 256
server accepts handled requests
3867290 3867290 9742152
Reading: 0 Writing: 48 Waiting: 208
注意 Waiting 值特别高(208),说明大量连接处于空闲等待状态。结合 ss -ant | grep 10.0.0.1:8080 | wc -l 输出发现,与后端的连接数远大于预期的 32*worker_processes。以 4 个 worker 进程为例,理论最大空闲连接数为 4 * 32 = 128,但实际 ESTABLISHED 连接数超过 500。显然,连接泄露了。
相关阅读:此处可内链到“Nginx常见问题”专题。
关联教程:此处可内链到“Nginx部署与验证”内容。
根因:keepalive_timeout 缺失与连接池回收机制缺陷
故障定位思路
问题的本质是:upstream 长连接池中的连接缺少超时回收机制。Nginx 默认只根据 keepalive 指令的数量上限来淘汰连接,但没有基于时间的空闲超时。一旦连接因为后端网络抖动或协议解析异常而进入“僵尸”状态,它们就会永远占用池子,直到该连接被显式关闭。而 keepalive_timeout 这个指令在 upstream 侧根本没有生效——它只控制客户端到 Nginx 的长连接。
更深层的原因在于:Nginx 的 upstream 模块设计时假设后端是可靠的,且连接复用率极高,不需要额外的超时释放。但实际生产环境中,后端网关、防火墙、负载均衡设备可能会主动关闭空闲连接,而 Nginx 收不到正常关闭信号(例如 RST 被防火墙丢弃),导致连接被“悬空”。
进阶阅读:此处可内链到“Nginx性能优化”指南。
Nginx:解决方案:双管齐下
方案一:显式设置 upstream 空闲超时
从 Nginx 1.11.11 版本开始,upstream 块增加了 keepalive_timeout 指令,专门用于控制 upstream 侧空闲连接的超时时间。官方文档描述为:
Sets a timeout during which an idle keepalive connection to an upstream server will stay open. The default is 60s.
注意:这个 keepalive_timeout 是在 upstream 块内使用,不是 http 块!配置示例:
upstream backend {
server 10.0.0.1:8080;
keepalive 32;
keepalive_timeout 30s;
}
设定 30 秒后,如果连接池中的某个连接空闲超过 30 秒,Nginx 会主动关闭它,释放槽位给后续复用。这从根本上避免了僵尸连接堆积。
方案二:配合 keepalive_requests 限制复用次数
有时连接泄露是因为一个连接被复用了太多次,导致某些积累问题。可以设置 keepalive_requests 限制每个长连接最多处理的请求数:
upstream backend {
server 10.0.0.1:8080;
keepalive 32;
keepalive_timeout 30s;
keepalive_requests 1000;
}
处理完 1000 个请求后,该连接会被优雅回收,避免单一连接长期占用。
方案三:检测后端是否支持长连接
如果后端服务器(如某些版本的 Tomcat、PHP-FPM)默认返回 Connection: close,那么 Nginx 的长连接池根本用不上。需要检查后端响应头,确认包含 Connection: keep-alive。如果需要,可以给后端 Nginx 或应用服务器配置长连接支持。
验证与回滚
我的处理经验
配置修改后,执行 reload:
nginx -t && systemctl reload nginx
观察一段时间:
- stub_status 中 Waiting 数量应明显下降,保持在一个合理范围(通常等于 worker 进程数与 keepalive 数乘积的 1~2 倍)。
- ss 统计的后端连接数应稳定在预期值附近,不再持续增长。
- 502 错误完全消失。
如果改动后出现大量 upstream prematurely closed connection 错误,说明 keepalive_timeout 设置过短,后端还没来得及响应就被关闭了。这时应适当增大超时时间(比如 60s 或 120s),或者检查后端处理时间是否过长。
回滚方案:只需删除 keepalive_timeout 和 keepalive_requests 配置,重新 reload 即可恢复原状。但建议保留更精细的监控,在下次流量高峰前找到最合适的参数值。
总结:不要忽略 upstream 侧的连接管理与Nginx
容易忽略的细节
Nginx 配置中 keepalive_timeout 最常见的使用场景是控制客户端连接 空闲超时,但很多运维人员不知道它对 upstream 端的长连接池几乎没有影响(旧版本完全无效,新版本需要显式在 upstream 块中设置)。本次排查的核心收获是:
- upstream 连接池的“泄露”往往不是代码 bug,而是配置不完善导致的资源管理失效。
- 引入
keepalive_timeout和keepalive_requests后,连接池从“静态上限”变为了“动态阈值”,更适应真实网络环境。 - 监控连接数、结合 stub_status 和抓包,是排查此类问题的标准范式。
最后,建议所有使用 Nginx 反向代理且启用 upstream keepalive 的团队,检查自己的配置文件中是否包含 keepalive_timeout 指令。如果没有,请根据后端响应时间和业务并发量,设置一个合理的值(通常 30s~120s)。一个小参数的缺失,可能正是你半夜被 502 报警折磨的元凶。
延伸阅读
