1. 一次线上大面积502的紧急排查复盘
那天下午三点多,监控群里突然炸了锅。值班的同学甩了一张图出来,曲线几乎垂直往上冲——网关层返回的502错误在五分钟内从个位数飙到了每分钟上千次。紧接着业务侧开始反馈,部分用户下单失败、页面白屏,客服电话也跟着响了起来。说实话,干运维这行,看到502这三个数字,心里大概就有数了:要么是后端服务挂了,要么是中间那层转发出了问题。但真正让人头疼的不是“知道它坏了”,而是“知道它为什么坏”。
这篇文章我想完整复盘一下那次排查的全过程。从最开始的告警确认,到中间一层层剥洋葱式的定位,再到最后找到根因并修复,整个链路我会尽量还原当时的思路和操作。如果你也在维护基于Nginx的网关或反向代理层,或者你所在的团队后端服务经常出现偶发或批量502,这篇内容应该能帮你少走一些弯路。我会把排查中用到的命令、判断依据、容易踩的坑都写清楚,哪怕你刚接触Nginx不久,也能跟着思路一步步往下走。
先说结论:那次502的根因并不在Nginx本身,而是后端一组服务实例因为连接池耗尽导致健康检查失败,被Nginx判定为不可用,而剩余实例扛不住流量,进而引发连锁反应。但排查过程中我们走了不少弯路,比如一开始怀疑是Nginx的worker连接数不够、后来又怀疑是DNS解析问题,最后才通过日志和系统指标锁定了真正的瓶颈。这些弯路的经验,恰恰是最值得分享的部分。
2. 502错误的本质与常见触发链路
2.1 从Nginx的视角理解502
很多人对502的理解停留在“网关挂了”这个层面,其实不够准确。502 Bad Gateway的本质含义是:作为代理角色的Nginx,从上游服务器(upstream)收到了一个无效的响应。注意关键词——“无效的响应”。这意味着Nginx本身是活着的,它成功接收了客户端请求,也成功把请求转发给了后端,但后端要么没回应,要么回应了但Nginx解析不了。
用生活化的类比来说,Nginx就像一个前台接待员,客户来了它负责把需求转给后面的工程师。502的意思是:接待员把需求递进去了,但工程师要么没接电话,要么回了一句听不懂的话。所以排查502的核心思路永远是两个方向:第一,后端为什么没给有效响应;第二,Nginx和后端之间的通信链路是不是断了。
在Nginx的配置中,和502关系最密切的几个指令包括proxy_pass、proxy_connect_timeout、proxy_read_timeout、proxy_next_upstream等。这些参数决定了Nginx如何连接后端、等待多久、以及在后端异常时是否重试。理解这些参数的默认值和实际生效逻辑,是排查的基础。
2.2 触发502的六种典型场景
根据我这些年处理过的案例,502的触发原因大致可以归为以下几类:
| 场景分类 | 具体原因 | 典型特征 |
|---|---|---|
| 后端服务崩溃 | 进程退出、OOM被杀 | 502持续且稳定,后端端口无响应 |
| 连接超时 | 后端处理慢,超过proxy_read_timeout | 502间歇性出现,伴随响应时间飙升 |
| 连接池耗尽 | 后端最大连接数打满 | 502批量出现,后端CPU可能不高 |
| 健康检查误判 | 后端短暂抖动被标记为down | 502突发突止,与健康检查周期相关 |
| 网络层问题 | 防火墙规则变更、端口不通 | 502集中在特定实例或网段 |
| 配置错误 | upstream地址写错、端口不对 | 502从配置变更后开始 |
那次我们遇到的情况,实际上是“连接池耗尽”和“健康检查误判”的叠加。后端某个服务的数据库连接池配置偏小,在流量高峰期连接被占满,导致健康检查接口响应超时,Nginx的被动健康检查机制将其标记为不可用,流量全部打到剩余实例,剩余实例很快也被压垮,形成雪崩。
注意:很多团队只配置了Nginx的被动健康检查(即
max_fails和fail_timeout),而没有主动健康检查。被动检查的缺点是,它必须等到真实用户请求失败后才会标记后端异常,这意味着第一批用户的请求已经失败了。
3. 排查前的信息收集与现场保护
3.1 第一时间要抓取的关键数据
出现批量502时,最忌讳的就是手忙脚乱地重启服务。重启确实能暂时恢复,但现场也被破坏了,根因可能永远找不到,下次还会再犯。我的习惯是,在动手任何操作之前,先花两分钟抓取以下信息:
- Nginx错误日志:
tail -f /var/log/nginx/error.log,重点关注upstream timed out、connect() failed、no live upstreams等关键词。 - Nginx访问日志中的502比例:通过
awk统计最近几分钟的502占比,确认影响面。 - 后端服务实例列表和健康状态:如果用了服务发现,查看注册中心里哪些实例被标记为不健康。
- 系统级指标:后端机器的CPU、内存、连接数(
ss -s或netstat -an | wc -l)、文件描述符使用情况。 - Nginx自身的连接状态:
ss -tnp | grep nginx | wc -l,看Nginx到后端的连接是否异常多。
这些数据抓下来,基本就能判断问题是出在Nginx层、网络层还是后端层。那次我们抓完数据后发现一个关键线索:Nginx错误日志里大量出现upstream timed out (110: Connection timed out) while connecting to upstream,这说明Nginx在连接后端时就超时了,而不是后端返回了错误响应。
3.2 不要急着改配置或重启
我见过不少同学一看到502就去改proxy_read_timeout,把它从60秒调到120秒,觉得“等久一点就好了”。这个操作在特定场景下确实能缓解问题,但大多数时候只是把问题推迟了。如果后端处理一个请求需要120秒,那说明后端本身有性能瓶颈,调大超时只会让更多请求堆积,最终导致更严重的雪崩。
另一个常见误区是直接重启Nginx。重启Nginx会断开所有现有连接,对于长连接场景(比如WebSocket)影响很大。而且如果根因在后端,重启Nginx根本解决不了问题,反而会丢失错误日志的连续性。
实操心得:我通常会在排查前先给Nginx的error_log临时调高日志级别到
info,这样能看到更详细的 upstream 交互过程。但注意,info级别日志量很大,排查完要记得调回warn或error,否则磁盘很快会被写满。
4. 逐层定位:从Nginx到后端的完整排查路径
4.1 第一步:确认Nginx自身是否正常
虽然502通常意味着后端有问题,但也不能完全排除Nginx自身的异常。首先要确认Nginx的worker进程是否足够、连接数是否打满。
# 查看Nginx进程数和worker连接数 ps aux | grep nginx | grep -v grep ss -tnp | grep nginx | awk '{print $1}' | sort | uniq -c如果发现ss输出中Nginx到后端的连接大量处于SYN-SENT状态,说明Nginx发起了连接但后端没有回应,问题大概率在后端或网络层。如果连接处于ESTABLISHED但数量接近worker_connections上限,说明Nginx的连接池不够用了。
那次我们检查后发现,Nginx自身的连接数并不高,worker进程也正常,SYN-SENT状态的连接也不多。这排除了Nginx自身瓶颈和网络不通的可能性,矛头指向了后端服务的响应能力。
4.2 第二步:分析upstream的响应状态
Nginx的upstream模块提供了一些状态信息,可以通过stub_status或者商业版的upstream_conf来查看。如果没有这些模块,最直接的方式还是看错误日志。
# 统计最近1000行错误日志中各类错误的出现次数 tail -1000 /var/log/nginx/error.log | grep -oP 'upstream.*?(?=,|$)' | sort | uniq -c | sort -rn这个命令能快速告诉你,Nginx到底是在“连接阶段”超时,还是在“读取响应阶段”超时。两者的排查方向完全不同:
- 连接阶段超时(
while connecting to upstream):后端端口没监听、防火墙拦截、后端accept队列满了。 - 读取阶段超时(
while reading response header from upstream):后端处理太慢、线程池打满、数据库查询慢。
我们那次看到的是连接阶段超时,这让我一开始怀疑是后端服务的accept队列满了。但登录后端机器后发现,服务进程还在,端口也在监听,ss -lnt显示Send-Q并不高。这就奇怪了——端口通,但Nginx连不上?
4.3 第三步:检查后端服务的真实负载
既然端口通但连接超时,下一步就是看后端服务到底在干什么。我们登录了一台被标记为异常的实例,用以下命令快速检查:
# 查看服务进程的线程数和CPU占用 top -H -p $(pgrep -f 'java.*服务名' | head -1) # 查看该进程打开的连接数 lsof -p $(pgrep -f 'java.*服务名' | head -1) | wc -l # 查看系统级别的连接统计 ss -s结果发现,该进程打开的连接数接近了操作系统的文件描述符限制(ulimit -n默认是1024,而该进程已经打开了900多个)。更关键的是,大量连接处于CLOSE_WAIT状态,说明后端服务没有正确关闭连接,导致连接泄漏。
这就是根因的雏形:后端服务的HTTP客户端连接池配置有问题,连接用完没有释放,逐渐耗尽文件描述符,最终无法接受新的连接。Nginx发起的新连接请求被操作系统的accept队列拒绝或超时,于是返回502。
4.4 第四步:验证健康检查与流量分配
确认了后端连接泄漏后,还有一个问题需要解释:为什么只有部分实例出现502,而不是全部?这就涉及到Nginx的负载均衡和健康检查机制。
我们检查了Nginx配置中的upstream块:
upstream backend { server 10.0.1.10:8080 max_fails=3 fail_timeout=30s; server 10.0.1.11:8080 max_fails=3 fail_timeout=30s; server 10.0.1.12:8080 max_fails=3 fail_timeout=30s; }max_fails=3意味着连续失败3次后,该实例会被标记为不可用,持续fail_timeout=30s。当实例10.0.1.10因为连接泄漏开始超时后,Nginx在30秒内将其剔除,流量转移到11和12。但11和12本身也在承受正常流量,突然多出来的流量加速了它们的连接消耗,很快也触发了同样的阈值。这就是雪崩的完整链路。
注意:
max_fails的计数是在fail_timeout窗口内累计的,不是永久计数。如果后端只是短暂抖动,30秒后会自动恢复。但如果后端持续异常,就会反复被剔除和恢复,造成流量忽高忽低。
5. 根因修复与长效防护措施
5.1 紧急止血:快速恢复服务
找到根因后,紧急恢复的手段其实很简单:重启后端服务,释放泄漏的连接。但重启之前,我们先做了两件事:
- 将出问题的实例从Nginx upstream中手动摘除(注释掉对应server行并reload),避免重启期间流量打到正在恢复的实例上。
- 临时调大操作系统的文件描述符限制(
ulimit -n 65535),给服务重启后争取更多的连接余量。
重启后,服务在几十秒内恢复了正常,502曲线迅速回落。但这只是止血,真正的修复还在后面。
5.2 彻底修复:连接池配置优化
连接泄漏的根因是后端服务使用的HTTP客户端连接池没有正确配置maxConnections和idleTimeout。具体来说,代码中创建连接池时使用了默认配置,而默认的最大连接数远小于实际并发需求,导致连接被长时间占用后无法释放。
修复方案包括:
- 将连接池的最大连接数从默认的50调整到200,匹配实际QPS。
- 设置
idleTimeout为30秒,确保空闲连接能被及时回收。 - 在获取连接的代码路径上增加超时机制,避免无限等待。
- 增加连接池监控指标,实时上报活跃连接数和等待队列长度。
这些改动上线后,后端服务的文件描述符使用量稳定在200左右,不再出现持续增长的情况。
5.3 长效防护:监控与告警体系完善
修复了根因不代表万事大吉,还需要建立防护机制,确保下次出现类似征兆时能提前发现。我们补充了以下几项监控:
| 监控项 | 阈值 | 告警方式 |
|---|---|---|
| Nginx 502比例 | 超过1%持续1分钟 | 电话+消息 |
| 后端文件描述符使用率 | 超过80% | 消息 |
| 后端连接池等待队列长度 | 超过10 | 消息 |
| Nginx upstream超时次数 | 超过10次/分钟 | 消息 |
另外,我们把Nginx的被动健康检查升级为主动健康检查(通过health_check指令或第三方模块),这样后端实例在真正影响用户之前就会被标记为异常,给运维留出反应时间。
实操心得:主动健康检查的间隔不要设得太短,否则会给后端带来额外压力。一般5到10秒比较合适。检查的接口最好是一个轻量级的、不依赖数据库的接口,比如
/health,只返回服务自身状态。
6. 排查502时最容易踩的五个坑
6.1 只看Nginx日志,不看后端日志
Nginx的错误日志只能告诉你“连接超时”或“读取超时”,但不会告诉你后端为什么慢。那次排查中,如果我们只盯着Nginx日志,最多只能得出“后端响应慢”的结论,永远找不到连接泄漏这个根因。正确的做法是Nginx日志和后端服务日志对照着看,时间戳对齐,才能还原完整的请求链路。
6.2 盲目调大超时参数
调大proxy_read_timeout和proxy_connect_timeout确实能减少502的数量,但代价是用户等待时间变长,而且问题被掩盖了。如果后端处理一个请求需要30秒,用户早就关掉页面了,Nginx返回502反而比让用户干等更合理。超时参数应该根据业务的实际响应时间分布来设定,而不是拍脑袋调大。
6.3 忽略操作系统的连接数限制
很多502的根因其实在操作系统层面:文件描述符不够、端口耗尽、accept队列溢出。这些指标在应用层是看不到的,必须登录到机器上用ss、netstat、lsof等命令检查。特别是容器化环境,容器的ulimit和宿主机可能不一致,更容易出问题。
6.4 没有区分“连接超时”和“读取超时”
这两种超时的排查方向完全不同。连接超时说明TCP握手都没完成,问题在端口、防火墙或accept队列;读取超时说明连接建立了但后端没及时返回数据,问题在后端处理逻辑。Nginx错误日志里会明确写出是while connecting还是while reading,一定要看清楚。
6.5 修复后不做回归验证
修复上线后,一定要在流量高峰期观察一段时间,确认502不再出现、连接数稳定、文件描述符使用量正常。我们那次修复后连续观察了三天,每天早晚高峰都检查一遍监控曲线,确认没有反弹才最终关闭故障单。
7. 写在最后的一些个人体会
处理线上故障这件事,技术能力只是一部分,更重要的是心态和流程。我见过太多人在告警响起时第一反应就是重启,结果问题反复出现,团队疲于奔命。其实每一次故障都是一次理解系统的好机会,现场保留得越完整,能学到的东西就越多。
另外,502这个错误本身并不可怕,可怕的是它背后的连锁反应。一个实例的连接泄漏,通过Nginx的健康检查机制,可以在几十秒内演变成整个集群的雪崩。所以如果你在维护类似的架构,我建议一定要关注两个东西:一是后端服务的资源使用趋势,二是Nginx的upstream状态变化。这两个指标如果能提前告警,很多故障都可以在影响用户之前被拦截。
最后分享一个小技巧:在Nginx配置中加上proxy_next_upstream error timeout http_502;,这样当某个后端返回502时,Nginx会自动重试下一个健康的实例。这个配置在实例偶发故障时非常有用,能显著降低用户感知到的错误率。但注意,它只对幂等请求安全,如果是POST请求,重试可能会导致重复提交,需要根据业务场景谨慎使用。