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.comhttp://golang.com 就是两个不同的域名。

第一次访问 http://baidu.com 域名的时候会建立一个连接,用完之后放到空闲连接池里,下次再要访问 http://baidu.com 的时候会重新从连接池里把这个连接捞出来复用。

go 连接复用

设置长链接超时

对于长链接,可以在 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.ResponseBody 读取完毕并且关闭后,才会重用底层的 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”问题

Go HTTP 重用底层TCP连接需要注意的关键点

golang http.client 的 Connection reset by peer 问题

Golang之HTTP EOF/connection reset by peer详解

频繁发起HTTP请求会报『read: connection reset by peer』

an error : “…connection reset by peer…” about go net/http