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

所以还是怀疑 http1.1 升级导致 , 这次直接改成使用 http2.Transport
httpClient.Transport = &http2.Transport{ TLSClientConfig: tlsConfig,}改了后 , 测试发现没有报错了 。
为了验证升级模式和直接 http2 模式的区别 。这里先回到升级模式中的 addConnIfNeeded 函数中 , 其会调用 addConnCall 的 run 函数:
func (c *addConnCall) run(t *Transport, key string, tc *tls.Conn) { cc, err := t.NewClientConn(tc)}run 参数中传入的是 http2 的 transport 。整个解释是 http1.1 创建连接后 , 会把传输层连接 , 通过 addConnIfNeeded->run->Transport.NewClientConn 构成一个 http2 连接 。因为 http2 和 http1.1 本质都是应用层协议 , 传输层的连接都是一样的 。然后在 newClientConn 连接中加日志 。
func (t *Transport) newClientConn(c net.Conn, singleUse bool) (*ClientConn, error) { // log.Println("http2.newClientConn")}结论:
升级模式下 , 会打印很多 http2.newClientConn , 根据前面的排查这是讲的通的 。而单纯 http2 模式下 , 也会创建新连接 , 虽然很少 。
 
并发连接数
那 http2 模式下什么情况下会创建新连接呢?
这里看什么情况下http2会调用 newClientConn 。回到 clientConnPool 中 , dialOnMiss在http2模式下为 true , getStartDialLocked 里会调用 dial->dialClientConn->newClientConn 。
func (p *clientConnPool) getClientConn(req *http.Request, addr string, dialOnMiss bool) (*ClientConn, error) { p.mu.Lock() for _, cc := range p.conns[addr] { if st := cc.idleState(); st.canTakeNewRequest { if p.shouldTraceGetConn(st) { traceGetConn(req, addr) } p.mu.Unlock() return cc, nil } } if !dialOnMiss { p.mu.Unlock() return nil, ErrNoCachedConn } traceGetConn(req, addr) call := p.getStartDialLocked(addr) p.mu.Unlock() }有连接的情况下 , canTakeNewRequest 为false , 也会创建新连接 。看看这个变量是这么得来的:
func (cc *ClientConn) idleStateLocked() (st clientConnIdleState) { if cc.singleUse && cc.nextStreamID > 1 { return } var maxConcurrentOkay bool if cc.t.StrictMaxConcurrentStreams { maxConcurrentOkay = true } else { maxConcurrentOkay = int64(len(cc.streams)+1) < int64(cc.maxConcurrentStreams) } st.canTakeNewRequest = cc.goAway == nil && !cc.closed && !cc.closing && maxConcurrentOkay && int64(cc.nextStreamID)+2*int64(cc.pendingRequests) < math.MaxInt32 // if st.canTakeNewRequest == false { // log.Println("clientConnPool", cc.maxConcurrentStreams, cc.goAway == nil, !cc.closed, !cc.closing, maxConcurrentOkay, int64(cc.nextStreamID)+2*int64(cc.pendingRequests) < math.MaxInt32) // } st.freshConn = cc.nextStreamID == 1 && st.canTakeNewRequest return}为了查问题 , 这里加了详细日志 。测试下来 , 发现是 maxConcurrentStreams 超了 , canTakeNewRequest 才为 false 。在 http2中newClientConn 的初始化配置中, maxConcurrentStreams 默认为 1000:
maxConcurrentStreams: 1000, // "infinite", per spec. 1000 seems good enough.但实际测下来 , 发现 500 并发也会创建新连接 。继续追查有设置这个变量的地方:
ffunc (rl *clientConnReadLoop) processSettings(f *SettingsFrame) error { case SettingMaxConcurrentStreams: cc.maxConcurrentStreams = s.Val //log.Println("maxConcurrentStreams", s.Val)}运行测试 , 发现是服务传过来的配置 , 值是 250 。
结论:服务端限制了单连接并发连接数 , 超了后就会创建新连接 。
服务端限制
在服务端框架中 , 找到 ListenAndServeTLS 函数 , 跟下去 ->ServeTLS->Serve->setupHTTP2_Serve->onceSetNextProtoDefaults_Serve->onceSetNextProtoDefaults->http2ConfigureServer 。
查到 new(http2Server) 的声明 , 因为 web 框架即支持 http1.1 也支持 http2 , 所以没有指定任何 http2 的相关配置 , 都使用的是默认的 。
// Server is an HTTP/2 server.type http2Server struct { // MaxConcurrentStreams optionally specifies the number of // concurrent streams that each client may have open at a // time. This is unrelated to the number of http.Handler goroutines // which may be active globally, which is MaxHandlers. // If zero, MaxConcurrentStreams defaults to at least 100, per // the HTTP/2 spec's recommendations. MaxConcurrentStreams uint32} 从该字段的注释中看出 , http2 标准推荐至少为 100 , golang 中使用默认变量 http2defaultMaxStreams ,  它的值为 250 。
 
真相上面的步骤 , 更多的是为了记录排查过程和源码中的关键点 , 方便以后类似问题有个参考 。


推荐阅读