第 10 / 23 篇

第9章 连接池与响应体的完整生命周期

Client 与 Transport 应当复用

Go 的 http.Client 与 Transport 为并发使用而设计。每次请求创建全新 Transport,会丢失连接复用,增加建连、TLS 与空闲连接成本;把共享 Client 的配置在请求中随意修改,则可能引入竞争。通常在服务启动时构造并配置,在运行期间复用,到关闭阶段清理空闲连接。

随书 Fetcher 复用同一个 Transport,并把 MaxConnsPerHost 设为二。这一限制包括连接建立中、活跃和空闲连接。超过上限时,新的请求可能等待连接,而不是立即报“连接池满”。因此等待本身必须受请求 context 约束,否则下游变慢会在客户端内部形成看不见的队列。

MaxIdleConnsPerHost 管理的是空闲连接保留,不是所有活跃请求的并发上限。把它调小不会自动限制并发外呼;把它调大也不表示下游允许更高负载。应用工作池和 Transport 有不同职责:前者限制业务接纳,后者管理网络连接。两层预算都需要明确。

Do 成功之后,工作还没结束

Client.Do 返回的响应包括 Body。只要 Body 尚未处理完,连接相关资源可能仍然被占用。调用者必须关闭 Body;若希望 HTTP/1.x 连接复用,通常还需要读到 EOF。只检查状态码然后忘记关闭,是最常见的连接耗尽来源之一。错误状态同样有 Body,也不能遗漏。

随书真实 TCP 测试先让响应头到达但保留未读响应体,在小连接预算下发起后续请求。后续请求等不到连接,最终由 context 超时。关闭先前响应体后,再次请求能够恢复。这个实验直接证明资源占用与恢复,比观察进程打开文件数的偶然变化更明确。

不要用无限 io.Copy(io.Discard, body) 试图保证复用。恶意或损坏上游可以无限发送,清理动作反而无限占用。应设置读取上限和总预算:小响应可以读完并复用;超过大小限制就关闭,必要时牺牲该连接。资源保护优先于复用率。

大小限制有一个边界字节

要区分恰好达到上限和超过上限,可以最多读取允许大小再加一个字节。若直接用 LimitReader 读取 N 字节,读到 N 并不意味着上游已经结束。随书 Fetcher 限制响应体为一 MiB,超限时返回明确错误并关闭响应体。它不承诺超限连接仍可复用,下一次请求可以建立新连接。

返回数据之前关闭 Body,可以把关闭责任留在 Fetcher 内部。若业务需要边读边处理,就应返回一个明确的 ReadCloser 或采用回调,让调用方知道自己接管了什么。接口隐藏网络流却不交代关闭责任,会使调用者无法正确释放资源。

取消读取也必须能释放。测试让上游发头后阻塞 Body,客户端取消传入 context,随后验证读取结束、服务端请求 context 观察到断开,以及连接槽可供下一次请求。这里的恢复断言非常重要:一个函数虽然返回错误,但 Transport 仍不可用,依然是资源故障。

连接复用不等于请求序列化

HTTP/1.1 与 HTTP/2 的连接、流和并发关系不同。一个连接上可能承载多个并发流,因此用 TCP 连接数推断请求并发并不普遍成立。随书连接耗尽实验使用明确的本地 HTTP/1.1 行为;若升级协议,需要重新定义对流数量和取消的断言。

IdleConnTimeout 用于空闲连接,不会终止正在缓慢读取的响应体。ResponseHeaderTimeout 主要限制请求发送完成后等响应头,不覆盖整个 Body 读取。理解每个字段的起止点,才能解释“明明配置了超时,为什么还在等”。总预算依然需要 context 或 Client.Timeout 承担。

CloseIdleConnections 只清理当前空闲连接,不是取消所有外呼的按钮。服务退出时应先停止新工作、让现有工作结束或取消,再清理共享 Transport。过早清理空闲连接通常不能解决在途阻塞;无限等待在途请求则需要从它们自己的 context 与截止检查。

练习:制造并修复连接耗尽

先把每主机连接上限设为一,发起一个不读 Body 的请求,再发第二个带短截止的请求。记录第二个失败的位置,并确认上游是否收到它。关闭第一个 Body 后重试,要求恢复。随后比较读到 EOF 后关闭与直接关闭的连接复用差异。

扩展实验时增加响应体上限与非二百状态,确保每个分支都关闭 Body。不要只检查返回错误;同时检查后续请求能够完成。最后将测试多次运行,确认没有依赖端口、固定睡眠或某次恰好的连接重用。

本章实测证据

测试使用真实TCP,未读Body占住小连接预算,额外请求受context限制失败;关闭Body后连接预算恢复。 以下摘自最终本机 go test -race -v ./...,完整命令及环境见附录B;括号中的耗时仅是当次运行记录。

--- PASS: TestConnectionExhaustionAndRecovery (0.05s)