你可能见过这种情况:接口逻辑只做了一件小事,查一条数据、拼一段 JSON,业务代码跑完只要几毫秒,可整条请求还是慢。问题往往不在业务本身,而在每次请求都重新建连接、重新握手、重新热身。
这篇文章只讲一个核心思路:有些性能优化,不是把计算做得更快,而是拿一部分常驻资源,去换掉反复建立连接的等待时间。常见手段就是 keepalive、连接池、长连接复用。它们特别适合短请求频繁、TLS 握手成本明显的场景;代价也很直接:连接不马上关,就会长期占用 FD 和内存。
如果你是初学者,先记一句人话版结论:业务像去窗口办事,连接像窗口本身。每来一个人都临时搭个窗口,当然慢;把窗口留着反复用,当然快;但窗口开多了,场地和人手也会被占住。
先说人话:它到底在优化什么
一次新请求如果要新建连接,通常不只是发一包数据那么简单。你得先把路打通,再确认对方是谁,再协商怎么加密,最后才能正式聊业务。
-
TCP 连接:人话就是先把一条可靠通道拉起来,像先把电话接通。 -
TLS 握手:人话就是正式说事前,先核验身份、商量加密规则,像进办公室前先刷门禁、验工牌。 -
长连接复用:人话就是这条通道别刚用完就拆,后面的请求还能接着走。 -
连接池:人话就是不是只保留一条通道,而是准备一批能借能还的通道,谁来办事谁先拿。
对短请求来说,真正的业务处理可能只占很小一段时间。反而是建连接和握手,把总耗时拉长了。尤其走 HTTPS、数据库加密连接、RPC 加密链路时,这个现象会更明显。
请求到来
-> 先看有没有可复用连接
-> 没有
-> TCP 建连
-> TLS 握手
-> 发送业务请求
-> 接收响应
-> 关闭连接 或 放回池子
-> 有
-> 直接发送业务请求
-> 接收响应
-> 放回池子,等待下次复用
动作建议:先把请求总耗时拆开,分成建连、握手、业务处理三段看,再决定值不值得上复用。
为什么短请求场景里,建连成本会特别扎眼
因为短请求本来就事少。
假设一个服务只是去另一个服务拿一个很小的配置值,请求体 2KB,服务端真正处理只要 3 到 5 毫秒。如果每次都新建 HTTPS 连接,除了 TCP 建连,还要做 TLS 握手和加密初始化。哪怕现在常见的 TLS 1.3 已经比老版本更快,首次握手仍然要额外往返和计算,这些开销在小请求里会显得很大。
生活类比一下:你只是去楼下便利店买一瓶水,结果每次都要重新登记身份证、领访客证、过安检,再进门付款。水拿到手很快,慢的是前面的手续。
一个常见迷惑是:明明单次接口返回数据很少,为什么 P95 延迟还是难看?答案往往是,请求太短,建连成本没被摊薄。请求越碎、频率越高、链路 RTT 越大,这种手续费越明显。
一个小例子:
-
页面首屏要调 8 个小接口,每个接口真正业务处理 5 毫秒以内。
-
如果浏览器、网关、服务之间连接复用得好,很多时间能直接省掉。
-
如果每层都喜欢新建连接,那你会感觉系统像一直在门口排队,真正干活反而没几秒。
keepalive、连接池、长连接复用,到底各管什么
很多初学者会把这三个词混成一团。最省心的记法是:长连接复用是目标,keepalive 是让连接别太快死,连接池是把一批连接管起来重复用。
1. keepalive:请求完先别急着断
先讲人话:办完一件事,先别把电话挂死,留几秒,说不定下一个请求马上又来了。
术语版:keepalive 通常指让已经建立好的连接在空闲一小段时间后仍然保持可用,这样后续请求可以继续复用它,而不是重新建连。很多 HTTP/1.1 实现天然支持持久连接,但能不能真正复用,还取决于客户端是否复用同一个连接管理器、空闲超时怎么配、代理层是否配合。
生活类比:打车平台给你保留几分钟会话,不用每点一次都重新登录。
小案例:网关连续把 100 个小请求转发到同一后端服务,如果每次转发后马上断开,后端要反复建连;如果保留空闲连接,下一个请求就能直接复用,延迟通常更稳。
有个容易踩的坑:今天说的 keepalive,重点是连接复用,不是操作系统里那个定时探测对端是否活着的 TCP keepalive。名字很像,但关注点不同。还有,如果你已经在用 HTTP/2 或 HTTP/3,也别死盯着 Connection: keep-alive 这个头,协议本身就有自己的复用方式。
2. 连接池:不是留一根线,而是养一批线
先讲人话:前台准备一盒工牌,谁来办事先借一张,用完归还;不是每来一个人都现场造工牌。
术语版:连接池会维护一组已经建立好的连接,控制最大总连接数、最大空闲连接数、空闲超时、连接生命周期等。请求来了先借,结束了再还。常见于数据库访问、HTTP 客户端、RPC 客户端。
生活类比:餐厅高峰期提前备好餐具,而不是客人坐下后再去现洗现配。
小案例:订单服务要频繁查数据库。如果每次查库都重新登录数据库,登录和鉴权会反复发生;如果用连接池,查询线程拿到可用连接就开查,用完归还,吞吐和稳定性通常都会更好。
但注意,连接池不是魔法袋。池子空了,请求就得等;池子太大,数据库或后端服务就会被你占满。
3. 长连接复用:它是总策略,不是单独一个按钮
先讲人话:同一个窗口可以连续办很多单,别办完一单就把窗口拆了再重搭。
术语版:长连接复用强调的是同一条连接,或者同一批已建立连接,被多个请求反复使用。keepalive 和连接池,都是在帮你完成复用这件事。HTTP/2 的多路复用,本质上也是把更多请求装进更少的连接里。
生活类比:不是每次快递到小区都临时开个新门,而是用现有门禁和通道连续放行。
小案例:一个应用全局只创建一个 HTTP client 和一个数据库句柄,让它们在进程生命周期内负责复用连接;这通常比每次请求里临时 new 一个 client 更靠谱。
| 手段 | 先讲人话 | 最适合的场景 | 主要收益 | 主要代价 | 常见坑 |
|---|---|---|---|---|---|
| keepalive | 先别挂电话 | 同一服务反复发短请求 | 少建连、少握手、延迟更稳 | 空闲连接常驻 | 超时不匹配,连接被对端先关掉 |
| 连接池 | 备一批能借能还的线 | 并发高、访问频繁 | 复用稳定、统一限流和管理 | 占用更多 FD 和内存 | 池子配太大,反压失控 |
| 长连接复用 | 同一窗口连续办单 | HTTP、RPC、数据库等多种连接场景 | 把建连成本摊薄 | 需要更细的生命周期管理 | 误以为长连接就是永不关闭 |
动作建议:如果你是在同一进程里频繁访问同一后端,先检查有没有正确复用 client;并发明显上来后,再看是否需要连接池。
哪些场景最值得用这套思路
不是所有请求都值得为它保留一堆连接。最值的是下面几类:
场景 1:短请求很多,而且很频繁
比如一个页面要连续拉很多小接口,或者一个微服务要不停访问另一个微服务。请求本体小、处理逻辑轻,建连开销就很容易压过业务开销。
场景 2:TLS 握手成本明显
只要用了加密连接,首次握手就不是白送的。它通常包含额外往返、证书校验、密钥协商和加解密初始化。业务本身越短,这笔入场费越刺眼。
场景 3:数据库或缓存访问非常碎
每次只是查一条记录、写一个状态、读一个配置,但访问频率高。这个时候,连接池几乎是常规配置,而不是锦上添花。
场景 4:服务拆得比较细,内部调用很多
一个外部请求进来,内部扇出调用 5 个、10 个甚至更多服务。如果每一跳都喜欢新建连接,总延迟会像滚雪球一样堆上去。
| 条件 | 优先开 keepalive | 需要连接池 | 不必太激进 |
|---|---|---|---|
| 同一目标服务会被反复请求 | 是 | 视并发而定 | 否 |
| 请求很短,真正业务只要几毫秒 | 是 | 常常需要 | 否 |
| 有 TLS 或数据库登录成本 | 是 | 常常需要 | 否 |
| 访问频率低,偶尔才来一次 | 否 | 往往不必 | 是 |
| 后端连接数上限很紧 | 谨慎 | 谨慎 | 是 |
| 你还没有任何监控数据 | 先小范围试 | 先小范围试 | 是 |
动作建议:先判断你的系统是不是建连主导型延迟,再决定复用的力度,别一上来把池子开到很大。
为什么说这是拿资源换时间
这句话非常关键。很多优化文案只说快,不说代价,容易把新手带沟里。
连接保活和连接池之所以能快,是因为连接不再立刻释放,而是常驻一段时间等下一次使用。常驻就意味着资源要一直占着。
1. FD 会被占住
FD 可以先把它理解成进程手里的一张资源票据。打开一个文件、一个 socket、一个连接,通常都要占一个或多个 FD。连接越多、空闲时间越长,占用的票据就越多。
2. 内存会被占住
连接不是一个空名字。它背后通常带着收发缓冲区、协议状态、TLS 会话状态、对象元数据等。连接池越大,空闲连接越多,内存就越容易被吃掉。
3. 后端承压方式会改变
你觉得自己只是多保留了一些连接,但对数据库、缓存、RPC 服务来说,这些连接都是真的。池子过大时,后端可能被大量空闲连接占住,新的有效请求反而更难进来。
4. 生命周期管理会更复杂
连接不是留着就完事。它可能过期、被对端先断掉、被负载均衡器回收、长时间空闲后失效。你还得考虑空闲超时、最大生命周期、健康检查、借出超时、重试策略。
一句不太严肃但很准确的话:这不是白嫖,这是包月。你省下了反复开户的时间,就得接受包月座位一直占着。
一次完整走读:看看它到底怎么把时间省出来
下面做一个可复现的思考实验,数据是示意值,不是所有系统的固定答案,但非常适合理解原理。
场景设定
-
用户服务每秒收到 100 个请求。
-
每个请求都要调用一次权限服务,再查一次数据库。
-
单次权限查询真正业务处理只要 5 毫秒。
-
数据库单次查询真正执行也只要 4 毫秒。
-
服务之间走加密连接,网络 RTT 不算极低。
改造前:每次都新建
-
用户请求进来。
-
调权限服务时,先新建连接、做握手,再发业务请求。
-
查数据库时,再新建连接、登录、查询、关闭。
-
这个过程每秒重复很多次。
结果会是什么?
-
业务明明很轻,但总延迟不低。
-
突发流量一来,大量新建连接会把 CPU 和网络都拖得更忙。
-
监控里可能看到 connect time、TLS handshake time、pool miss 一类指标偏高。
改造后:共享 client,加连接池
-
进程启动后,创建可复用的 HTTP client 和数据库连接池。
-
请求进来时,先借已有连接;没有空闲时,再按上限新建。
-
请求结束后,不直接关闭,而是放回池子。
-
连接空闲太久或活太久,再被回收。
这样做之后,真正省掉的不是业务代码时间,而是大量重复手续。
| 指标 | 改造前 | 改造后 | 变化原因 |
|---|---|---|---|
| 每秒新建连接次数 | 很高 | 明显下降 | 后续请求直接复用已有连接 |
| 平均延迟 | 偏高且波动大 | 更低且更稳 | 少了建连和握手抖动 |
| P95 延迟 | 容易难看 | 往往更容易收敛 | 高频小请求不再反复走入场流程 |
| FD 占用 | 较低 | 上升 | 连接常驻等待复用 |
| 内存占用 | 较低 | 上升 | 池中空闲连接要保存状态 |
| 后端连接压力 | 瞬时建连压力大 | 常驻连接压力更大 | 压力形态从频繁开关变成持续占用 |
动作建议:先做一轮小流量验证,把延迟收益和 FD、内存、后端连接数一起看,不要只盯着接口快了多少。
新手最容易踩的 6 个坑
坑 1:每次请求里都新建 client
这是最常见的反模式。你以为只是写起来顺手,实际上等于主动放弃连接复用。
坑 2:只会把池子调大,不会调边界
连接池不是越大越好。过大的池子会放大后端压力,还可能掩盖慢查询、慢接口和下游瓶颈。
坑 3:只配最大连接数,不配空闲超时和生命周期
这样容易让一堆老连接一直躺着,占资源,还可能在你不注意时变成半死不活的连接。
坑 4:客户端和服务端超时不对齐
客户端以为连接还能复用,服务端其实早就关了。下一次借出来就报错,表现成偶发失败、偶发重试、偶发慢。
坑 5:借了连接不及时归还
比如数据库查询结果没正确关闭,连接就回不到池子里。表面看池子很大,实际可用连接越来越少。
坑 6:把长连接理解成永不关闭
长连接不是永生连接。它只是让连接在合理时间内多用几次,该回收时还是要回收。
该怎么落地,初学者可以直接照着做
如果你现在就想把这个思路用起来,可以按这个顺序:
- 先确认瓶颈
看连接建立时间、TLS 握手时间、请求总耗时、P95/P99,而不是只看平均值。
- 先做最小改动
先把会重复创建的 HTTP client、数据库句柄改成进程级共享对象,让复用先发生。
- 再补连接池边界
配好最大连接数、最大空闲连接数、空闲超时、最大生命周期,不要只配一个上限。
- 压测时一起看两组指标
一组看延迟和吞吐,另一组看 FD、内存、后端连接数。快了但资源爆了,不算真正优化。
- 最后再细调
如果请求已经走 HTTP/2、多路复用很充分,或者访问频率本来就不高,就不要过度优化。
最后记住 5 句话
-
先
check你的慢,到底慢在业务处理,还是慢在建连接和握手。 -
先
measure建连次数、握手时间、池等待时间、FD 占用,再谈优化。 -
学会
choose场景:短请求频繁、TLS 成本明显时,复用最值钱。 -
一定要
set好连接数、空闲超时、最大生命周期,别只会把池子开大。 -
记得
verify收益和代价一起看,别把延迟省下来了,却把内存和后端连接压垮了。
当你把这件事想明白,就会发现:keepalive、连接池、长连接复用,表面上是在调连接,底层其实是在做一笔性能上的交换。你拿常驻资源换掉重复手续,所以系统能更快;但资源是要付租金的,所以配置必须克制、监控必须跟上。对初学者来说,这比背参数更重要。