Nginx upstream keepalive 漏一行配置,QPS 直接砍 6 倍

新搭 Nginx 反代,QPS 2000 后端就 502。本文讲清楚 Nginx 默认到 upstream 是短连接、三件套配置缺一不可、非幂等重试的坑、WebSocket/SSE/gRPC 反代差异,附完整配置 + CI 自检脚本 + 6 条必读规则。

新搭的 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;
    }
}

三件套:

  1. keepalive 128 —— 没这行就没有连接池
  2. proxy_http_version 1.1 —— 没这行 Nginx 走 HTTP/1.0,即使 upstream 支持 keep-alive 也用不上
  3. 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 条

  1. 三件套必齐:keepalive + proxy_http_version 1.1 + proxy_set_header Connection ""
  2. 非幂等请求别重试:除非业务幂等
  3. 超时三阶梯:connect 3s + send 10s + read 30s
  4. WebSocket / SSE / gRPC 单独 location:配置完全不同
  5. 日志记 upstream_response_time:监控真实后端延迟
  6. CI 自检脚本:语法 + 必备指令检查,5 行 shell 就能写

反代这件事看着简单,坑都在细节里。三行配置漏一行,整套优化白做。这种知识只能靠踩过 + 系统总结才能记住,希望这篇能让你少踩一次。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

Docker 镜像从 1.2GB 瘦到 80MB:我们做的 7 步优化和 10 条黄金法则

2026-5-19 10:41:15

技术教程

pandas 内存从 8GB 压到 800MB:60 万行 CSV 处理的 7 步优化

2026-5-19 10:45:55

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索