go http 库 Connection reset by peer 解决
Connection reset by peer
最近使用go 中 http 包的默认服务端发起 get 请求,个别请求出现了:Connection reset by peer
的错误。大致的报错长这个样子:
read tcp xx.xxx.xxx.xxx:xx->xx.xxx.xxx.xxx:xx: read: connection reset by peer
在网上查找了一些资料,这里归结使用 http 包的注意事项以及上述错误出现的原因。
原因
http 长链接
HTTP协议从1.1
之后就默认使用长连接。golang标准库里也兼容这种实现。
通过建立一个连接池,针对每个域名建立一个TCP长连接,比如 http://baidu.com
和 http://golang.com
就是两个不同的域名。
第一次访问 http://baidu.com
域名的时候会建立一个连接,用完之后放到空闲连接池里,下次再要访问 http://baidu.com
的时候会重新从连接池里把这个连接捞出来复用。
设置长链接超时
对于长链接,可以在 Transport 中设置一个超时时间,这个时间其实就是长链接的有效时间,过了这个时间就会报 timeout 错误:
tr = &http.Transport{
MaxIdleConns: 100,
Dial: func(netw, addr string) (net.Conn, error) {
conn, err := net.DialTimeout(netw, addr, time.Second*2) //设置建立连接超时
if err != nil {
return nil, err
}
err = conn.SetDeadline(time.Now().Add(time.Second * 3)) //设置发送接收数据超时
if err != nil {
return nil, err
}
return conn, nil
},
}
在上面设置了发送、接收数据的超时时间为 3s。
假设第一次请求要100ms
,每次请求完 http://baidu.com
后都放入连接池中,下次继续复用,重复29
次,耗时2900ms
。
第 30 次请求的时候,连接从建立开始到服务返回前就已经用了3000ms
,刚好到设置的 3s 超时阈值,那么此时客户端就会报超时 i/o timeout
。
虽然这时候服务端其实才花了100ms
,但耐不住前面 29 次加起来的耗时已经很长。
也就是说只要通过 http.Transport
设置了 err = conn.SetDeadline(time.Now().Add(time.Second * 3))
,并且用了长连接,哪怕服务端处理再快,客户端设置的超时再长,总有一刻,程序会报超时错误。
关闭 rsp 的 Body
在进行 http 请求后,我们一般会有以下代码:
defer rsp.Body.Close()
该代码用于在读取完 Body 信息后关闭它。
在默认的 http 包中,会采用长链接的方式执行请求,如果没有关闭上述 Body 或者没有读取完毕 Body 的内容,则可能会导致不用重用 TCP 连接:
// Body represents the response body.
//
// The response body is streamed on demand as the Body field
// is read. If the network connection fails or the server
// terminates the response, Body.Read calls return an error.
//
// The http Client and Transport guarantee that Body is always
// non-nil, even on responses without a body or responses with
// a zero-length body. It is the caller's responsibility to
// close Body. 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.
//
// The Body is automatically dechunked if the server replied
// with a "chunked" Transfer-Encoding.
//
// As of Go 1.12, the Body will also implement io.Writer
// on a successful "101 Switching Protocols" response,
// as used by WebSockets and HTTP/2's "h2c" mode.
Body io.ReadCloser
注释中提到,必须将 http.Response
的 Body
读取完毕并且关闭后,才会重用底层的 TCP 连接。
例如对于以下代码,虽然代码写了关闭 Body
,但是因为没有从 Body
中读取数据,最终还是无法重用底层的 TCP 连接:
func main() {
count := 100
for i := 0; i < count; i++ {
resp, err := http.Get("https://www.oschina.net")
if err != nil {
panic(err)
}
// 这一行代码读取并丢掉Body
//io.Copy(ioutil.Discard, resp.Body)
resp.Body.Close()
}
}
出现 Connection reset by peer 的原因
go 中的 connection 一般会有两个协程,一个用于读,一个用于写(readLoop 和 writeLoop)。在大多数的情况下,readLoop 会检测 socket 是否关闭,并适时的关闭 connection。如果一个新请求在 readLoop 检测到关闭之前就来到,那么就会产生 EOF 错误并中断执行,而不是去关闭前一个请求。
执行时建立一个新的连接,这段程序执行完成后退出,再次打开执行时服务器并不知道已经关闭了连接,所以提示连接被重置;如果不退出程序而使用 for 循环多次发送时,旧连接未关闭,新连接却到来,会报 EOF。
这种情况经常发生在并发高的场景下,此时由于客户端的长链接由于被服务端由于各种原因而断开,例如服务端限制了最大并发连接数。但此时客户端还不知道服务端断开了连接而发送了请求,就会导致上述的报错。
解决方式
解决上述问题分为以下几个方法:
在客户端关闭 http 连接
func main() {
req, err := http.NewRequest("GET", "http://www.baidu.com",nil )
if err != nil {
log.Errorf("")
}
req.Close = true
resp, err := http.Client.Do(req)
}
在头部设置连接状态为关闭
func main() {
req, err := http.NewRequest("GET", "http://www.baidu.com",nil )
if err != nil {
log.Errorf("")
}
req.Header.Add("Connection", "close")
resp, err := http.Client.Do(req)
}
使用 Transport 取消 HTTP利用连接
func main() {
tr := http.Transport{DisableKeepAlives: true}
client := http.Client{Transport: &tr}
resp, err := client.Get("https://www.baidu.com/")
if resp != nil {
defer resp.Body.Close()
}
checkError(err)
fmt.Println(resp.StatusCode) // 200
body, err := ioutil.ReadAll(resp.Body)
checkError(err)
fmt.Println(len(string(body)))
}
实际使用时我自己是把三种都配上了。
参考文章:
Go 解决”Connection reset by peer”或”EOF”问题
golang http.client 的 Connection reset by peer 问题
Golang之HTTP EOF/connection reset by peer详解