HTTP keep-alive 二三事
Category(分类): NET Protocol Status: 未知
本文尽量保留原文关于 HTTP 长连接、客户端连接池、Tomcat、TIME-WAIT 和抓包实验的内容,并补充 HTTP/2、HTTP/3 以及当前 Spring Boot/Tomcat 的配置说明。
一、HTTP keep-alive 是什么?
HTTP keep-alive 更准确的名称是 HTTP persistent connection(HTTP 持久连接)。它表示在一个 HTTP 连接上完成一次请求—响应后,不立即关闭底层连接,而是允许后续请求继续复用这个连接,从而减少反复建立和关闭连接的成本。
一次新的 HTTP/1.1 TCP 连接通常要经历:
- TCP 三次握手;
- 如果是 HTTPS,还要进行 TLS 握手;
- 发送 HTTP 请求;
- 接收 HTTP 响应;
- 连接关闭时进行 TCP 挥手。
如果连续请求多个资源,每次都建立新连接,就会重复承担握手、慢启动、加密协商和连接关闭的开销。持久连接可以减少这些重复成本。
HTTP 持久连接和 TCP SO_KEEPALIVE 不是一回事:
- HTTP persistent connection:复用 HTTP 连接发送多个请求;
- TCP keepalive:操作系统通过 TCP 探测报文检测连接是否仍然可达。
本文主要讨论前者。
二、HTTP/1.0 和 HTTP/1.1 的区别
HTTP/1.0
HTTP/1.0 默认通常在一个请求—响应完成后关闭连接。部分 HTTP/1.0 客户端和服务端通过下面的首部协商持久连接:
Connection: keep-alive
但客户端发送这个首部不代表服务端一定同意。服务端需要在响应中明确保持连接,并且正确提供响应消息边界;否则客户端仍然需要等待连接关闭来判断响应结束。
Keep-Alive 首部中的超时、最大请求数等参数主要属于历史扩展或实现约定,不应当当作所有 HTTP/1.0 实现都支持的统一标准。
HTTP/1.1
HTTP/1.1 默认使用持久连接。任意一方可以通过下面的连接级首部表示当前连接在本次消息完成后关闭:
Connection: close
因此,HTTP/1.1 通常不需要显式写 Connection: keep-alive。真正决定连接能否复用的关键还包括:
- 响应是否有明确的消息边界;
Content-Length是否正确;- 是否使用最终的
Transfer-Encoding: chunked; - 是否遇到不允许携带响应体的状态码;
- 连接是否发生协议错误或超时;
- 客户端和服务端是否仍然愿意复用连接。
如果响应既没有 Content-Length,也没有 chunked 编码,客户端可能只能通过服务端关闭连接判断响应体结束,这种响应通常不能继续复用连接。
HTTP/2 和 HTTP/3
现代 HTTP 不能只讨论 HTTP/1.0 和 HTTP/1.1:
- HTTP/2 通常在一条 TCP 连接中复用多个并发 Stream;
- HTTP/3 在 QUIC 连接上运行,QUIC 使用 UDP,不是 TCP;
- HTTP/2 和 HTTP/3 的连接默认具有持久性和多路复用能力;
- HTTP/2、HTTP/3 不应使用 HTTP/1.1 的
Connection: keep-alive、Connection: close、Keep-Alive等连接级首部; - HTTP/2/3 仍然会因为空闲超时、GOAWAY、服务重启或网络变化关闭连接。
所以,现代浏览器中的“连接复用”可能是 HTTP/1.1 持久连接,也可能是 HTTP/2 多路复用或 HTTP/3 QUIC 连接。
三、用还是不用,这是个问题
keep-alive 技术创建的目的,就是在多次 HTTP 请求之间重用同一个连接,从而减少创建和关闭连接的开销,包括握手延迟、CPU 消耗、TLS 协商成本和 TCP 慢启动影响。
但是天下没有免费的午餐。如果客户端和服务端在请求完成后仍然保留连接,服务端仍需要为这个连接保留一定资源,例如:
- Socket 和文件描述符;
- 连接对象和读写缓冲区;
- TLS 会话状态;
- HTTP/2/3 的 Stream 和流量控制状态;
- 连接池槽位;
- 监控、限流和认证状态。
这些资源不一定等于一个长期占用的业务线程。以阻塞式 BIO 实现为例,空闲连接可能占用处理线程;NIO、事件循环和异步实现通常不会为每个空闲连接占用一个业务线程,但仍然需要消耗文件描述符、内存和事件循环资源。因此不能简单说 NIO“没有资源占用问题”。
如果客户端和服务端确实会进行多次通信,开启连接复用通常是更好的选择,例如:
- 浏览器加载 HTML、CSS、JavaScript 和图片;
- 微服务调用同一个或少数几个下游服务;
- API 客户端批量访问同一主机;
- 使用 HTTP/2/HTTP/3 进行多路复用。
但连接池不应无限增大。过多空闲连接会造成服务端连接数、代理连接数和内存压力,还可能导致负载分布不均。实际需要根据请求频率、并发量、服务端限制、负载均衡策略和空闲超时配置设置连接池参数。
短连接与 TIME-WAIT
在一些 TPS/QPS 很高的 REST 服务中,如果每次请求都使用短连接,短时间内可能创建大量 TCP 连接。主动关闭连接的一方通常会进入 TIME-WAIT,本地临时端口和连接四元组在一段时间内不能立即用于相同连接,这可能造成临时端口耗尽或连接建立失败。
经典 TCP 教材常用 2*MSL 描述 TIME-WAIT 时长,但实际 MSL、定时器和端口复用行为由操作系统实现和配置决定,并不是一个所有系统都固定的秒数。进入 TIME-WAIT 的也不一定总是客户端,而是通常由主动关闭的一方进入。
连接复用可以减少握手和主动关闭的次数,但不能保证完全没有 TIME-WAIT。还应结合以下手段:
- 使用合理的 HTTP 连接池;
- 避免每个请求都主动关闭连接;
- 调整临时端口范围和系统网络参数时谨慎评估;
- 监控连接建立失败、TIME-WAIT 数量和连接池耗尽情况;
- 使用幂等设计,避免因重试造成重复业务操作。
四、客户端如何开启?
现在多数 HTTP 客户端默认会尝试使用连接复用,但连接是否真的复用还取决于响应是否完整、连接是否被关闭、连接池是否有空闲连接、目标主机是否相同以及协议版本。
浏览器
现代浏览器通常会自动管理 HTTP/1.1 连接池,并在 HTTP/2 或 HTTP/3 中进行连接和 Stream 复用。浏览器还会根据最大并发连接数、域名、代理、网络状态和资源优先级动态调整策略。
原文提到的 IE6 只具有历史参考意义,不能代表当前浏览器行为。
Java HttpURLConnection
Java 8 的 HttpURLConnection 通常默认启用 HTTP 持久连接,并维护连接缓存。原文提到的“默认保留 5 个连接”属于特定 JDK 实现和版本中的历史默认值,不应当视为所有 Java 版本的固定规则。
现代 Java 应优先根据实际使用的 HTTP 客户端和版本查看连接池配置,例如 Java 11+ HttpClient、Apache HttpClient、OkHttp 等实现的连接池和 HTTP/2 策略并不完全相同。
Apache HttpClient
Apache HttpClient 4.x 的历史默认值常被概括为:每个目标地址最多 2 个连接,连接池总数最多 20 个。但这属于特定版本默认配置,HttpClient 5 和应用自定义连接池可能完全不同。生产环境应显式配置:
- 每个路由的最大连接数;
- 连接池最大总数;
- 连接建立超时;
- 从连接池获取连接的超时;
- 响应读取超时;
- 空闲连接和过期连接清理策略。
Python requests
Python requests 使用同一个 Session 时,可以通过 urllib3 连接池复用连接:
import requests
with requests.Session() as session:
session.get('https://example.com/a')
session.get('https://example.com/b')
直接重复调用 requests.get() 不应当等同于长期复用同一个 Session。实际是否复用还取决于响应是否被完整读取、连接是否可复用以及服务器是否关闭连接。
Feign
Feign 本身主要定义声明式 HTTP 客户端接口,实际连接复用能力取决于底层 HTTP 客户端。使用微服务调用时,通常应明确选择并配置底层客户端和连接池,而不是只依赖 Feign 默认行为。
五、服务端如何实现
不同服务端对持久连接的实现方式不同。服务端通常需要同时处理:
- 连接建立和关闭;
- HTTP 请求解析;
- 响应消息边界;
- 空闲连接超时;
- 单连接最大请求数;
- 连接池、文件描述符和内存限制;
- 代理和负载均衡器的超时;
- HTTP/2/3 的 Stream 和连接关闭。
Tomcat NIO 的历史源码示例
原文以 Tomcat 9.0.22 的 NIO 模式源码说明连接状态。这个思路仍有参考价值,但类名、字段和实现细节会随 Tomcat 版本变化,不能把源码片段当成稳定 API。
原文提到的处理逻辑大致是:
NioEndpoint#SocketProcessor根据内部状态决定是否关闭 Socket;Http11Processor#service在连接可继续读取时返回OPEN或相关状态;- Poller 在连接空闲超时后处理关闭事件。
例如旧版本中可以看到类似逻辑:
if (state == SocketState.CLOSED) {
poller.cancelledKey(key, this.socketWrapper);
}
以及:
} else if (this.openSocket) {
return this.readComplete ? SocketState.OPEN : SocketState.LONG;
} else {
这些代码只用于理解 Tomcat 如何把连接交给 NIO Poller 管理,不建议应用直接依赖内部源码。
Spring Boot 和 Tomcat 配置
对于当前 Spring Boot 内嵌 Tomcat,建议区分连接建立超时和 Keep-Alive 空闲超时,例如:
server.tomcat.connection-timeout=20s
server.tomcat.keep-alive-timeout=60s
server.tomcat.max-keep-alive-requests=100
server.tomcat.connection-timeout:等待请求行或请求数据的时间;server.tomcat.keep-alive-timeout:已有连接等待下一个 HTTP 请求的空闲时间;server.tomcat.max-keep-alive-requests:单个持久连接允许处理的最大请求数。
具体配置名称和默认值与 Spring Boot、Tomcat 版本及连接器类型有关。Tomcat 的 connectionTimeout、keepAliveTimeout、HTTP/2 配置和代理超时也不是同一个概念,不能简单地把所有超时都称为 Keep-Alive 超时。
代理链路中还应检查 Nginx、CDN、负载均衡器的:
keepalive_timeout;proxy_http_version;proxy_read_timeout;- 上游连接池;
- HTTP/2/HTTP/3 配置。
六、抓包实验
抓包之下,连接复用的过程会更直观。下面的图片是 HTTP/1.1 场景的历史 Wireshark 截图,不能直接代表 HTTP/2 或 HTTP/3 的报文形式。
使用持久连接

第二次 HTTP 请求通常不会重新创建 TCP 连接:
- 没有新的 TCP SYN;
- 使用之前建立的连接;
- HTTP/1.1 请求依次复用连接;
- HTTP/2/3 可能是在同一连接中创建或复用不同 Stream。
原文截图中服务端在默认约 60 秒后发送 FIN,这只是当时服务端和配置下的现象。实际关闭时间取决于 Tomcat、Nginx、代理、负载均衡器、客户端和操作系统配置,不能推广为 HTTP 的固定规则。
不使用持久连接

如果请求或响应使用:
Connection: close
或者服务端无法提供可复用的消息边界,那么当前请求完成后连接可能被关闭,下一次请求需要重新建立连接。重新建立连接会再次产生握手成本,也可能增加 TIME-WAIT 和临时端口压力。
抓包时的注意事项
- TCP 数据包边界不等于 HTTP 请求或响应边界;
- HTTP/1.1 可以看到文本请求行和 TCP SYN/FIN;
- HTTPS 场景下 HTTP 内容通常被 TLS 加密,抓包工具只能直接看到 TLS 记录;
- HTTP/2 关注 Stream、Frame、SETTINGS、GOAWAY 等;
- HTTP/3 关注 QUIC 包、Stream 和连接级关闭,而不是 TCP SYN/FIN。
七、HTTP 持久连接的实践建议
- 对同一目标主机存在多次请求时,优先使用连接池和连接复用;
- 合理设置连接池大小,避免连接过多或空闲连接过多;
- 为连接建立、读取、空闲和池获取分别设置超时;
- 确保 HTTP/1.1 响应具有正确的消息 framing;
- 对 HTTP/2/3 不要手动添加
Connection: keep-alive; - 对 POST 等非幂等请求谨慎自动重试,必要时使用幂等键;
- 监控连接数、活跃请求数、空闲连接数、连接复用率、TIME-WAIT 和错误率;
- 让客户端、网关、负载均衡器和服务端的空闲超时保持协调;
- 服务升级或负载过高时,使用优雅关闭,避免直接丢弃正在处理的请求;
- 不要把 HTTP 持久连接与 WebSocket、SSE 等应用层长时间数据流混为一谈。
八、小结
HTTP keep-alive 更准确地说是 HTTP 持久连接。它通过复用连接减少 TCP/TLS 握手和连接关闭的成本,在浏览器、微服务和高频 API 调用中通常有明显价值。
但它不是“连接永远不关闭”,也不是 TCP SO_KEEPALIVE。连接是否复用取决于 HTTP 版本、消息边界、连接池、客户端和服务端策略,以及中间代理的超时配置。
在 HTTP/1.1 中,持久连接依赖正确的 Content-Length 或 chunked framing;在 HTTP/2 和 HTTP/3 中,则进一步涉及 Stream 多路复用、流量控制和 QUIC 连接管理。实际项目应结合连接池、超时、重试、负载均衡和监控一起设计。
参考
- RFC 9112:HTTP/1.1 —— HTTP/1.1 消息 framing 和持久连接
- RFC 9113:HTTP/2
- RFC 9114:HTTP/3
- HTTP Keepalive Connections and Web Performance —— Nginx 关于长连接与性能的说明
- Apache Tomcat HTTP Connector
- Spring Boot Application Properties
- 复习:TCP 三次握手、四次挥手
- HttpURLConnection 的一些问题