Go 1.27 中更好的 HTTP/1 连接重用
看看以下函数: ```go func getStatus(url string) (int, error) { resp, err := http.Get(url) if err != nil { return 0, err } defer resp.Body.Close() return resp.StatusCode, nil } ``` 它向 URL 发送一个 GET 请求并返回 HTTP 状态代码。在我的机器上,使用 HTTP/1(GODEBUG=http2client=0)对 index.golang.org 调用该函数 100 次在 Go 1.26 中需要约 710 毫秒。 在 Go 1.27 中,相同的程序只需约 150 毫秒。 这几乎快了 5 倍!这样显著的性能差异怎么可能发生? 建立 HTTP 连接的成本 为了理解差异,考虑我们发出 HTTP 请求时网络上发生了什么。 Go 的 net/http 包支持 HTTP/1.0、HTTP/1.1 和 HTTP/2。 三者均通过 TCP 运行,并且在与 HTTPS 端点通信时通常通过 TLS 进行。 通过新连接发出请求看起来像这样: TCP 握手(1 RTT):客户端发送一个 SYN 数据包,服务器回复 SYN-ACK,客户端回复 ACK。由于客户端可以在最终的 ACK 旁边发送第一个 TLS 消息,因此建立 TCP 连接需要 1 次往返时间 (RTT)。 TLS 握手(2 RTT):接下来,TLS 握手交换加密密钥并验证服务器的身份;使用 TLS 1.2,此过程需要 2 RTT。 HTTP 请求:在交换密钥后,可以最终在总共 3 RTT 后发送 HTTP 请求。 这些往返(加上一些用于 TLS 握手的 CPU 时间)加起来很快。在跨区域通信中,单向延迟通常达到或超过图中所示的 50 毫秒(100 毫秒 RTT)。 与仅用于设置连接的 300 毫秒相比,HTTP 请求本身的服务成本可能很便宜,在带宽充足或 HTTP 响应较小时,仅需略多于 1 RTT。即使在更有利的条件下,打开新连接仍然可能很昂贵。 我们的基准查询 index.golang.org,它在全球分布并支持 TLS 1.3。这意味着延迟相对较低,TLS 握手实际上可以在仅 1 RTT 内完成。尽管如此,性能差异依然非常明显。 连接重用 为了避免在每个请求上支付此设置成本,HTTP 客户端和服务器会尽可能尝试在多个请求中重用现有连接。 这种机制根据 HTTP 版本有所不同: - HTTP/1.0:虽然不是官方规范的一部分,但客户端和服务器通常通过发送 Connection: keep-alive 头部选择连接重用。 重用持续到任一端省略该头部或发送 Connection: close 头部。 - HTTP/1.1:连接默认是持久的,并且可以用于后续请求和响应,除非任一方声明另行发送 Connection: close 头部。 - HTTP/2:多个请求和响应可以在单个 TCP 连接上复用。 当连接重用失败时 在 HTTP/1(包括 HTTP/1.0 和 HTTP/1.1)中,同一连接上多个请求和响应是依次在网络上传输的。 如果向 index.golang.org 发送两个 GET 请求,通过相同的 TCP 连接,响应将作为一个字节流返回: 如图所示,net/http.Get 从字节流中读取和解析状态行和头部,以填充 net/http.Response,然后立即返回。 响应主体保持在连接上未读取,仅在调用者从 net/http.Response.Body 读取时消耗。 由于响应是并行发送的,net/http 只能在当前响应主体读取到最后(由 Content-Length 或 Transfer-Encoding 头确定)后找到下一个响应的开始。 现在回顾我们的示例程序,其中我们从未读取 resp.Body: ```go func getStatus(url string) (int, error) { resp, err := http.Get(url) if err != nil { return 0, err } defer resp.Body.Close() return resp.StatusCode, nil } ``` 在 Go 1.26 中,当调用 resp.Body.Close 时,net/http 看到仍然有未读取的响应主体。 意识到无法重用连接,它放弃现有连接并为下一个请求创建新连接,导致反复遭受建立新连接的成本。 (HTTP/2 没有这个问题。由于它在单个连接上复用帧流而不是严格按顺序发送响应,因此一个流上的未读取响应主体不会阻止其他请求共享该连接。) 连接重用失败的影响 这种 HTTP/1 连接重用的微妙之处可能导致在表面上看似无害的代码中静默的性能下降。 正如我们开头的例子所示,客户端可能只关心 API 响应的状态码。 然而,它所请求的 API 端点可能发送的主体会提供更多详细信息但最终被忽略,例如 {"success": true, "message": "update processed"}。 不幸的是,这将导致连接重用失败。 即使是读取响应的程序也会受此影响。
本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。
☕请我喝杯咖啡