Nginx 偶发 502 且上游日志正常:upstream keepalive 陈旧连接的排查与治理

适用场景

服务部署在 Nginx 反向代理之后,流量高峰或发布后偶发 502 Bad Gateway。Nginx 错误日志出现 upstream prematurely closed connectionrecv() failed (104: Connection reset by peer)upstream timed out,但应用日志没有对应的 5xx,直接访问应用健康检查又正常。后端是 Gunicorn、uWSGI、Java Web 容器或其他支持 HTTP keepalive 的服务时尤其常见。

本文处理的是「代理复用了后端已关闭的空闲连接」这一类问题;它和后端真实崩溃、连接数耗尽是不同故障,需要分别取证。

现象与典型链路

某 Python API 使用 4 个 Gunicorn worker,前面有两台 Nginx。低峰期一切正常,批量任务结束后的下一波请求偶尔 502,刷新即恢复。Nginx 记录:

2026/08/12 10:03:21 [error] 2814#2814: *974 recv() failed (104: Connection reset by peer) while reading response header from upstream,
client: 10.20.1.15, server: api.example.com, request: "GET /v1/orders/42 HTTP/1.1",
upstream: "http://10.20.3.21:8000/v1/orders/42", host: "api.example.com"

应用侧只有定时重载 worker 的记录,没有这次请求。原因是 Nginx 从连接池取出了一条空闲 TCP 连接;后端已按自己的 keepalive 超时关闭它,Nginx 在复用时才收到 RST。请求是否能成功重试取决于请求是否已发往上游、是否幂等,不能把「刷新成功」当作已经修复。

先建立可关联的证据

不要只按分钟比对日志。给 Nginx 增加请求 ID、上游地址和耗时,再让应用把同一 ID 写入访问日志:

log_format upstream_timing '$request_id $remote_addr "$request" $status '
                           'upstream=$upstream_addr upstream_status=$upstream_status '
                           'connect=$upstream_connect_time header=$upstream_header_time '
                           'response=$upstream_response_time request_time=$request_time';
access_log /var/log/nginx/api_access.log upstream_timing;

proxy_set_header X-Request-ID $request_id;
proxy_http_version 1.1;
proxy_set_header Connection "";

字段解释:upstream_status 是后端实际返回的状态,空值或 502 配合很短的 upstream_connect_time 常见于复用连接立即失败;upstream_header_time 很大则更像应用处理慢;$upstream_addr 可确认问题是否集中到某个实例。应用中间件应读取 X-Request-ID,不要自行每层生成新 ID。

现场先筛选异常请求及其上游:

sudo awk '$0 ~ / 502 / {print}' /var/log/nginx/api_access.log | tail -n 50
sudo rg -n 'prematurely closed|Connection reset|upstream timed out' /var/log/nginx/error.log | tail -n 50
sudo ss -tanp '( sport = :8000 )' | head -n 30
sudo journalctl -u gunicorn --since '30 min ago' --no-pager

ss 用来观察后端监听和连接状态,不能单凭大量 TIME-WAIT 判定故障;journalctl 要特别检查 worker 的优雅重载、SIGTERM、OOM 或部署脚本是否在错误时段关闭了连接。

分四步排查

1. 确认 Nginx 是否启用了上游连接池

sudo nginx -T 2>/dev/null | rg -n -C 3 'upstream |keepalive|proxy_http_version|proxy_set_header Connection'

典型配置如下。keepalive 64 是每个 worker 对该 upstream 缓存的空闲连接上限,不是全站并发上限:

upstream order_api {
    server 10.20.3.21:8000 max_fails=3 fail_timeout=10s;
    server 10.20.3.22:8000 max_fails=3 fail_timeout=10s;
    keepalive 64;
}

若存在 keepalive,而代理仍使用 HTTP/1.0 或显式传递 Connection: close,连接复用配置并不完整,先修正协议头再继续判断。

2. 对齐两端的空闲超时

安全原则是:后端允许的空闲 keepalive 时间应大于 Nginx 保留空闲连接的时间,留出网络与调度余量。以 Nginx 开源版为例,可显式限制上游池中的连接寿命:

upstream order_api {
    server 10.20.3.21:8000;
    server 10.20.3.22:8000;
    keepalive 64;
    keepalive_timeout 50s;
    keepalive_requests 1000;
}

然后将 Gunicorn 的 --keep-alive 设为 65 秒或更高;若由 systemd 启动,检查实际生效参数:

systemctl cat gunicorn
ps -ef | rg '[g]unicorn'

不要混淆 proxy_read_timeout:它约束读取一次上游响应时的两次读操作间隔,解决慢响应;keepalive_timeout 才控制池内空闲连接。Java、uWSGI 或云负载均衡也可能有更短的 idle timeout,取链路中最短值作为风险点。

3. 排除发布和负载均衡造成的主动断连

若 502 只在发布时出现,先检查实例摘流顺序。错误顺序是先重启应用、负载均衡仍在转发;正确顺序是先从服务发现或上游列表摘除,等待在途请求结束,再优雅停止 worker。可临时把问题实例注释并平滑加载 Nginx,验证是否立即消失:

upstream order_api {
    # server 10.20.3.21:8000;
    server 10.20.3.22:8000;
    keepalive 64;
}
sudo nginx -t && sudo systemctl reload nginx

这是一项有流量影响的操作,只应在有剩余容量、变更窗口和回滚配置时执行。若单台实例命中异常明显,检查该机的 OOM、文件描述符、重载脚本以及容器重启次数。

4. 只对安全请求启用上游重试

GET、HEAD 等幂等读取可以在尚未向客户端发送响应时有限重试;支付、创建订单等写操作不能依赖代理重试保证正确性。建议把重试策略收敛到明确的位置:

proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 2;
proxy_next_upstream_timeout 3s;

该配置降低瞬时单实例错误的用户可见率,但会放大后端压力,且对非幂等请求可能造成重复执行。写接口应由业务提供幂等键、持久化去重和调用方可识别的结果,而不是盲目扩大 Nginx 重试范围。

修复方案与验证

一次可回滚的修复可以按以下顺序执行:

  1. 记录当前 nginx -T、后端启动参数和异常请求比例,作为基线。
  2. 配置 Nginx proxy_http_version 1.1、清空 Connection 头,设置 50 秒 keepalive_timeout;将后端 idle keepalive 调到 65 秒。
  3. nginx -t 成功后平滑 reload;后端采用优雅重载,避免强杀连接。
  4. 观察一整个低峰到高峰周期,按 $upstream_addr 比较 502 率、连接建立耗时和后端重启次数。

可用下面命令快速计算访问日志中的 502 数量;变更前后请使用相同窗口:

sudo awk '$0 ~ / 502 / {n++} END {print n+0}' /var/log/nginx/api_access.log

验证成功的标准不是只看 Nginx 配置已加载,而是异常日志中的 reset/premature-close 消失或显著下降、所有上游实例的流量分布正常,并且业务侧没有重复写入。

预防措施

  • 将 Nginx、应用服务器、Ingress、四层/七层负载均衡的 idle timeout 记录在同一配置表,变更时一起评审。
  • 监控按 upstream_addr 聚合的 5xx、$upstream_response_time 分位数、连接重置日志和应用重启次数;总 5xx 掩盖单实例问题。
  • 发布流程先摘流再优雅退出,设置足够的终止宽限期,并演练回滚。
  • 对写操作实现业务幂等;把代理重试限制在确实可重试的场景。
  • 每次升级 Nginx 或应用运行时后,压测并检查 keepalive、连接数和错误日志,而不是只验证健康检查。

总结

Nginx 偶发 502 不一定代表应用「随机挂了」。用请求 ID 和上游耗时先区分陈旧 keepalive 连接、慢响应和实例重启,再按「后端超时长于代理连接池」对齐参数。最后通过可观测指标和幂等边界验证,才能既减少 502,又避免用无约束重试制造更难追的重复请求。