一,问题起因

    线上 server to server 的服务,出现大量的 TIME_WAIT。用 netstat 发现,不断的有连接在建立,没有保持住连接。抓 TCP 包确认 request 和 response 中的 keepalive 都已经设置,但是每个 TCP 连接处理 6 次左右的 http 请求后,就被关闭。

    就处理该问题的过程中,查看了一下 http client 的部分源码。

    二,HTTP Client 简单结构

    1,简单 HTTP client 定义

    1. httpClient := &http.Client{ Timeout: config.Client_Timeout * time.Millisecond,

    Timeout:从发起请求到整个报文响应结束的超时时间。

    Transport:为http.RoundTripper接口,定义功能为负责http的请求分发。实际功能由结构体net/http/transport.go中的Transport struct继承并实现,除了请求发分还实现了对空闲连接的管理。如果创建client时不定义,就用系统默认配置。

    1. **2,**DefaultTransport定义
    1. var DefaultTransport RoundTripper = &Transport{ Proxy: ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second,

    net.Dialer.Timeout: 连接超时时间。

    net.Dialer.KeepAlive:开启长连接(说明默认http client是默认开启长连接的)。

    http.Transport.TLSHandshakeTimeout:限制TLS握手使用的时间。
    http.Transport.ExpectContinueTimeout:限制客户端在发送一个包含:100-continue的http报文头后,等待收到一个go-ahead响应报文所用的时间。

    http.Transport.MaxIdleConns:最大空闲连接数。(the maximum number of idle (keep-alive) connections across all hosts. Zero means no limit.)

    http.Transport.IdleConnTimeout:连接最大空闲时间,超过这个时间就会被关闭。

    三,问题跟踪-keepAlive设置

    1. 按照DefaultTransport自定义Transport后,怎么调整参数,线上问题依旧没有得到解决。怀疑是对keepAlive参数的理解不到位,所以继续看源码中对keepAlive参数的使用。

    2. net包中net/dial.go中, 使用方法func (d *Dialer) DialContext()创建新连接,有代码片段如下:

    1. if tc, ok := c.(*TCPConn); ok && d.KeepAlive > 0 { setKeepAlive(tc.fd, true) setKeepAlivePeriod(tc.fd, d.KeepAlive)
    1. Dialer中设置的一个keepalive参数,被分解成了两个分支,一是开关,二是keepalive周期。再继续往下跟踪源码的时候,就开始系统调用了,提取出关键代码如下:
    1. syscall.SetsockoptInt(fd.sysfd, syscall.SOL_SOCKET, syscall.SO_KEEPALIVE, boolint(keepalive))
    1. syscall.SetsockoptInt(fd.sysfd, syscall.IPPROTO_TCP, sysTCP_KEEPINTVL, secs) syscall.SetsockoptInt(fd.sysfd, syscall.IPPROTO_TCP, syscall.TCP_KEEPALIVE, secs)
    1. 大致意思是,首先开启系统socketSOL\_SOCKET设置;然后TCP\_KEEPINTVLTCP\_KEEPALIVE用的同一个时间来设置。

    3. 可以查看linux系统中TCP关于keepalive的三个参数,执行man 7 tcp 命令可以找到以下三个参数:

    1. tcp_keepalive_intvl (integer; default: 75; since Linux 2.4)
    2. The number of seconds between TCP keep-alive probes.
    3. tcp_keepalive_probes (integer; default: 9; since Linux 2.2)
    4. The maximum number of TCP keep-alive probes to send before giving up and killing the connection if no response is obtained
    5. from the other end.
    6. tcp_keepalive_time (integer; default: 7200; since Linux 2.2)
    7. The number of seconds a connection needs to be idle before TCP begins sending out keep-alive probes. Keep-alives are only
    8. sent when the SO_KEEPALIVE socket option is enabled. The default value is 7200 seconds (2 hours). An idle connection is
    9. terminated after approximately an additional 11 minutes (9 probes an interval of 75 seconds apart) when keep-alive is
    10. enabled.
    11. Note that underlying connection tracking mechanisms and application timeouts may be much shorter.

    大意为:要想使用 keepalive 机制,首先得开启 SO_KEEPALIVE 设置;然后系统会在 connection 空闲 keepalive_time 时间后发起探针,连续 keepalive_probes 个探针失败时,系统将关闭连接。keepalive_intvl 为两次探针的间隔时间。

    明白 go 的 keepalive 后,理论上应用中的设置是没问题的,实际经过调大调小该参数,也是没有解决保持不住长连接的问题。无奈从 Client 开始继续看源码…

    四,Transport

    client 发起请求一般是由 Do(req Request) (Response, error) 方法开始,而真正处理请求分发的是 transport 的 RoundTrip(Request) (Response, error) 方法,Transport 定义如下:

    1. idleConn map[connectMethodKey][]*persistConn idleConnCh map[connectMethodKey]chan *persistConn reqCanceler map[*Request]func(error)Proxy func(*Request) (*url.URL, error) DialContext func(ctx context.Context, network, addr string) (net.Conn, error) Dial func(network, addr string) (net.Conn, error) DialTLS func(network, addr string) (net.Conn, error) TLSClientConfig *tls.Config TLSHandshakeTimeout time.Duration IdleConnTimeout time.Duration ResponseHeaderTimeout time.Duration ExpectContinueTimeout time.Duration TLSNextProto map[string]func(authority string, c *tls.Conn) RoundTripper ProxyConnectHeader Header MaxResponseHeaderBytes int64 h2transport *http2Transport

    大意为Transport是一个支持HTTP、HTTPS、HTTP Proxies的RoundTripper,是协程安全的,并默认支持连接池。

    从源码能看到,当获取一个IdleConn处理完request后,会调用tryPutIdleConn方法回放conn,代码有这样一个逻辑:

    1. if len(idles) >= t.maxIdleConnsPerHost() {return errTooManyIdleHost

    也就是说IdleConn不仅受到MaxIdleConn的限制,也受到MaxIdleConnsPerHost的限制,DefaultTranspor中是没有设置该参数的,而默认的参数为2.

    由于我们业务为server to server,所以是定点访问,经过该参数的调整,服务器上已经保持住稳定的长连接了。

    五,参考资料

    1. Go net/http 超时指导

    2. golang 长短连接处理

    3.为什么基于TCP的应用需要心跳包(TCP keep-alive原理分析)

    4.Golang 优化之路——HTTP长连接

    1. golang的垃圾回收与Finalizer——tcp连接是如何被自动关闭的 https://blog.csdn.net/kdpujie/article/details/73177179?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.control ```