新搭的 Nginx 反代,QPS 不到 2000 后端就开始返回 502。后端服务器 ss -s 显示几万个 ESTABLISHED + 几万 TIME_WAIT,CPU 主要花在 TCP 握手 / 挥手上。问题不复杂:Nginx 到 upstream 一直在用短连接,每个请求一次握手 + 挥手。但修起来一个细节漏了就不生效。本文讲完整配置 + 验证方法。
故障现场
# 后端服务器(被反代的目标)
$ ss -ant | awk 'NR>1 {print $1}' | sort | uniq -c
18234 ESTAB # 1.8 万长连接
47521 TIME-WAIT # 4.7 万 TIME_WAIT,后端是被动关闭方
1042 LISTEN
# 后端 sysctl 限制
$ sysctl net.ipv4.ip_local_port_range
net.ipv4.ip_local_port_range = 32768 60999
# 可用端口 28K,TIME_WAIT 已经 47K,完全爆了
# 后端 syslog
kernel: TCP: too many orphaned sockets
# Nginx access.log
2024-10-12T03:14:23 502 0.012 - - upstream timed out
2024-10-12T03:14:23 502 0.013 - - upstream timed out
2024-10-12T03:14:23 502 0.014 - - upstream timed out
问题诊断:Nginx 每次转发都和后端建一个新 TCP 连接,后端忙到来不及处理,握手都来不及。
原理:为什么默认是短连接
HTTP/1.0 默认每个请求一个 TCP 连接,处理完关闭。HTTP/1.1 默认 keep-alive,同一连接可以处理多个请求。
Nginx 作为反向代理时:
- 对客户端那边:Nginx 默认 keep-alive(
keepalive_timeout 75s) - 对 upstream 那边:默认是 HTTP/1.0 短连接,每个请求一个 TCP 连接
这是常见的认知盲区。即使你的后端服务支持 HTTP/1.1 keep-alive,只要 Nginx 不复用,就是浪费。
正确配置:upstream keepalive 三件套
upstream backend {
server 10.0.5.21:8080 max_fails=3 fail_timeout=30s;
server 10.0.5.22:8080 max_fails=3 fail_timeout=30s;
server 10.0.5.23:8080 max_fails=3 fail_timeout=30s;
# === 1. 启用 keepalive 连接池 ===
keepalive 128; # 每个 worker 跟所有 upstream 一共保留 128 个空闲连接
keepalive_timeout 60s; # 空闲超过 60 秒关闭
keepalive_requests 10000; # 单个连接最多处理 1 万请求后关闭(防内存泄漏 / 状态污染)
}
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://backend;
# === 2. 必须 HTTP/1.1(默认是 1.0) ===
proxy_http_version 1.1;
# === 3. 清空 Connection header(默认 Nginx 会传 close) ===
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_set_header X-Forwarded-Proto $scheme;
# 超时
proxy_connect_timeout 3s;
proxy_send_timeout 10s;
proxy_read_timeout 30s;
# 失败时换下一台
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 2;
proxy_next_upstream_timeout 5s;
}
}
三件套:
keepalive 128—— 没这行就没有连接池proxy_http_version 1.1—— 没这行 Nginx 走 HTTP/1.0,即使 upstream 支持 keep-alive 也用不上proxy_set_header Connection ""—— 没这行 Nginx 会发Connection: close,后端处理完一个请求就关
三个少一个就完全不生效。我们当年第一次修就少了第三行,以为修好了,实际还是短连接。
验证 keep-alive 真的生效了
# 方法 1: 在 nginx access log 加一个变量,记录上游连接状态
log_format upstream_log '$remote_addr - $time_iso8601 - $upstream_addr '
'upstream_connect=$upstream_connect_time '
'upstream_response=$upstream_response_time '
'status=$status';
# 看 upstream_connect_time 是不是 0(复用) 还是 几毫秒(新建)
awk -F'upstream_connect=' '{print $2}' access.log | awk '{print $1}' | \
awk '{ if ($1+0 == 0) reuse++; else newconn++ } END { print "reuse:", reuse, "new:", newconn }'
# 方法 2: 后端机器看 ESTABLISHED 数量(应该几十上百,而不是几万)
ss -ant state established '( sport = :8080 )' | wc -l
# 方法 3: 看 TIME_WAIT 数量(后端是被动关的,所以正常情况后端 TIME_WAIT 很少)
ss -ant state time-wait '( sport = :8080 )' | wc -l
# 方法 4: 用 tcpdump 看连接是否复用
tcpdump -i any -nn 'host 10.0.5.21 and tcp port 8080' -c 100
# 如果看到 SYN / FIN 频繁,说明短连接
# 如果几乎全是 PSH / ACK,说明在复用
keepalive 数怎么调
这个值不是越大越好。计算公式:
keepalive 应该 ≈ (QPS / 后端实例数 / 平均连接复用次数) + buffer
举例:
- 单 Nginx 处理 5000 QPS
- 后端 5 个实例,均衡到每个 1000 QPS
- 每个连接平均 1 秒内被复用 N 次,假设 100 次/连接
- 每实例需要约 10 个并发连接
- keepalive 设 20-30 比较合适
设太大浪费内存,设太小达不到复用效果。我们最终配 keepalive 128 是因为我们这台 Nginx 转发到 8 个 upstream,128/8=16 平均每个 16 连接,合理。
upstream 健康检查
Nginx 开源版自带的健康检查很弱(被动检查:请求失败 max_fails 次才标记 down)。生产环境要主动检查:
upstream backend {
server 10.0.5.21:8080 max_fails=3 fail_timeout=30s;
server 10.0.5.22:8080 max_fails=3 fail_timeout=30s;
keepalive 128;
}
# 用 ngx_http_healthcheck_module 或者 nginx-upstream-check
# 或者直接用 OpenResty,可以写 Lua 主动 ping
# 兜底:Prometheus blackbox_exporter + alertmanager
# 配置一个每 10 秒探一次 upstream 的 job
如果用 Nginx Plus(商业版)就有主动健康检查。开源党用 Tengine 或 OpenResty 比较多。
upstream 失败重试不要无脑开
location / {
proxy_pass http://backend;
# 看起来"更可靠":失败就换一台
proxy_next_upstream error timeout http_502 http_503 http_504 http_500;
proxy_next_upstream_tries 5; # 最多换 5 次
proxy_next_upstream_timeout 30s;
}
看起来安全,但有个坑:非幂等请求(POST / PUT / DELETE / PATCH)默认也会被重试!付款接口被重试 = 用户被扣两次钱。
正确做法:
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
# 只对幂等请求重试 + 严格的错误类型
proxy_next_upstream error timeout http_502 http_503;
proxy_next_upstream_tries 2;
# 默认 proxy_next_upstream 已经会跳过 non-idempotent(POST/LOCK/PATCH)
# 但要确保 non_idempotent 没显式开
# proxy_next_upstream_non_idempotent off; # 这是默认值
}
# 或者区分 location
location ~ ^/api/(read|query|search) {
proxy_pass http://backend;
proxy_next_upstream error timeout http_502 http_503;
proxy_next_upstream_tries 3;
}
location ~ ^/api/(pay|order|charge) {
proxy_pass http://backend;
proxy_next_upstream off; # 完全不重试
}
负载均衡算法选择
upstream backend {
# 默认:round_robin —— 按顺序轮询,均匀但不感知后端负载
# 加权:server X weight=3 —— X 处理 3 倍于其他
# least_conn —— 选当前连接数最少的(适合长连接场景)
least_conn;
# ip_hash —— 同一客户端 IP 永远到同一后端(session 黏性,但 NAT 后大量客户端集中)
# ip_hash;
# hash $request_uri consistent; —— 一致性哈希,适合缓存命中率优先
server 10.0.5.21:8080;
server 10.0.5.22:8080;
server 10.0.5.23:8080;
keepalive 128;
}
我们一般 API 走 least_conn(每个请求处理时间不一致,least_conn 自适应);静态资源缓存走一致性哈希(同 URL 总是到同一缓存节点)。
WebSocket / SSE / gRPC 反代
这些长连接协议要单独配:
# WebSocket
location /ws {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 不能用空 Connection!WebSocket 升级握手需要 Upgrade
proxy_read_timeout 86400s; # 24h
proxy_send_timeout 86400s;
}
# SSE (Server-Sent Events)
location /sse {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off; # 关键:不缓冲,事件实时传给客户端
chunked_transfer_encoding on;
proxy_read_timeout 86400s;
}
# gRPC(需要 HTTP/2)
location / {
grpc_pass grpc://backend; # 注意是 grpc_pass 不是 proxy_pass
grpc_send_timeout 60s;
grpc_read_timeout 60s;
}
# gRPC 的 upstream 也要 keepalive
upstream grpc_backend {
server 10.0.5.21:50051;
keepalive 32;
# 没有 grpc 专用的 keepalive 指令,用 HTTP/2 标准的
}
配置上线前自检脚本
#!/usr/bin/env bash
set -euo pipefail
CONF=$1
echo "=== 检查 $CONF ==="
# 1. 语法检查
nginx -t -c "$CONF" || { echo "FAIL: syntax error"; exit 1; }
# 2. upstream 必有 keepalive
grep -E 'upstream\s+\w+' "$CONF" | while read line; do
block=$(awk "/$line/,/}/" "$CONF")
if ! echo "$block" | grep -q 'keepalive '; then
echo "WARN: upstream 没有 keepalive: $line"
fi
done
# 3. proxy_pass 配合 HTTP 1.1
if grep -q 'proxy_pass' "$CONF" && ! grep -q 'proxy_http_version 1.1' "$CONF"; then
echo "FAIL: proxy_pass 但没设 proxy_http_version 1.1"
exit 1
fi
# 4. 检查 Connection header
if grep -q 'proxy_pass' "$CONF" && ! grep -q 'proxy_set_header Connection' "$CONF"; then
echo "FAIL: proxy_pass 但没清 Connection header"
exit 1
fi
# 5. 检查 POST 类是否误开 retry
if grep -A2 'proxy_next_upstream' "$CONF" | grep -q 'proxy_next_upstream_non_idempotent on'; then
echo "WARN: 开了 non_idempotent 重试,确认是只读接口"
fi
echo "OK: $CONF 检查通过"
把这个脚本放 CI 里,每次改 Nginx 配置先跑一遍。我们后来又救过自己 2 次。
修复后效果
配置 后端 ESTABLISHED 后端 TIME_WAIT QPS 上限 P99 延迟
原版(短连接) 45000+ 47000+ 2000 850ms
+ keepalive 128 220 80 12000 45ms
+ proxy_http_version 1.1 220 80 12000 45ms
+ Connection "" 220 80 12000 45ms
QPS 提升 6 倍,延迟降 19 倍,后端 CPU 从 90%(花在握手挥手)降到 30%。这套配置上完之后,我们再也没遇到过短连接耗端口的问题。
Nginx 反代必读 6 条
- 三件套必齐:keepalive + proxy_http_version 1.1 + proxy_set_header Connection ""
- 非幂等请求别重试:除非业务幂等
- 超时三阶梯:connect 3s + send 10s + read 30s
- WebSocket / SSE / gRPC 单独 location:配置完全不同
- 日志记 upstream_response_time:监控真实后端延迟
- CI 自检脚本:语法 + 必备指令检查,5 行 shell 就能写
反代这件事看着简单,坑都在细节里。三行配置漏一行,整套优化白做。这种知识只能靠踩过 + 系统总结才能记住,希望这篇能让你少踩一次。
—— 别看了 · 2026