综合 NGINX upstream keepalive 实测:配了 keepalive 16,后端连接数仍是 15557 条

2026-11-06 21:31:05

NGINX upstream keepalive 实测:配了 keepalive 16,后端连接数仍是 15557 条

实验环境:Ubuntu 26.04 LTS,内核 7.0.0-42-generic,4 vCPU,NGINX 1.28.3 (Ubuntu),wrk 4.1.0,后端是 Python 3.14 写的 HTTP/1.1 测试服务,全部跑在 127.0.0.1 上。默认 ip_local_port_range 为 32768 60999,tcp_tw_reuse=2。

最小可用配置

upstream app_backend {
    server 127.0.0.1:8080;
    keepalive 16;          # 每个 worker 缓存的空闲连接数
    keepalive_timeout 50s; # 要比后端的 idle timeout 短
}
server {
    location / {
        proxy_pass http://app_backend;
        proxy_http_version 1.1;      # 1.29.7 之前必须写
        proxy_set_header Connection "";  # 1.29.7 之前必须写
    }
}

三部分都配齐后,同样的 10 秒压测,后端连接数从约 15600 条降到 64 条,吞吐约 3 倍,TIME_WAIT 从 10708 个降到 35 个。

用 nginx -v 确认版本。NGINX 1.29.7 起 upstream keepalive 默认开启、代理协议默认 HTTP/1.1;发行版打包的版本大多更老。新版本上把这几行显式写出来也没有副作用。

四组对照实验

四个完全相同的后端,各挂在一个 location 下,keepalive 配置的完整程度不同:

  • /none/:keepalive 否,proxy_http_version 否,Connection "" 否
  • /only/:是,否,否
  • /http11/:是,是,否
  • /full/:是,是,是

压测方式:每个 location 跑 wrk -t2 -c20 -d10s。后端自己统计 accept 到的 TCP 连接数,TIME_WAIT 在每轮跑完后立刻用 ss 数。

locationReq/s后端连接数请求数TIME_WAIT
/none/1552.52155571555710708
/only/1561.87156621566210897
/http11/1580.72158411584110711
/full/4798.18644803835

只写 keepalive 16,或者 keepalive + HTTP/1.1,表现和不配 keepalive 完全一样:一个请求一条连接。只有完整配置才复用了连接。

原因在 NGINX 发给后端的内容上。默认它用 HTTP/1.0,并自己加上 Connection: close。单加 HTTP/1.1 没用,因为 NGINX 依旧发 Connection: close,后端响应完就挂断。proxy_set_header Connection "" 的作用就是把那个头去掉。

半配置会引发隐藏错误

常见说法是 keepalive 不配那两个头“什么都不做,不报错,不告警”。如果后端收到 Connection: close 后也在响应头里回一个 Connection: close,这个说法成立。

第一轮用的后端不返回任何头,直接关连接。NGINX 于是认为这条连接可复用,缓存下来,把下一个请求写进后端早已关闭的 socket:

2026/10/30 15:35:01 [error] 19196#19196: *32813 upstream prematurely closed connection while reading response header from upstream, client: 127.0.0.1, server: , request: "GET /http11/x HTTP/1.1", upstream: "http://127.0.0.1:9403/http11/x", host: "127.0.0.1:8401"
  • /only/:16141 个请求,8448 次 prematurely closed
  • /http11/:16135 个请求,8683 次 prematurely closed

wrk 报告的失败请求数是 0。wrk 只发 GET,而 NGINX 默认 proxy_next_upstream error 会把每次失败换到新连接上重试,所以 access log 里是 ups=502, 200,客户端拿到的是 200。换成 POST,或者把 proxy_next_upstream 关掉,这些就是客户端可见的 502。半成品配置不总是无害的:要么配完,要么把 keepalive 删掉。

端口耗尽复现

每条新的 upstream 连接都要占用一个本地临时端口,关闭的 socket 会把端口留在 TIME_WAIT 60 秒。为了不靠大流量也能复现耗尽,实验把端口范围收窄到 500 个,并加了到时自动回滚:

net.ipv4.ip_local_port_range = 40000 40499
net.ipv4.tcp_tw_reuse = 2
--- loopback, no keepalive, 500 local ports
    Non-2xx or 3xx responses: 86034
Requests/sec: 9632.72
    TIME_WAIT=560 errors99=86050
    10315 200
    86050 502

这个 req/s 是假的,绝大多数是立刻返回的 502。错误日志:

2026/10/30 15:39:04 [crit] 19198#19198: *96268 connect() to 127.0.0.1:9401 failed (99: Cannot assign requested address) while connecting to upstream

有两处要注意。这条日志的级别是 [crit] 而不是 [error],只匹配 [error] 的告警规则或 grep 会漏掉它。另外 tcp_tw_reuse=2(默认值)并没有阻止耗尽,即使流量全在 loopback 上。别拿这个 sysctl 当 keepalive 的替代品。

同样 500 个端口范围,换成完整 keepalive 配置:

--- loopback, full keepalive, 500 local ports
Requests/sec: 4784.61
    TIME_WAIT=38 errors99=0 backend conns=130 reqs=95936
    47893 200
    4 499

零端口错误,约 96000 个请求只用了 130 条连接。

按默认端口范围算:约 28000 个端口,TIME_WAIT 60 秒,那么对单个后端 ip:port 新建连接的上限大约是每秒 470 条。

keepalive N 不是连接上限

keepalive 16 限制的是每个 worker 保留的空闲连接,不是活跃连接。用 wrk -c200 打 /full/:

--- keepalive is not a connection cap: 200 concurrent clients
    ESTABLISHED nginx->backend during load: 200
Requests/sec: 4129.57
    ESTABLISHED after load: 64 TIME_WAIT: 74

200 个客户端并发时,到后端就是 200 条 ESTABLISHED;压力停下后剩下 64 条(16 × 4 个 worker)。后端容量按活跃峰值来估,压测结束后常驻的空闲连接数则是 keepalive × worker_processes。真要限制每个后端的连接数,用 server 行上的 max_conns。

相关指令:

  • keepalive N —— upstream 上下文,每个 worker 缓存的空闲连接上限
  • keepalive_requests —— 单条连接处理多少个请求后由 NGINX 关闭(默认 1000)
  • keepalive_timeout —— 缓存的空闲连接保留多久(默认 60s)

1.29.7 之前的版本,least_conn、ip_hash 这类负载均衡指令要写在 upstream 块里 keepalive 的前面。

慢的那次 /full/:Nagle 与延迟 ACK

最早一次 /full/ 比完全不配 keepalive 还慢:

Requests/sec: 486.34 full: TIME_WAIT=0 ESTABLISHED(nginx->backend)=4 backend conns=21 reqs=4885

连接确实复用了(21 条),但每个请求到后端平均约 40 ms。测试后端把响应头和 body 分两次小写发出,且没设 TCP_NODELAY。在复用连接上,Nagle 算法会把第二次写压住,直到第一次写被 ACK,而这个 ACK 又被延迟了。给后端加上 TCP_NODELAY 后,同一份配置直接到 4798 req/s。生产环境的应用服务器大多已经默认开了 TCP_NODELAY。如果开启 keepalive 后延迟稳定跳到 ~40 ms,先查后端的 socket 选项,再怀疑 NGINX。

超时错配:实验室里没复现

流行说法是 NGINX 的 upstream keepalive_timeout 必须短于后端的 idle timeout,否则 NGINX 会复用后端刚关掉的连接,产生偶发的 upstream prematurely closed connection 或 104: Connection reset by peer 502。

实验尝试触发它:两个 upstream 指向会在 1 秒后关闭空闲连接的后端,一个用 NGINX 默认的 60s keepalive_timeout,一个用 500ms。20 个客户端各以约每秒一次的频率发请求(±50 ms 抖动)。两种超时下都是零错误、零重试。

原因在于后端关闭空闲连接时会发 FIN,而 NGINX 会盯着缓存的空闲连接,收到 FIN 就丢弃。竞态只存在于很小的窗口里:NGINX 挑中一条连接、往里写,而 FIN 还在路上。在 loopback 上这个窗口是微秒级;换成真实网络,或者后端用 RST 而不是正常关闭,窗口会更大。

把 NGINX 的超时设得比后端短,仍然是便宜且正确的做法,因为它把这个竞态完全消除。后端 idle keepalive 的默认值可以对照:Node.js http.Server 的 keepAliveTimeout 是 5s,Gunicorn --keep-alive 是 2s,Apache httpd KeepAliveTimeout 是 5s。但别指望用它解释频繁出现的 502——如果看到大量 prematurely closed,更可能是上面说的静默关闭,或者后端在崩。

在自己的环境上验证复用

ss -Htan state established '( dport = :8080 )' | wc -l
ss -Htan state time-wait   '( dport = :8080 )' | wc -l

复用生效:ESTABLISHED 基本保持平稳,TIME_WAIT 维持在低位。没生效:TIME_WAIT 随请求速率一路爬升。

nginx -T 2>/dev/null | grep -nE "upstream|keepalive|proxy_http_version|Connection"

stub_status 在这里没用:它的 Waiting 计数是空闲的客户端连接,不是 upstream 连接。

WebSocket 的 location 不要硬写 Connection "",用 map 让 upgrade 请求照常工作:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      '';
}

然后 proxy_set_header Connection $connection_upgrade;。

复用完没问题,502 还在

先看后端容量、具体的错误信息,以及 NGINX 自身的限制(too many open files)。

FAQ:upstream 块里的 keepalive_timeout 会影响客户端吗?不会。写在 upstream 里它管的是到后端的空闲连接;写在 http 或 server 上下文里的 keepalive_timeout 是另一条指令,管客户端连接。keepalive 该配多大?实验里每个 worker 16,在 4800 req/s 下复用正常。tcp_tw_reuse 能让 keepalive 变得不必要吗?不能。upstream keepalive 对 HTTPS 后端有用吗?有,复用的连接同样省掉 TLS 握手。

复制全文 生成海报 nginx 运维 反向代理 502

推荐文章

程序员茄子在线接单