HTTP 持久连接允许多次请求与响应复用同一条连接,减少重复的 TCP 建连;使用 HTTPS 时,也可能省去新的 TLS 协商。收益取决于网络延迟、响应大小、服务端处理时间以及池中是否已经有可用连接。连接复用不会消除业务计算,也没有承诺某个套接字永远保持打开。讨论优化之前,先明确观察的是哪一段连接、哪个协议版本。

完整报文才允许复用

HTTP/1.1 默认允许持久连接,除非某一方发送 Connection: close,或实际关闭传输连接。通常不必额外添加 Connection: keep-alive。客户端连接池可以保留空闲连接,供符合源、代理、证书和池策略的后续请求使用。连续执行两次独立的命令行程序,通常不会共享内存中的连接池,因而不能凭两次响应成功就断言已经复用。

复用的前提是双方都能识别每条消息的结束位置。响应可以通过准确的 Content-Length、分块传输编码,或无正文响应的协议规则界定边界。如果只能依赖关闭连接表示正文结束,这条连接自然无法留给下一次请求。客户端需要读完响应正文;服务器需要读完请求正文,或者关闭连接。残留字节一旦被当成下一条消息解析,问题就从性能退化变成报文错位。

HTTP 持久连接与 TCP keepalive 也是两回事。后者在传输连接空闲足够久后发送探测,帮助识别失联的对端,不负责 HTTP 消息分界,也不能覆盖代理或应用自己的空闲超时。仅仅打开操作系统的探活选项,并不会让一个已经被代理回收的连接重新可用。排查时应分别记录应用层关闭、传输层错误和连接池回收动作。

顺序复用还不同于流水线请求。客户端可以等前一个响应完成后再发送下一条请求,同时一直使用相同连接。HTTP/1.1 流水线允许提前发送请求,但响应仍须按序返回。因此,仅有持久连接并不具备 HTTP/2 多路复用提供的独立响应流,不能把两者的并发收益混为一谈。

每一跳管理自己的超时

空闲超时按照具体实现的规则衡量无活动时间,请求截止时间则限制一次操作能等待多久,两者需要独立配置。反向代理同时拥有客户端连接和上游连接,两侧可能使用不同协议、连接池和超时。因此,浏览器到边缘复用了连接,并不能证明边缘到源站也复用;看到一侧握手减少,也不能直接解释另一侧的延迟变化。

连接池应结合已知的服务端限制提前淘汰空闲连接,但始终可能发生竞态:客户端刚发送下一条请求,服务器恰好开始关闭。监控应同时包含新建连接数、成功复用次数、连接重置、池占用和请求延迟。保留更久可以减少握手,也会占用文件描述符与内存。应依据实际请求间隔和资源预算确定保留策略,不能仅凭“站点很忙”推导一个固定超时。

坏掉的样子

正文长度错误可能让读取方一直等待,或让下一条响应的开头混进上一条正文。遇到请求挂起时先检查消息分界,再考虑调大超时。另一个常见故障是调用方放弃流式响应,却既不读完也不关闭,导致池中的连接长期占用。此时服务器还有处理能力,新的请求却全在客户端等空位,扩大服务端容量无法解决这个问题。

陈旧连接报错不代表服务器没有执行请求。如果支付已经提交而响应在回程丢失,自动重连并原样重发可能重复扣款。是否重试必须依照方法语义和业务幂等契约判断,不能把传输异常当成回滚证明。HTTP/2 虽然可以在连接上复用多个流,却禁止 Connection、Keep-Alive 等 HTTP/1.x 专属字段;把这些头搬进 HTTP/2 配置没有优化意义。

快速验证

下面是可直接运行的 Python 3 实验:服务器绑定系统分配的回环端口,客户端连续请求四次,结束后关闭测试服务器。它不需要外部服务;内置 HTTP 服务器只用于本地验证,不是部署配置。

from http.client import HTTPConnection
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from threading import Thread

class Handler(BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"

    def do_GET(self):
        self.server.connections.append(self.connection)
        self.send_response(200)
        self.send_header("Content-Length", "2")
        if self.path == "/close":
            self.send_header("Connection", "close")
        self.end_headers()
        self.wfile.write(b"ok")

    def log_message(self, *args):
        pass

with ThreadingHTTPServer(("127.0.0.1", 0), Handler) as server:
    server.connections = []
    worker = Thread(target=server.serve_forever, daemon=True)
    worker.start()
    client = HTTPConnection(*server.server_address, timeout=3)
    try:
        for path in ("/one", "/two", "/close", "/four"):
            client.request("GET", path)
            response = client.getresponse()
            assert response.status == 200
            assert response.read() == b"ok"
        sockets = server.connections
        print("reused:", sockets[0] is sockets[1] is sockets[2])
        print("new after close:", sockets[2] is not sockets[3])
    finally:
        client.close()
        server.shutdown()
        worker.join()

预期输出是 reused: True 和 new after close: True。前两次完整读取正文后,第三次仍使用同一个套接字;第三次收到显式关闭指示后,第四次重新建连。这里比较的是服务器持有的连接对象,不是容易误读的响应耗时。实验只证明本地 HTTP/1.1 的复用与关闭行为,没有覆盖 TLS、浏览器连接池或生产代理;部署验证仍应逐跳检查连接标识与新建连接计数。

延伸阅读

来源