Go Http Client连接优化
背景
:由于之前项目采用了请求外部服务使用了http短链接形式,随着用户流量的增长!公司公网带宽SNAT连接数飙升!已经达到最高规格已经无法升级了!是时候需要优化了?
怎么优化
HTTP长连接(持久化连接)
长连接的优点:
1、减少CPU及内存的使用,因为不需要经常的建立及关闭连接,当然高峰并发时CPU及内存也是比较多的;
2、允许 HTTP pipelining
(HTTP 1.1中支持)的请求及响应模式
3、减少网络的堵塞,因为减少了TCP请求;
4、减少后续请求的响应时间,因为此时不需要建立TCP,也不需要TCP握手等过程;
5、当发生错误时,可以在不关闭连接的情况下进行提示;
由于之前使用的是默认的httpclient 未做任何优化我们来看看DefaultTransport定义
// DefaultTransport is the default implementation of Transport and is // used by DefaultClient. It establishes network connections as needed // and caches them for reuse by subsequent calls. It uses HTTP proxies // as directed by the $HTTP_PROXY and $NO_PROXY (or $http_proxy and // $no_proxy) environment variables. var DefaultTransport RoundTripper = &Transport{ Proxy: ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, DualStack: true, }).DialContext, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, }
- net.Dialer.Timeout 限制创建一个TCP连接使用的时间(如果需要一个新的链接)
- http.Transport.MaxIdleConns 最大空闲连接数是100
- http.Transport.IdleConnTimeout IdleConnTimeout它用于控制一个闲置连接在连接池中的保留时间,而不考虑一个客户端请求被阻塞在哪个阶段。
- http.Transport.TLSHandshakeTimeout TLS握手超时时间
http client发起请求一般是由Do(req *Request) (*Response, error)方法开始,而真正处理请求分发的是transport的RoundTrip(*Request) (*Response, error)方法。那么Transport 到底应该怎么设置才合理呢?
// Transport only retries a request upon encountering a network error // if the request is idempotent and either has no body or has its // Request.GetBody defined. HTTP requests are considered idempotent if // they have HTTP methods GET, HEAD, OPTIONS, or TRACE; or if their // Header map contains an "Idempotency-Key" or "X-Idempotency-Key" // entry. If the idempotency key value is an zero-length slice, the // request is treated as idempotent but the header is not sent on the // wire. type Transport struct { idleMu sync.Mutex closeIdle bool // user has requested to close all idle conns idleConn map[connectMethodKey][]*persistConn // most recently used at end idleConnWait map[connectMethodKey]wantConnQueue // waiting getConns idleLRU connLRU reqMu sync.Mutex reqCanceler map[*Request]func(error) altMu sync.Mutex // guards changing altProto only altProto atomic.Value // of nil or map[string]RoundTripper, key is URI scheme connsPerHostMu sync.Mutex connsPerHost map[connectMethodKey]int connsPerHostWait map[connectMethodKey]wantConnQueue // waiting getConns // Proxy specifies a function to return a proxy for a given // Request. If the function returns a non-nil error, the // request is aborted with the provided error. // // The proxy type is determined by the URL scheme. "http", // "https", and "socks5" are supported. If the scheme is empty, // "http" is assumed. // // If Proxy is nil or returns a nil *URL, no proxy is used. Proxy func(*Request) (*url.URL, error) // DialContext specifies the dial function for creating unencrypted TCP connections. // If DialContext is nil (and the deprecated Dial below is also nil), // then the transport dials using package net. // // DialContext runs concurrently with calls to RoundTrip. // A RoundTrip call that initiates a dial may end up using // a connection dialed previously when the earlier connection // becomes idle before the later DialContext completes. DialContext func(ctx context.Context, network, addr string) (net.Conn, error) // Dial specifies the dial function for creating unencrypted TCP connections. // // Dial runs concurrently with calls to RoundTrip. // A RoundTrip call that initiates a dial may end up using // a connection dialed previously when the earlier connection // becomes idle before the later Dial completes. // // Deprecated: Use DialContext instead, which allows the transport // to cancel dials as soon as they are no longer needed. // If both are set, DialContext takes priority. Dial func(network, addr string) (net.Conn, error) // DialTLS specifies an optional dial function for creating // TLS connections for non-proxied HTTPS requests. // // If DialTLS is nil, Dial and TLSClientConfig are used. // // If DialTLS is set, the Dial hook is not used for HTTPS // requests and the TLSClientConfig and TLSHandshakeTimeout // are ignored. The returned net.Conn is assumed to already be // past the TLS handshake. DialTLS func(network, addr string) (net.Conn, error) // TLSClientConfig specifies the TLS configuration to use with // tls.Client. // If nil, the default configuration is used. // If non-nil, HTTP/2 support may not be enabled by default. TLSClientConfig *tls.Config // TLSHandshakeTimeout specifies the maximum amount of time waiting to // wait for a TLS handshake. Zero means no timeout. TLSHandshakeTimeout time.Duration // DisableKeepAlives, if true, disables HTTP keep-alives and // will only use the connection to the server for a single // HTTP request. // // This is unrelated to the similarly named TCP keep-alives. DisableKeepAlives bool // DisableCompression, if true, prevents the Transport from // requesting compression with an "Accept-Encoding: gzip" // request header when the Request contains no existing // Accept-Encoding value. If the Transport requests gzip on // its own and gets a gzipped response, it's transparently // decoded in the Response.Body. However, if the user // explicitly requested gzip it is not automatically // uncompressed. DisableCompression bool // MaxIdleConns controls the maximum number of idle (keep-alive) // connections across all hosts. Zero means no limit. MaxIdleConns int // MaxIdleConnsPerHost, if non-zero, controls the maximum idle // (keep-alive) connections to keep per-host. If zero, // DefaultMaxIdleConnsPerHost is used. MaxIdleConnsPerHost int // MaxConnsPerHost optionally limits the total number of // connections per host, including connections in the dialing, // active, and idle states. On limit violation, dials will block. // // Zero means no limit. MaxConnsPerHost int // IdleConnTimeout is the maximum amount of time an idle // (keep-alive) connection will remain idle before closing // itself. // Zero means no limit. IdleConnTimeout time.Duration // ResponseHeaderTimeout, if non-zero, specifies the amount of // time to wait for a server's response headers after fully // writing the request (including its body, if any). This // time does not include the time to read the response body. ResponseHeaderTimeout time.Duration // ExpectContinueTimeout, if non-zero, specifies the amount of // time to wait for a server's first response headers after fully // writing the request headers if the request has an // "Expect: 100-continue" header. Zero means no timeout and // causes the body to be sent immediately, without // waiting for the server to approve. // This time does not include the time to send the request header. ExpectContinueTimeout time.Duration // TLSNextProto specifies how the Transport switches to an // alternate protocol (such as HTTP/2) after a TLS NPN/ALPN // protocol negotiation. If Transport dials an TLS connection // with a non-empty protocol name and TLSNextProto contains a // map entry for that key (such as "h2"), then the func is // called with the request's authority (such as "example.com" // or "example.com:1234") and the TLS connection. The function // must return a RoundTripper that then handles the request. // If TLSNextProto is not nil, HTTP/2 support is not enabled // automatically. TLSNextProto map[string]func(authority string, c *tls.Conn) RoundTripper // ProxyConnectHeader optionally specifies headers to send to // proxies during CONNECT requests. ProxyConnectHeader Header // MaxResponseHeaderBytes specifies a limit on how many // response bytes are allowed in the server's response // header. // // Zero means to use a default limit. MaxResponseHeaderBytes int64 // WriteBufferSize specifies the size of the write buffer used // when writing to the transport. // If zero, a default (currently 4KB) is used. WriteBufferSize int // ReadBufferSize specifies the size of the read buffer used // when reading from the transport. // If zero, a default (currently 4KB) is used. ReadBufferSize int // nextProtoOnce guards initialization of TLSNextProto and // h2transport (via onceSetNextProtoDefaults) nextProtoOnce sync.Once h2transport h2Transport // non-nil if http2 wired up tlsNextProtoWasNil bool // whether TLSNextProto was nil when the Once fired // ForceAttemptHTTP2 controls whether HTTP/2 is enabled when a non-zero // Dial, DialTLS, or DialContext func or TLSClientConfig is provided. // By default, use of any those fields conservatively disables HTTP/2. // To use a custom dialer or TLS config and still attempt HTTP/2 // upgrades, set this to true. ForceAttemptHTTP2 bool }
Transport是一个支持HTTP、HTTPS、HTTP Proxies的RoundTripper,是协程安全的,并默认支持连接池。
当获取一个IdleConn处理完request后,会调用tryPutIdleConn方法回放conn:
if t.closeIdle {
return errCloseIdle
}
if t.idleConn == nil {
t.idleConn = make(map[connectMethodKey][]*persistConn)
}
idles := t.idleConn[key]
if len(idles) >= t.maxIdleConnsPerHost() {
return errTooManyIdleHost
}
for _, exist := range idles {
if exist == pconn {
log.Fatalf("dup idle pconn %p in freelist", pconn)
}
}
IdleConn不仅受到MaxIdleConn的限制,也受到MaxIdleConnsPerHost的限制,DefaultTranspor中是没有设置该参数的,而默认的参数为2这是 RFC2616
建议的单个客户端发起的持久连接数,不过在大部分情况下,这个值有是不够用的。
MaxIdleConnsPerHost限制的是相同connectMethodKey(代表着不同的协议 不同的host,也就是不同的请求)到 persistConn 的映射的空闲连接数量
DefaultMaxIdleConnsPerHost的默认值是2,这对一个大并发的场景是完全不够用的。
// Register to receive next connection that becomes idle. if t.idleConnWait == nil { t.idleConnWait = make(map[connectMethodKey]wantConnQueue) }
通过自定义Transport:
type Config struct { Dial xtime.Duration Timeout xtime.Duration KeepAlive xtime.Duration MaxConns int MaxIdle int BackoffInterval xtime.Duration // Interval is second retryCount int } type HttpClient struct { conf *Config client *xhttp.Client dialer *net.Dialer transport *xhttp.Transport retryCount int retrier retry.Retriable } // NewHTTPClient returns a new instance of httpClient func NewHTTPClient(c *Config) *HttpClient { dialer := &net.Dialer{ Timeout: time.Duration(c.Dial), KeepAlive: time.Duration(c.KeepAlive), } transport := &xhttp.Transport{ DialContext: dialer.DialContext, MaxConnsPerHost: c.MaxConns, MaxIdleConnsPerHost: c.MaxIdle, IdleConnTimeout: time.Duration(c.KeepAlive), TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } _ = http2.ConfigureTransport(transport) bo := backoff.NewConstantBackoff(c.BackoffInterval) return &HttpClient{ conf: c, client: &xhttp.Client{ Transport: transport, }, retryCount: defaultRetryCount, retrier: retry.NewRetrier(bo), } }
完整代码: https://github.com/quan-xie/tuba/blob/master/transport/httpclient/context_client.go
连接数终于稳定下来了