首页 游戏 软件 排行 专题

反向代理开了keepalive反而变慢:200个空闲连接的代价

来源: 本站整理 日期:2026/11/4 23:19:29

去年秋天我们把一个单机的 Node 服务拆到两台 8 核机器上,前面放 Nginx 做反向代理。当时的想法很朴素:Nginx 到后端是短连接,每次请求建一条、走完就断,干净利落。可上线一周后压测的同事跑来一句"你这代理比不代理还慢",我第一反应是他压测脚本有问题,结果不是。

抓包和翻配置花了我大半天,最后定位到一行我以为"无害"的配置:keepalive 64;。开了 keepalive 之后,Nginx 确实会复用连接,握手次数确实降下来了,QPS 也确实上去了。但连接池里同时躺着两百多条空闲连接,每一条都要等着超时。真正的问题不是"多开了一条连接",而是"这条连接什么时候才肯死"。

这篇文章就把这件事从头到尾捋一遍:keepalive 到底省了什么、又多付了什么,为什么它会让反向代理变慢,以及最后我是怎么把这 200 条空闲连接压回个位数的。

如果你正在排查反向代理的连接复用问题,或者想换一台网络更稳的机器把上游服务重新部署一遍,可以先看看这几家的线路与配置说明:恒创科技(香港及亚太精品线路) 与 rabisu(全球高性能云服务器)

一、keepalive 省掉的是握手,付出去的是内存和等待

先说结论:keepalive 本身不是慢的代名词,它省下的是 TCP 三次握手加 TLS 握手的开销。正常情况下,一个客户端到 Nginx 的请求,Nginx 再向后端发起请求,这一段是完整的一次建连。对于并发不高的场景,这笔开销可以忽略;但当并发上千、上万的时候,成千上万次握手累积起来是很可观的数字。所以 keepalive 的方向是对的。

问题出在"复用"这件事需要一块资源来承载:Nginx 必须为每个 upstream 维护一个连接缓存池(官方叫 keepalive 指令下的 cache),把空闲的后端连接缓存起来,等下一个请求来的时候直接拿一条现成的用。这时候有两个数字开始互相打架。

第一个数字是缓存池的大小,也就是 keepalive 后面跟的那个数。它代表每个 worker 进程最多缓存多少条空闲连接到某个后端节点。注意是每个 worker,不是每个进程组。如果你的 Nginx 开了 8 个 worker,那实际上限就是 8 乘以这个数。我当时写的是 64,理论上 8 个 worker 就是最多 512 条空闲连接。这个数在文档里写得很清楚,但我当时根本没意识到它的量级。

第二个数字是空闲连接的存活时间,也就是 keepalive_timeout。默认是 60 秒。这个值的意思是:一条连接空着没人用,超过 60 秒就主动关掉。听起来很合理,但它是"每个后端节点各 60 秒",也就是说如果你的流量有波峰波谷,波峰过后这批连接会在 60 秒内陆续到期,而不是同时到期。

问题就在这儿。连接在缓存池里躺着,等着被复用,但在真正被复用之前,它占着 fd、占着 socket 缓冲区、还占着后端那边的工作线程或连接槽位。后端如果是个 Java 应用(Tomcat 默认 maxThreads 200)或者 PHP-FPM(默认 worker 数有限),这些"其实没在干活但也没死"的连接一样会消耗它的并发资源。

Nginx upstream keepalive 连接池中堆积空闲连接的示意图

二、我那 200 条空闲连接到底是怎么来的

先放一张当时的监控图。当时我用 stub_status 加了一个自研的连接池指标,每 10 秒打一次点。可以看到 keepalive 池里的连接数在流量回落之后不是掉到 0,而是稳定停在 200 上下,像一条不会回到零的直线。

原因我一开始想错了。我以为是某个后端实例挂掉了,Nginx 一直往它上面发请求失败,连接被 mark 成不可用却没释放。后来发现不对,是两个配置叠加出来的结果。

第一个是 keepalive_timeout。后端应用自己设的空闲超时是 30 秒,而 Nginx 这边是 60 秒。也就是说:一条连接在后端那边已经被服务端悄悄关掉了,Nginx 这边还浑然不觉地把它当作"可复用的空闲连接"留着。等到下一个请求正好挑中这条连接拿出去用的时候,就会撞上 upstream prematurely closed connection,日志里直接报 502。

第二个是 keepalive_requests。这个参数我压根没配,Nginx 新版本默认会带上一个比较保守的上限,但老版本如果没开,某些情况下会复用一条已经传了海量数据的连接。我后来把它显式设成 1000,超了就重建。

还有第三个因素,就是请求分布不均。Nginx 默认是轮询(round-robin)把请求分给 upstream 列表里的每一个后端。如果我有两个后端,客户端的实际访问分布却不是五五开比如某个接口被调用得特别频繁,而同一个上游节点上还有另一堆冷接口那热的那条链路上缓存池会一直很满,冷的那条链路上缓存池则几乎全是躺着没人用的死连接。我把 least_conn 换上去之后,空闲连接总数立刻从 200 掉到 30 出头。

upstream 调度策略对连接池空闲连接数量的影响示意图

三、怎么查:三个最实用的命令

排查这类问题不需要什么复杂工具,系统自带的就够。

第一个是 ss,直接看本机到后端端口的连接状态分布。ss -tan | grep 8080 | awk '{print $1}' | sort | uniq -c,你会看到 ESTAB 那一栏有多大。如果空闲连接长期维持在几百条,那基本可以确认就是 keepalive 池子在囤积。

第二个是 netstat -s 里的 TIME_WAIT 计数。TIME_WAIT 特别多,说明连接在频繁建立和销毁,那多半是 keepalive 根本没生效,可以回去检查 proxy_http_version 是不是 1.1、有没有清掉 Connection 头这两个老坑。

第三个是我自己加的一个 stub_status 之外的自定义指标,统计 upstream 各节点的活跃连接与空闲连接。真正定位到"是哪个后端节点在囤积空闲连接",靠的就是这个粒度。stub_status 只给全局总数,看不出节点差异,这一步是我当时绕了弯路的地方。

使用 ss 和 netstat 命令排查空闲连接的终端操作场景

四、我最后改了什么

调完之后我的最终配置大概是这样,我把它拆开说每一项对应的现象。

keepalive_timeout 60s 改成了 10s。 这是最见效的一项。后端应用的空闲超时是 30 秒,我把 Nginx 这边调得比它短,让 Nginx 主动先关。这样一来缓存池里的连接都是"还活着且后端也认为还活着"的,命中率提上去以后,空闲连接数量反而下降了因为连接周转快了,不用同时屯那么多条。

keepalive 64 改成了 32。 这是我一开始想都不敢想的调整。按理说缓存池越大复用率越高,但实测下来池子大了之后,命中率提升的收益远小于"大量空闲连接带来的资源占用"。32 是个折中值,不同业务差别很大,别照抄。

proxy_http_version 1.1 加 clear Connection 头。 这两个是 keepalive 能生效的前提,缺一个 keepalive 就是摆设。写反了会看到后端日志里全是短连接,Nginx 侧连接数不降反升。

upstream 调度改成 least_conn。 前面说过,这是解决节点间负载不均的关键。改完之后连接池的分布均匀了,热点节点不再把池子撑爆。

keepalive_requests 显式设为 1000。 避免复用一条承载了过多请求的老连接,也避免个别请求撞上后端主动关闭连接导致 502。

另外还有一点容易被忽略:Nginx 侧的 keepalive 超时必须严格小于后端应用的空闲超时,中间留出余量。这条规则比任何参数微调都重要,我前面 502 的根因就是这两边不一致。

反向代理网关与后端服务节点连接关系及错误来源的等距示意图

五、还有一种可能:根本不该开 keepalive

折腾完这一轮我突然意识到一个前提问题:不是所有场景都适合开 keepalive。

如果你的后端是一个无状态服务、请求之间完全独立、而且 QPS 并不高,那么连接建立的开销在总耗时里占比很小,keepalive 带来的收益有限,反而引入了空闲连接、连接超时不一致、502 这些新问题。这种情况下老老实实短连接,或者干脆用 HTTP/2、HTTP/3 这种多路复用的协议来解决问题,比在 keepalive 上做微调要省心得多。

keepalive 真正划算的场景是:并发高、握手成本占比明显、后端是有状态或连接敏感的(比如数据库连接池代理、Redis、消息队列消费进程)。这时候每一个 RTT 都要抠着用。

所以我的建议顺序是:先用工具量一下,别凭感觉。第一步看 ss 里的连接分布,判断空闲连接到底有没有异常堆积;第二步对比 Nginx 的 keepalive_timeout 和后端应用的空闲超时,两边差太远就一定会有 502;第三步再考虑改 keepalive 数值和调度策略。如果量完发现根本没这些问题,那就别折腾了。

回头看这次故障,我犯的是一个特别基础的错误:加了一个参数,却没有理解它背后的资源占用模型。keepalive 不是"打开就赚"的开关,它是一笔预付的成本你用内存和等待时间,换掉了握手的开销。这笔账要算清楚,比盲目调参数重要得多。

玩家留言 跟帖评论
查看更多评论
相关文章
猜你喜欢
同类下载