关于 Golang 中 http.Response.Body 未读取导致连接复用问题的一点研究
在 Golang 的 HTTP 库的源代码中,关于 http.Response.Body
的说明如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
注意到其中的一句话:The default HTTP client's Transport may not reuse HTTP/1.x "keep-alive" TCP connections if the Body is not read to completion and closed.,
它说,如果 Body 未被读完且关闭的话,默认的 HTTP 客户端的传输层是不会复用 HTTP/1.x 的 “Keep-Alive” 连接的。
(在 HTTP/1.0 的时代,“Keep-Alive” 还不是默认行为,如果浏览器和服务器支持,可以在请求头和响应头加上“Connection: Keep-Alive”。
然而在 HTTP/1.1 中,“Keep-Alive” 是默认行为,除非特别声明了不允许(比如 Connection: close
)^。)
为什么要连接复用?当然是基于性能的考虑啦。多路复用能不香吗?
日常写代码的方式
而再看我们平常写的代码,似乎很少有人注意到这点(只是发送 HTTP 请求,不需要数据的情况):
1 2 3 4 5 6 |
|
如果只是忘记 Body.Close()
,大概率 linter 是过不了的。但是却没有人提醒你,Body 该不该被读走。
按照文档所述,如果 Body 未被读完的话,连接是不会被复用的。为什么?我认为非常简单:因为 HTTP 客户端无法知道你是否还会用到 Body。
它可不敢帮你读走(以完成一次 Request/Response)以主动复用连接,毕竟万一网络突然卡了,读取 Body 占用大量时间。
本来 Close 一下是很快的,结果因为想要复用却导致一大堆不能用且没关闭的连接。这在标准库的设计上来说是不可取的。
按照 Go 语言中 http.Request/Response 的实现原理:服务端在读完请求(Request)的头部(Headers),或客户端在读完响应(Response)的头部(Headers),以后就算完成了请求或响应的读取,Body 被包装成 ReadCloser 接口作为流交给程序后面自己去读取(并关闭)。这一点非常好,不会因为请求或响应包含一个非常大的 Body 而占用大量内存。
应该考虑的写法
综上所述,文档中关于连接复用的现象的说法就是必然存在的了。 那么我们是不是总是应该在不需要 Body 的地方执行一句
1
|
|
以读完 Body 呢?就像 defer resp.Body.Close()
需要总是记得关闭那样。
答案我不确定:如果能确定接口非常快并且响应数据少或没有数据,那么执行一下可以想像并无性能损失。但是万一网络卡住,那么,读取 Body 的时候就会耗时很久了。 所以,按需吧。如果你在编写代码时能考虑到这一点(或者加个注释也行),那也是更严谨的体现。
一个用于验证连接复用的例子
我写了段测试代码以验证这个行为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
像下面这样,不读走 Body 调用时:
1 2 3 |
|
多次执行,均能看到同样的结果:发起了 2 次 connect 建立连接。是不是说明连接没有复用?我想是的。 虽然两次的文件描述符都是 6,那是因为每次都关闭了连接,并且按照 Unix 的特点:文件描述符总是从最小未被使用的开始。
而当读走 Body 调用时:
1 2 |
|
均只能看到只有 1 次 connect 的调用。 是不是已经能说明连接确实被复用了?我想是的。