Go 中 http 超时问题的排查( 二 )


  1. 连接建立 , 三次握手 。
  2. tls握手的耗时 , 见下面 http2 章节的 dialConn 源码 。
分别在 dialConn 函数中 t.dial 和 addTLS 的位置追加日志 。可以看到 , 三次握手的连接还是比较稳定的 , 后面连接的在 tls 握手耗时上面 , 耗费将近 1s 。
2019/10/23 14:51:41 DialTime 39.511194ms https.Handshake 1.059698795s2019/10/23 14:51:41 DialTime 23.270069ms https.Handshake 1.064738698s2019/10/23 14:51:41 DialTime 24.854861ms https.Handshake 1.0405369s2019/10/23 14:51:41 DialTime 31.345886ms https.Handshake 1.076014428s2019/10/23 14:51:41 DialTime 26.767644ms https.Handshake 1.084155891s2019/10/23 14:51:41 DialTime 22.176858ms https.Handshake 1.064704515s2019/10/23 14:51:41 DialTime 26.871087ms https.Handshake 1.084666172s2019/10/23 14:51:41 DialTime 33.718771ms https.Handshake 1.084348815s2019/10/23 14:51:41 DialTime 20.648895ms https.Handshake 1.094335678s2019/10/23 14:51:41 DialTime 24.388066ms https.Handshake 1.084797011s2019/10/23 14:51:41 DialTime 34.142535ms https.Handshake 1.092597021s2019/10/23 14:51:41 DialTime 24.737611ms https.Handshake 1.187676462s2019/10/23 14:51:41 DialTime 24.753335ms https.Handshake 1.161623397s2019/10/23 14:51:41 DialTime 26.290747ms https.Handshake 1.173780655s2019/10/23 14:51:41 DialTime 28.865961ms https.Handshake 1.178235202s结论:第二个疑问的答案就是 tls 握手耗时
http2
为什么 Http2 没复用连接 , 反而会创建大量连接?前面创建 http.Client 时 , 是通过 http2.ConfigureTransport(transport) 方法 , 其内部调用了configureTransport :
func configureTransport(t1 *http.Transport) (*Transport, error) { // 声明一个连接池 // noDialClientConnPool 这里很关键 , 指明连接不需要dial出来的 , 而是由http1连接升级而来的connPool := new(clientConnPool) t2 := &Transport{ ConnPool: noDialClientConnPool{connPool}, t1: t1, } connPool.t = t2// 把http2的RoundTripp的方法注册到 , http1上transport的altProto变量上 。// 当请求使用http1的roundTrip方法时 , 检查altProto是否有注册的http2 , 有的话 , 则使用// 前面代码的useRegisteredProtocol就是检测方法 if err := registerHTTPSProtocol(t1, noDialH2RoundTripper{t2}); err != nil { return nil, err } // http1.1 升级到http2的后的回调函数 , 会把连接通过 addConnIfNeeded 函数把连接添加到http2的连接池中 upgradeFn := func(authority string, c *tls.Conn) http.RoundTripper { addr := authorityAddr("https", authority) if used, err := connPool.addConnIfNeeded(addr, t2, c); err != nil { go c.Close() return erringRoundTripper{err} } else if !used { go c.Close() } return t2 } if m := t1.TLSNextProto; len(m) == 0 { t1.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{ "h2": upgradeFn, } } else { m["h2"] = upgradeFn } return t2, nil}TLSNextProto 在 http.Transport-> dialConn 中使用 。调用upgradeFn函数 , 返回http2的RoundTripper , 赋值给 alt 。alt 会在 http.Transport 中 RoundTripper 内部检查调用 。
func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (*persistConn, error) { pconn := &persistConn{ t: t, } if cm.scheme() == "https" && t.DialTLS != nil { // 没有自定义DialTLS方法 , 不会走到这一步 } else { conn, err := t.dial(ctx, "tcp", cm.addr()) if err != nil { return nil, wrapErr(err) } pconn.conn = conn if cm.scheme() == "https" { // addTLS 里进行 tls 握手 , 也是建立新连接最耗时的地方 。if err = pconn.addTLS(firstTLSHost, trace); err != nil { return nil, wrapErr(err) } } } if s := pconn.tlsState; s != nil && s.NegotiatedProtocolIsMutual && s.NegotiatedProtocol != "" { if next, ok := t.TLSNextProto[s.NegotiatedProtocol]; ok { // next 调用注册的升级函数 return &persistConn{t: t, cacheKey: pconn.cacheKey, alt: next(cm.targetAddr, pconn.conn.(*tls.Conn))}, nil } } return pconn, nil}结论:
当没有连接时 , 如果此时来一大波请求 , 会创建 n 多 http1.1 的连接 , 进行升级和握手 , 而 tls 握手随着连接增加而变的非常慢 。
 
解决超时
上面的结论并不能完整解释 , 复用连接的问题 。因为服务正常运行的时候 , 一直都有请求的 , 连接是不会断开的 , 所以除了第一次连接或网络原因断开 , 正常情况下都应该复用 http2 连接 。
通过下面测试 , 可以复现有 http2 的连接时 , 还是会创建 N 多新连接:
sdk.Request() // 先请求一次 , 建立好连接 , 测试是否一直复用连接 。time.Sleep(time.Second)n := 1000var waitGroutp = sync.WaitGroup{}waitGroutp.Add(n)for i := 0; i < n; i++ { go func(x int) { sdk.Request() }}waitGroutp.Wait()


推荐阅读