Keep-Alive 在通过代理建立持久连接的语境中,是一种 HTTP 机制:它允许单个 TCP 连接保持打开状态,并在客户端与代理之间、以及代理与源服务器之间被复用于多次 HTTP 请求和响应,从而减少为每次事务建立新连接所带来的开销。
理解 Keep-Alive
HTTP Keep-Alive,也称为持久连接,是 Web 性能的一项基础优化。如果没有 Keep-Alive,每个 HTTP 请求都需要建立一次新的 TCP 连接(TCP 握手),然后传输数据,再终止连接。这一过程会带来显著开销,对于还需要 TLS 握手的加密连接尤其如此。
持久连接的好处
- 降低延迟: 消除同一连接上后续请求所需的 TCP 与 TLS 握手延迟。对往返时延(RTT)较高的客户端而言尤为明显。
- 降低 CPU 与内存占用: 更少地建立和终止连接,可减轻客户端、代理和源服务器的计算负载,包括套接字操作占用的 CPU 周期、保存连接状态所需的内存以及文件描述符的使用。
- 减少网络拥塞: 打开和关闭的 TCP 连接更少,意味着网络上的信令流量(SYN、SYN-ACK、FIN、FIN-ACK 数据包)更少。
- 提升吞吐量: 让 TCP 的拥塞控制机制得到更高效的利用,因为连接可以随时间达到更高的吞吐速率。
HTTP/1.x 中的 Keep-Alive
Keep-Alive 的实现方式和默认行为在 HTTP/1.0 与 HTTP/1.1 之间存在差异。
HTTP/1.0 的 Keep-Alive
在 HTTP/1.0 中,持久连接并非默认行为。客户端必须在请求中加入 Connection: keep-alive 头来显式请求 Keep-Alive。服务器则用相同的头进行响应,以表明支持持久连接。若任何一方未包含该头,连接将在当前响应结束后关闭。
HTTP/1.1 的 Keep-Alive
HTTP/1.1 将持久连接设为默认行为。除非另有明确声明,客户端和服务器都会认为连接在一次事务后应保持打开。要关闭连接,任一方必须发送 Connection: close 头。这一变化在默认情况下显著提升了 Web 性能。
作为逐跳(hop-by-hop)头的 Connection
Connection 头是逐跳头,即它只适用于两个通信方之间的直接连接(例如客户端到代理,或代理到源服务器),不得由代理或中间节点转发。代理在转发请求或响应之前必须剥离或修改逐跳头。否则可能导致协议违规和意外行为,因为下游服务器或客户端可能会误解连接管理指令。
其他常见的逐跳头包括 Keep-Alive、Proxy-Authenticate、Proxy-Authorization、TE、Trailers 和 Upgrade。
通过代理的 Keep-Alive
代理在管理持久连接方面起着关键作用。它们通常维护两组持久连接:
- 客户端到代理的连接: 代理与其客户端之间维持持久连接。
- 代理到源站的连接: 代理与其转发请求的源服务器之间维持持久连接。
代理在头部管理中的角色
当代理收到客户端发来的 Connection: keep-alive 头时,它理解为客户端希望保持与代理之间的连接。但代理不得将该 Connection 头转发给源服务器。代理应根据自身配置和源服务器的能力,独立决定是否与源服务器建立持久连接。
代理的常见做法(尤其在处理 HTTP/1.1 时)是从客户端请求中剥离 Connection 头,然后单独管理到源服务器的上游连接。如果代理希望在上游使用 HTTP/1.1 持久连接,只需不发送 Connection: close 头即可。
# 使用 Keep-Alive 代理 HTTP/1.1 的 Nginx 配置示例
location / {
proxy_pass http://backend_server;
proxy_http_version 1.1; # 指示 Nginx 对上游使用 HTTP/1.1
proxy_set_header Connection ""; # 移除客户端请求中的 Connection 头,
# 以确保 Nginx 独立管理上游 Keep-Alive。
# 这对于正确处理逐跳头至关重要。
}
在这个 Nginx 示例中,proxy_http_version 1.1; 确保 Nginx 连接 backend_server 时使用 HTTP/1.1。默认情况下,HTTP/1.1 连接是持久的。proxy_set_header Connection ""; 则显式移除可能来自客户端的 Connection 头,避免其被转发到源服务器,并让 Nginx 正确管理自己的上游持久连接。
连接池
代理通常会为到源服务器的上游连接实现连接池,即维护一组与各源服务器之间空闲且已打开的 TCP 连接。当针对某个源站的新请求到达时,代理会先检查池中是否有到该源站的空闲连接。若有,则复用该连接;若无,则建立新连接。这样可减少代理需要建立的新连接数量,进一步提升效率。
Keep-Alive 超时与资源管理
持久连接虽然有益,但需要谨慎管理,以防代理和源服务器上的资源被耗尽。
超时机制
每一方(客户端、代理、源服务器)都可以设置 keep-alive 超时。如果在该超时时间内持久连接上没有数据交换,连接就会被关闭。
- 客户端超时: 如果客户端在一定时间内未发送新请求,可能会关闭与代理的连接。
- 代理端超时: 代理可以为面向客户端和面向源站的连接分别配置自己的
keep-alive超时。若空闲的客户端到代理连接或代理到源站连接超过超时时间,代理会将其关闭。 - 源服务器超时: 若连接空闲时间过长,源服务器会关闭与代理的连接。
超时设置不匹配会引发问题。例如,若源服务器的 keep-alive 超时短于代理的超时,源站可能关闭一个代理仍认为处于打开状态的连接。当代理尝试复用这个"过期"连接时,会遇到错误(例如 connection reset),不得不在新连接上重试请求。这会增加延迟并抬高错误率。因此,通常建议将代理到源站的 keep-alive 超时配置为略短于或等于源服务器的超时。
最大连接数
代理还需要限制其维护的持久连接总数,包括面向客户端和面向源服务器的连接。每个打开的连接都会消耗系统资源(内存、文件描述符)。不加限制的持久连接可能导致:
- 文件描述符耗尽: 代理用尽可用的文件描述符。
- 内存耗尽: 打开的连接过多会消耗过量内存。
- CPU 负载上升: 管理大量空闲连接仍会带来一定开销。
代理配置通常允许管理员设置 keep-alive_timeout 和 keep-alive_requests(单个持久连接在被关闭并重建之前所允许的最大请求数)。
# Nginx 示例:面向客户端连接的 Keep-Alive 设置
http {
keepalive_timeout 60s; # 客户端到 Nginx 的 keep-alive 超时
keepalive_requests 1000; # 每个客户端到 Nginx 的 keep-alive 连接的最大请求数
# ...
}
# Nginx 示例:代理到源站连接的 Keep-Alive 设置
location / {
proxy_pass http://backend_server;
proxy_http_version 1.1;
proxy_set_header Connection "";
# 可选:如有需要,配置特定的上游 keep-alive 参数
# Nginx 上游 keep-alive 的默认行为是:只要连接未被上游显式关闭
# 或未被 Nginx 判定超时,就复用这些连接。
# 如需对上游连接池进行更明确的控制:
# proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
# proxy_connect_timeout 5s;
# proxy_read_timeout 10s;
# proxy_send_timeout 10s;
}
HTTP/2 及之后版本中的 Keep-Alive
HTTP/2 引入了多路复用,允许多个 HTTP 请求和响应在单个 TCP 连接上并发传输。这在应用层无需显式的 Connection: keep-alive 头即可天然获得 Keep-Alive 的好处。HTTP/2 连接一旦建立,其设计目标就是保持打开,并承载客户端与服务器(或客户端与代理、代理与源站)之间的全部流量,直到被显式关闭或超时。
尽管 HTTP/2 屏蔽了显式的 Keep-Alive 头,但通过维持持久 TCP 连接来降低开销这一底层原则依然至关重要。支持 HTTP/2 的代理会与客户端维持一条持久的 HTTP/2 连接,并可根据源站能力和代理配置,将请求转换为与源服务器之间的 HTTP/1.1 持久连接或 HTTP/2 连接。代理在管理这些底层 TCP 连接及其超时方面的作用,对性能和资源管理仍然十分关键。
Keep-Alive 行为总结
| 特性 | HTTP/1.0 | HTTP/1.1 | HTTP/2 |
|---|---|---|---|
| 默认持久性 | 否,每次请求后关闭连接。 | 是,默认为持久连接。 | 是,多个 stream 共用单条 TCP 连接。 |
| 持久性相关头 | Connection: keep-alive(显式) |
用 Connection: close 终止(显式) |
不适用(由多路复用实现持久性) |
| 请求流水线(pipelining) | 可行但复杂(队头阻塞) | 可行但复杂(队头阻塞) | 支持,完整多路复用 |
| 代理的管理职责 | 代理必须管理 Connection 头并决定上游是否持久。 |
代理必须剥离 Connection 头并管理上游持久性。 |
代理在持久 TCP 上管理 HTTP/2 stream。 |
| 开销削减 | 减少 TCP 握手开销。 | 显著减少 TCP 握手开销。 | 完全消除每次请求的 TCP/TLS 握手开销。 |
