☰
Nginx 502 故障排查:三行配置导致 keepalive 失效与连接复用问题
2026/10/20 5:21:10 网站建设 项目流程

做运维这些年,遇到过的Nginx 502不算少,但大部分时候靠"看错误日志→查后端服务→重启大法"三板斧就能混过去。直到那次,线上API网关在高峰期频繁502,时好时坏,持续了几个小时,几十个业务方在群里艾特我。我前前后后花了将近3个小时,最后发现罪魁祸首不是我一开始怀疑的后端服务,也不是网络波动,而是Nginx配置里三行不起眼的设置。这篇文章就把完整的排查链路、底层原理和修复方法写出来,给以后遇到同类问题的朋友一个参考。

1. 故障现象:502在下午两点半准时“打卡”

1.1 从业务报警开始的问题描述

那天下午两点半左右,监控平台开始刷屏:某对外API网关的成功率掉到85%以下,接口大面积返回502 Bad Gateway。受影响的不只一两个接口,几乎所有走这个网关的请求都有概率中招。更诡异的是,这种状态并不是持续的——稍微过个几分钟,成功率又自己拉回99%以上,然后再过十几分钟又掉下去。业务方的反馈是"一会好一会坏,没法用"。

我当时的第一反应是后端服务可能在做发布或者重启。毕竟502最常见的含义就是Nginx作为反向代理,无法从上游服务器拿到有效响应。但查了一圈后端集群的进程启动时间,全都很干净,没有人做过发布,也没有进程重启记录。

1.2 为什么我一开始没当回事

坦白讲,刚开始我是有点松懈的。因为这类间歇性502,我此前见过太多次,十有八九是后端某个实例又OOM被系统杀了,或者健康检查的探针路径写错了。我甚至还在群里回了句"我先看下后端状态,应该是某个节点的问题"。

结果这一看就是俩小时。

1.3 规律浮出水面:间歇性,与并发强相关

在反复观察了大概半小时后,我发现了一个不太对劲的规律:502并不是均匀分布的,它总是在流量往上冲的时候出现。比如QPS从800涨到1500的时候,成功率就会突然掉下来;等流量回落,一切又恢复正常。更奇怪的是,从Nginx的错误日志看,每次出问题的时间窗口只有几十秒,不会持续太久。

这个规律很重要,它基本排除了"后端进程挂掉"这种持续性故障,把嫌疑指向了"某个资源在并发升高时被瞬间打满"。但到底是什么资源被打满,我当时还没有概念。按照我惯用的排查顺序,下一步应该上日志了。

2. 前两小时:沿着最常规的路子排查

2.1 第一步:Nginx 错误日志里的关键线索

我习惯先看/var/log/nginx/error.log,因为它是整个排障链路里信息量最大、也最诚实的文件。那次也一样,错误日志里刷满了两类报错:

connect() failed (111: Connection refused) while connecting to upstream connect() failed (110: Connection timed out) while connecting to upstream

这两行字几乎就是502的标准答案——Nginx在建立到上游服务器的TCP连接时,要么被对方直接拒绝(111),要么一直等不到握手成功(110)。所以问题一定出在"Nginx到后端"这一段链路上。

当时我做了两个动作:先看Nginx到后端服务器的网络是否通畅,再确认后端端口是否在监听。分别在Nginx机器上执行了:

telnet 后端IP 8080

以及在后端机器上执行:

ss -lntp | grep 8080

结果很讽刺:Nginx这边telnet能通,后端那边端口也在正常监听。也就是说,在"单独测试"的场景下,链路完全正常。

2.2 第二步:后端服务明明活着,curl 一切正常

为了排除后端应用自身的问题,我直接在后端机器上curl了本地的健康检查接口:

curl -i http://127.0.0.1:8080/health

返回200 OK,响应体是一个再正常不过的JSON。顺手看了下JVM的GC情况、线程数、CPU使用率,也都在合理区间。最迷惑的就是这里:从后端服务自身的视角来看,它完全是健康的。可Nginx这边偏偏连接不上它。

那问题有没有可能在Nginx所在的机器上?我检查了Linux连接跟踪表(conntrack)和文件描述符使用情况,全都没有异常。这个时候我已经在这类常规检查上花了差不多一个小时,进度几乎是零。

2.3 第三步:监控数据自相矛盾的迷惑现场

接下来我打开了后端服务的监控面板,想看它在502发生时段的表现。结果很有意思:在502出现的那个时间窗口里,后端的TCP连接数瞬间冲高,active connections从几百跳到两三千,但紧接着又掉下来。CPU、内存、GC完全平稳,没有任何瓶颈特征。

那一刻我产生了一个猜测:会不会是后端处理请求的速度跟不上,导致大量请求在Nginx侧排队?随后这个猜测也被推翻了——如果真是后端处理慢,错误日志里应该更多出现upstream timed out这类读响应超时,而不是connect() failed。关键是,connect这一步都没成功,说明连TCP连接都没建立起来,压根谈不上"处理慢"。

这个矛盾点让我的排查方向发生了一次重要转变:问题大概率不在后端进程的处理能力上,而在连接建立这个环节。要么是Nginx所在机器的临时端口被耗尽了,要么是后端机器的accept队列被打满了。

2.4 中间还踩了重启Nginx这个“有效但治标”的坑

排查到中途,因为业务方催得紧,我先做了一次Nginx reload。生效之后,故障确实立刻消失了,大概正常了15分钟。但等我回过头继续看日志,报错又出现了。

这个现象当时差点把我带偏——它太像"配置变更后忘记reload导致旧配置生效"的问题了。我还特意确认了配置文件的md5值,确认真实配置和预期一致。后来才明白,重启也好、reload也好,本质上只是把Nginx到上游之间积累的一堆异常连接全部清空了,短暂恢复了连接池,但生成这堆异常连接的根因纹丝不动,所以问题很快又会长回来。也就是从这一刻起,我决定停下来,不再做任何"外科手术式"的操作,老老实实复现问题。

3. 第三小时:压力复现与 netstat 暴露的真相

3.1 用压测脚本复现线上故障

既然线上现象是间歇性的,最好的办法就是造流量把它复现出来。我拉了两台用于压测的机器,用wrk加了一个模拟线上请求分布的脚本,从低并发开始逐级往上升:

wrk -t8 -c300 -d60s --timeout 3s http://网关域名/api/xxx

在300并发、60秒的测试里,成功率从99%慢慢开始出现零星502。当我把并发数提到500后,502的比例开始快速上升,而且表现和线上几乎一模一样:一阵一阵地出现,不会持续。

这里补充一个经验:压测复现的时候,千万不要一上来就发超大流量。那样虽然也会触发502,但会把系统和网络层面的问题全部放大,反而掩盖真正的诱因。从200、300、500这样一个台阶一个台阶往上加,更容易观察到故障的临界点。

3.2 连接数曲线:只涨不跌的 TIME_WAIT

在压测的同时,我在Nginx机器上开启了两个实时观测窗口,一个看Nginx到后端的连接状态统计,一个看系统全局的TCP连接情况:

watch -n 1 'ss -ant | grep 后端IP | awk "{print $1}" | sort | uniq -c' watch -n 1 'ss -s'

结果让我一下子清醒了。正常情况下,Nginx到后端会有几十个ESTABLISHED连接,这就是keepalive连接池已经建立并复用的表现。但这个生产环境里,ESTABLISHED几乎看不到,反而是TIME_WAIT的数量在持续累积,一度突破了四位数。

TIME_WAIT本身不是洪水猛兽,它是TCP四次挥手后主动关闭连接的一方必经的状态,几秒钟后就会被系统回收。但当它疯狂堆积,说明线上正在高频地建立连接、又立刻断开连接。这压根不是"连接池"该有的表现,更像是在搞"每请求一条新连接"的原始模式。

3.3 找到核心嫌疑:upstream 连接根本没有复用

到这一步,连接是否被复用,成了整个排查的核心问题。我直接执行了下面这条命令,统计当前Nginx与后端之间存在的连接总数:

ss -ant | grep -c 后端IP

结果只有个位数。而我们已知这个网关的QPS在高峰时超过1500,一个请求如果对应一条新建连接,那后端在1秒内要处理1500次TCP握手,还要在响应后主动断开连接。这对任何一台机器来说都是巨大的负担,连接一拥堵,Nginx就会开始报111或110,502自然就出来了。

可问题来了:我记得网关的nginx.conf里明明配置了keepalive,为什么实际没有复用?为了确认配置没有被人为改动过,我把线上生效的conf完整拉下来对比:

upstream backend_cluster { server 后端IP1:8080 max_fails=3 fail_timeout=10s; server 后端IP2:8080 max_fails=3 fail_timeout=10s; keepalive 32; }

配置确实有keepalive 32;这一行。那它为什么失效了?答案就在proxy_pass的location块里。

4. 根源分析:keepalive 配置为什么形同虚设

4.1 配置里明明写了 keepalive,却没生效

我贴一下当时location块里跟上游通信相关的配置:

location /api/ { proxy_pass http://backend_cluster; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }

反复看了几遍,终于意识到少了什么。没有proxy_http_version 1.1,也没有proxy_set_header Connection ""。而这两个配置,恰恰是Nginx与上游之间启用连接复用的前置条件。

也就是说,upstream里的keepalive 32;只是告诉Nginx"你可以在空闲连接池里缓存32条连接到后端的keepalive连接",但真正建立连接时,Nginx默认用HTTP/1.0协议和后端通信,而没有持久连接语义的HTTP/1.0请求,后端在处理完响应后就会把连接关闭。连接池里缓存连接的动作还没执行,连接就已经断了。keepalive指令因此名存实亡。

4.2 HTTP/1.0 与 HTTP/1.1 的持久连接差异

这里我尽量用大白话讲一下底层逻辑。HTTP/1.0协议里,每个TCP连接默认只处理一个请求,服务器响应完就直接关闭连接,除非客户端显式加一个Connection: keep-alive头,双方才可能把连接保留下来。HTTP/1.1协议则把持久连接变成了默认行为,只要没有Connection: close头,TCP连接就可以被多个请求复用。

而Nginx向后端转发请求时,默认使用的是HTTP/1.0。在这种情况下,即便upstream块里写了keepalive,连接也无法进入"空闲可复用"队列。Nginx和后端的交互过程就变成了:一个请求进来,新建TCP连接,完成HTTP请求响应,连接立刻关闭。下一个请求再重复一遍。

这个差异在高并发场景下是致命的。短连接模式下,QPS每往上走1000,每秒就要新增1000次TCP三次握手和四次挥手。TCP握手需要时间,TIME_WAIT会堆积,后端机器的accept队列会被打满,最终表现就是Nginx在connect阶段就失败。

4.3 源端口耗尽:从 TCP 层面理解 502 的最后一击

还有一个更隐蔽的助推因素:源端口耗尽。Nginx作为发起连接的一方,每次向后端建立新连接时,都会占用本机一个临时端口。Linux系统默认的临时端口范围通常是从32768到60999,也就是大约28000多个可用端口。前面说过了,TIME_WAIT状态的连接要等大约60秒才会被系统回收,如果新建连接的速度远远快于回收速度,临时端口很快就被TIME_WAIT连接占满。

一旦源端口耗尽,Nginx再想向后端发起新连接,系统直接返回Cannot assign requested address。这个错误会同样体现为502。我就是在这个环节彻底确认了整个链路:配置缺三行,连接退化成短连接,短连接导致TIME_WAIT堆积和源端口耗尽,最终表现为间歇性502。

5. 修复与验证:三行配置解决战斗

5.1 最终修复配置

修改后的location块是这样:

location /api/ { proxy_pass http://backend_cluster; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }

核心就两件事。第一行proxy_http_version 1.1;让Nginx改用HTTP/1.1协议与上游通信,这样连接具备持久化能力。第二行proxy_set_header Connection "";则是清空Nginx向后端传递的Connection头,避免客户端请求里自带的Connection头(比如WebSocket升级请求里的Connection: Upgrade)被原样透传给后端,干扰连接复用逻辑。

那为什么keepalive数字建议保持32或者适当调大?因为空闲连接池本身也是在缓存与后端的连接,这些连接虽然空闲,但仍占用后端的线程和文件描述符资源。数值设太大,比如200,反而可能把后端的连接数打高。32在实际运行中是一个相对平衡的起步值。

5.2 验证结果:TIME_WAIT 断崖式下降

执行nginx -t检查配置语法没问题后,我做了reload,然后用之前同样的压测方式重新打了500并发。几组数据对比非常直观:

指标修复前(500并发)修复后(500并发)
502错误率约7%-12%0%
Nginx到后端连接数个位数稳定在25-35之间
TIME_WAIT连接数峰值超过4000峰值降到200左右
请求平均响应时间部分请求超3s稳定在100ms以内

压测稳定跑了5分钟,一个502都没出现。随后流量逐渐切回生产,原来那种"每到高峰就掉成功率"的现象彻底消失。Nginx到后端的ESTABLISHED连接数稳在了keepalive池设置的32附近,TIME_WAIT数量也降到了一个非常健康的水平。

5.3 回滚预案:如果不能动后端怎么办

这里有个特殊场景我也想提一下:如果后端服务本身是老旧系统,只支持HTTP/1.0,升级到1.1反而可能导致兼容问题。这种情况下可以考虑保持HTTP/1.0,但必须从其他层面控制连接建立频率,比如调大proxy_connect_timeout在可接受范围内、规划更分散的上游节点、或者在Nginx前再加一层负载分发来降低单机连接压力。不过这些都是绕路方案,最根本的解法依然是让连接能复用。

我还额外确认过,这个场景下并不需要调keepalive_timeout、keepalive_requests这类和客户端侧相关的指令,因为问题只发生在上游通信段。很多时候配置调优失控,就是因为分不清这个"keepalive"属于连接池还是属于HTTP连接的空闲超时,把它俩混为一谈,结果越调越乱。

6. 这次故障教会我的:Nginx 连接复用配置自查清单

6.1 快速自查清单

我把这次故障的排查经验整理成了一份检查清单,每次写新的Nginx反代配置或线上出现诡异502时,我都会按顺序过一遍:

  • upstream块里是否写了keepalive,且数量是否符合预期
  • 对应location块里是否有proxy_http_version 1.1
  • 对应location块里是否显式清空了Connection头
  • 代理的协议是HTTP还是HTTPS、WebSocket,不同协议对连接的要求不一样
  • 后端服务本身是否支持长连接,如Tomcat、Spring Boot等容器对HTTP/1.1的支持通常是默认开启的,但某些自研框架或老旧中间件可能默认关闭

如果配置里keepalive写了、但上面两条漏了,那等于没写。这是我这次踩坑后最刻在脑子里的记忆。

6.2 排查 502 的一线经验顺序

经历过这次之后,我处理502的排查顺序也固定了下来:

先看Nginx日志,确定报错类型是connect阶段还是read阶段。connect阶段的问题优先查网络连通性、后端监听端口、连接池复用、源端口耗尽;read阶段的问题优先查后端处理耗时、超时配置、响应缓冲区。然后立即观察Nginx和后端之间的连接状态,确认连接是否在复用。如果连接数稀少而QPS很高,基本可以锁定短连接问题。

还有一个很实用的方法:直接在Nginx机上抓包过滤。比如用tcpdump抓后端端口的TCP握手包,如果每秒SYN包数量异常高,就能从底层佐证每请求都在新建连接。这个方法在大规模生产环境里尤其好用,比反复看监控图表更直观。

6.3 最后一个反直觉的提醒

最后分享一个反直觉但很重要的点:问题爆发时会想当然地加机器、加线程池,但那次的事实是,后端机器的CPU、内存完全没到瓶颈,加机器只能解决表象,治标不治本。

另一点是,任何一次reload或重启让故障短暂消失,都值得警觉。它说明系统里积累的异常状态被重置了,但生成异常状态的机制还活着,很快就会复发。遇到这种情况,宁可顶着压力把根因找出来,也不要反复使用"重启大法"。

说到底,Nginx的502排查并不一定非要用多高深的手段,很多时候就是一层窗户纸。但这层窗户纸如果没捅破,可能让大家在线上耗掉一个又一个下午。希望这篇复盘能帮各位少走一次冤枉路,至少在打开nginx.conf的时候,多看两眼那三行不起眼的配置。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询