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 数。
| location | Req/s | 后端连接数 | 请求数 | TIME_WAIT |
|---|---|---|---|---|
| /none/ | 1552.52 | 15557 | 15557 | 10708 |
| /only/ | 1561.87 | 15662 | 15662 | 10897 |
| /http11/ | 1580.72 | 15841 | 15841 | 10711 |
| /full/ | 4798.18 | 64 | 48038 | 35 |
只写 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 握手。