Nginx 反向代理偶发 502?upstream keepalive 超时竞态排查与配置对齐
问题现象:低频 502,日志里全是 "upstream prematurely closed connection"
上周一个网关层的 Nginx 每天稳定出现几十条 502,量不大但很扎眼:白天业务高峰反而不报,凌晨批量任务时才零星出现,重启 Nginx 能压一两天。典型现象如下:
2026/11/06 02:14:07 [error] 31244#0: *8821331 upstream prematurely closed connection while reading response header from upstream,
client: 10.20.31.7, server: api.example.com, request: "POST /v1/batch/import HTTP/1.1",
upstream: "http://10.30.4.18:8080/v1/batch/import", host: "api.example.com"
关键信息三点:错误是读 response header 阶段被后端断开、只发生在 POST 批量接口、后端是 Nginx 到业务服务的 keepalive 长连接。这三条基本把方向锁死在“连接复用”上,而不是后端崩了。
第一步:先分清 502 是谁给的
502 有两种来源,处理方式完全不同:一种是后端真的拒绝了(connect refused / reset),一种是 Nginx 复用了后端已经准备关闭的 keepalive 连接。看 error.log 里的动词就够了:
# 后端连不上:connect() failed
grep -c "connect() failed" /var/log/nginx/error.log
# 后端断开(连接复用被踢):prematurely closed / reset by peer
grep -c "prematurely closed\|reset by peer" /var/log/nginx/error.log
实测结果:connect() failed 为 0,prematurely closed 占 100%。说明 TCP 三次握手成功,问题出在复用后的第一个请求上——这是经典的 keepalive 竞态:后端在 Nginx 拿到连接后、发出请求前刚好关闭了这个空闲连接。为什么凌晨容易撞上?因为那时连接空闲时间长,正好卡在后端 keepalive timeout 的边界。
第二步:对齐三方的超时时间
竞态的根因是超时时间没对齐。Nginx 会缓存空闲连接 keepalive_timeout 这么久,如果这个值大于等于后端(或后端前面的 SLB)的空闲回收时间,就必然出现“我以为是活的,其实已经死了”。
检查 Nginx 侧:
nginx -T 2>/dev/null | grep -nE "keepalive_timeout|keepalive_requests|upstream|proxy_http_version|proxy_set_header Connection"
upstream api_backend {
server 10.30.4.18:8080;
server 10.30.4.19:8080;
keepalive 64; # 每个 worker 缓存的空闲连接数
keepalive_timeout 65s; # 关键:空闲多久后自己丢掉
keepalive_requests 1000;
}
server {
location /v1/ {
proxy_http_version 1.1; # 必须 1.1,否则 keepalive 无效
proxy_set_header Connection ""; # 必须清空,否则发 close
proxy_connect_timeout 3s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
}
这里有两个高频坑:漏了 proxy_http_version 1.1 和 没清空 Connection 头。少任何一个,upstream keepalive 都不生效,每请求一个短连接,性能直接掉一半,同时还会因 TIME_WAIT 堆积引发端口问题。
再看后端侧(Java/Tomcat、Go http.Server、SLB)的空闲回收:
# 后端是 SLB 或云负载均衡时,查它的空闲超时(常见 15s / 60s)
# 后端是自己服务时:
ss -tnp state established '( sport = :8080 )' | head
# Go 服务里应满足 IdleTimeout < Nginx keepalive_timeout
grep -rn "IdleTimeout\|ReadHeaderTimeout" main.go
我们实测的配置是:Nginx keepalive_timeout 65s、SLB 空闲 60s、后端 Tomcat connectionTimeout 20000。三个数字互相错开且 Nginx 最大,竞态窗口长达 45 秒。改法是让上游最小的那个再减 5~10 秒:
keepalive_timeout 45s; # SLB 60s、Tomcat 20s → 取 20s 下面再小一档亦可
保守做法是直接取后端最小值减 10s。若后端 connectionTimeout=20s,Nginx 侧就设 10s,宁可多一次握手,也不要发到死连接上。
第三步:用重试兜住剩余的竞态
超时对齐能把 99% 的竞态消掉,但网络抖动、滚动发布、后端主动 close 依然会漏。对幂等接口加一次重试代价极低:
proxy_next_upstream error timeout http_502 non_idempotent;
proxy_next_upstream_tries 2;
proxy_next_upstream_timeout 5s;
注意三点:一是 non_idempotent 只对明确可重放的写接口开启,POST 导入类接口若不加幂等键,重试会造成重复入库,我这里只对 /v1/query 这类只读路径放开;二是重试会把请求打到另一台后端,若 session 粘在单机需要确认无状态;三是 proxy_next_upstream_tries 2 是总次数,别设成 5,否则故障时会成倍放大流量。
第四步:补上观测,别等下次再猜
光靠 error.log 只能事后追,加两个轻量指标就能提前发现:
# stub_status 里的连接状态(Active / Reading / Writing / Waiting)
curl -s http://127.0.0.1:8081/nginx_status
# 空闲连接数应该长期接近 keepalive 上限的 30%~70%
# Waiting 长期为 0 → keepalive 没生效(多半是 proxy_http_version 没写)
再补一条 502 速率告警:
- record: nginx:502_rate
expr: sum(rate(nginx_http_requests_total{status="502"}[5m])) by (upstream_addr)
/ sum(rate(nginx_http_requests_total[5m])) by (upstream_addr) > 0.001
结果与经验
改完 keepalive_timeout 对齐(65s → 10s)、补上 proxy_http_version 1.1 与清空 Connection、对只读接口加单次重试后,nginx -t 通过并 nginx -s reload 平滑生效。观察 3 天,502 从日均 40+ 条降到 0,stub_status 的 Waiting 从个位数涨到 200 左右,说明连接真正被复用了,后端 TIME_WAIT 也同步下降。
一句话总结:502 先看错误动词定位是“连不上”还是“连接复用被踢”,后者一律先对齐超时,再谈重试和观测。随手把 keepalive_timeout 设得比后端还大,是最常见也最隐蔽的坑。
- 点赞
- 收藏
- 关注作者
评论(0)