Go 1.27 让 HTTP/1 未读完的响应体不再拖垮连接复用

Go 1.27 的 net/http 在响应体被关闭时,会自动在后台把没读完的部分读掉,上限是 50 毫秒或者 256 KB,读完就把这条 HTTP/1 连接放回池子里等下一个请求。在这之前,body 没读到结尾就只能废弃连接,程序不报错,却在反复做 TCP 和 TLS 握手。这个改动不需要改任何代码,但凡写过 HTTP 客户端的人都该了解。

适用读者是维护 Go 客户端和服务端的开发者,以及被接口延迟困扰、正在往网络层排查的工程团队。

一条 HTTP/1 连接要满足什么条件才会被复用

HTTP/1.1 的默认行为就是持久连接,客户端和服务端任意一方声明 close 才会断开。HTTP/1.0 不默认持久,需要 keep-alive 头。HTTP/2 走的是另一套逻辑,多条流复用在一条连接上。

HTTP/1 上请求和响应在同一条连接里按顺序排,客户端必须把当前响应的 body 读到结尾,才能知道下一个响应从哪个字节开始,边界靠 Content-Length 或者 Transfer-Encoding 判断。body 没到 EOF,这条连接就交不出去。

少读一个字节的代价

只关心状态码的函数,拿到响应就 Close,body 一个字节没读。Go 1.26 及更早的版本看到还有未读内容,判断这条连接不能复用,直接放弃,下一个请求从头握手。

读了一半也可能中招。用 JSON 解码器解析响应时,它读到对象的右花括号就返回,服务端在 JSON 后面补了个换行,这个换行还留在连接上。

跨地域访问时这个代价最明显。TCP 握手一个往返,TLS 1.2 握手两个往返,三个往返之后才能发出第一个请求,单程延迟 50 毫秒的话,建连本身就花掉 300 毫秒。TLS 1.3 能把握手压到一个往返,仍然比复用一条已有连接贵。同一个只取状态码的函数向同一台服务器发一百次请求,走 HTTP/1 的情况下,1.26 上大约 710 毫秒,1.27 上大约 150 毫秒。

Go 1.27 具体改了哪一步

从 1.27 开始,Transport 关闭一条没读完的 HTTP/1 响应体时,会在后台继续读到 EOF,上限是 256 KB 和 50 毫秒,谁先到就停。读到结尾,连接回到空闲池;超时或者超过字节数,连接废弃。响应头里声明了 close 的话,读下去必然是白费功夫,实现直接跳过。

以前要自己写 drain,现在把 body 关掉就行,官方给出的建议就是延迟一行 Close。

手写 drain 为什么容易出错

最朴素的写法是把 body 整个复制到 io.Discard。响应体大的时候,读完再丢掉比重新建一条连接还慢。加上字节上限之后,遇到慢慢吐数据的服务端又会卡住,于是再补一个时间上限。异步版本还得防止 goroutine 泄漏,靠 Close 去打断正在进行的复制。

drain 的写法传开以后,不少人顺手给 HTTP/2 的连接也加了一遍。HTTP/2 上一条流的 body 没读完并不妨碍其他请求共用这条连接,那段代码纯属多余。

连接池按什么分组,默认值是多少

池子的键由代理、协议、地址和一个是否强制 HTTP/1 的标志组成。请求头不参与分组,User-Agent 或者 Authorization 不同,不会各自占一批连接。默认 Transport 的 MaxIdleConns 是 100,IdleConnTimeout 是 90 秒,前者管全局空闲连接总数,后者管空闲多久之后关闭。每个主机的空闲连接上限由 MaxIdleConnsPerHost 决定,不设置时取默认值 2。空闲定时器只挂在 HTTP/1 连接上,HTTP/2 自己管。

高并发下连接池成为瓶颈怎么查

  1. 先用 httptrace 的 GotConn 回调看每条请求拿到的连接是否复用。GotConnInfo 里有 Reused、WasIdle 和 IdleTime 三个字段,复用率低就先回头查 body 有没有读完。
  2. 复用正常但延迟仍然高,看每个主机的空闲连接上限。默认 2 在几十上百个并发请求面前很容易变成排队点,按实际并发量调大。
  3. 请求头里带了 close 的,每条连接用完就断,池子等于没用,去掉它。
  4. 空闲连接被 IdleConnTimeout 收走属于正常,但请求间隔超过 90 秒时每次都在建新连接,这时该把超时调长,或者用一条轻量请求把连接预热。
  5. 不要在每次请求里新建 Transport。它并发安全,一个请求一个实例等于把池子拆成碎片。

常见问题

升级到 1.27 要改代码吗? 不用。原来手写的 drain 可以删掉,删之前确认它没有兼职别的事,比如顺带校验响应长度。

响应体很大又不想读怎么办? 直接 Close。1.27 会在后台最多读 50 毫秒或者 256 KB,剩下的交给连接废弃。