问题背景
公司 API Gateway 层使用 Nginx 作为反向代理,后端 10 台 Tomcat 服务器(Java 应用)组成 upstream 池,配置了被动健康检查(max_fails + fail_timeout)。正常情况下 10 台后端均匀承担约 2000 QPS 的流量。
故障现象
晚上 8:00,运维监控突然告警:API Gateway 返回大量 502 Bad Gateway。登录 Nginx 服务器查看后端状态:
|
|
10 台后端服务器全部被 Nginx 标记为 down——流量没有地方去,全部返回 502。但登录后端服务器检查,Tomcat 进程运行正常,直接访问 10.0.1.x:8080/health 返回 200 OK!
排查过程
第一步:检查 Nginx 错误日志
|
|
所有被标记 down 的服务器都出现了 upstream timed out。
第二步:检查后端 Tomcat 日志
|
|
20:08:11,Tomcat 触发了一次 Full GC,耗时 2.45 秒。Full GC 期间所有应用线程被暂停(Stop-The-World),Tomcat 无法处理任何请求——包括 Nginx 的健康检查请求。
第三步:分析 Nginx 超时配置
Nginx 的 proxy 超时配置:
|
|
proxy_connect_timeout 2s 控制了建立 TCP 连接的超时时间。Full GC 期间 Tomcat 虽然 accept 了 TCP 连接(内核态完成三次握手),但应用层无法处理——所以 proxy_connect_timeout 没有触发超时。
但 Nginx 在建立连接后发送 HTTP 请求,等待 Tomcat 返回响应——这走的是 proxy_read_timeout。在 Full GC 结束前,Tomcat 不会返回任何响应。30 秒后 Nginx 判定该请求超时,计为一次 failure。
Nginx 配置中 max_fails=2 意味着 2 次超时就标记 downstream。10 台 Tomcat 因为部署一致,几乎在同一时间触发 Full GC(相同的内存压力模式),于是短时间内全部被标记为 down。
第四步:验证时间线
|
|
健康检查的 fail_timeout=30s 后节点被重新加入 upstream 池,但流量涌回又导致新的 Full GC,节点再次被标记 down——形成恶性循环。
解决方案
紧急恢复
|
|
修复配置
|
|
关键改动:
fail_timeout从 30s 降到 10s——节点恢复更快proxy_read_timeout从 30s 降到 10s——不会因 GC 等待太久
应用层优化
- 调大 Tomcat 堆内存:从 4GB 扩大到 8GB
- 优化 GC 策略:从 ParallelGC 切换到 G1GC,减少 Full GC 频率和时延
- 添加主动健康检查端点:
/health接口增加内存压力检测,堆使用 >90% 返回 503 让 Nginx 主动摘除
根因分析
直接原因:Java Full GC 的 STW 暂停(2.5 秒)导致 Tomcat 无法处理请求,Nginx 在 proxy_read_timeout(30 秒)后判定超时,累积触发 max_fails=2 将节点标记 down。所有节点因相同的内存模式同时触发 Full GC,被批量剔除。
深层原因:健康检查配置没有区分"节点过载"和"节点宕机"——Full GC 期间节点还在但暂时无法响应,应该给予缓冲而非直接剔除。
预防措施
- 主动健康检查:使用 Nginx Plus 或第三方模块(nginx_upstream_check)做主动健康检查,区分 503(过载)和 超时(宕机)
- 渐进式摘除:节点因超时被标记 down 后,先降权而非直接剔除
- JVM GC 监控:监控 Full GC 频率和耗时,Full GC >1 秒 / 频率 >1次/小时 告警
- 后端容错设计:至少保留 50% 的健康节点,即使全触发 GC 也不会全 down
- Nginx upstream 冗余:
server块配置backup节点作为最后兜底
总结
Nginx 的健康检查本质上是"事后惩罚"——请求已经超时了,它才标记节点 down。当所有后端同时"卡顿"(GC、死锁、资源耗尽)时,健康检查不仅帮不上忙,还会把最后活着的节点也干掉。真正靠谱的做法是在应用层做主动降级(堆内存高了返回 503 主动退出),让 Nginx 在节点真正不可用之前就把流量导走,而非被动等到超时。