1.前言

之前萌叔曾在文章
 imroc/req 连接池使用须知提及过Golang标准库net/http提供的连接池http.Transport,但是是浅尝辄止。
本文萌叔想从http.Transport出发来谈谈一个连接池设计应该考虑哪些问题?

2.连接池的功能特征

下图是针对Grpc和Thrift压测结果(见参考资料1)。我么可以看出,长连接相比与短连接相比,QPS大概提升了1倍多。这是因为长连接减少连接建立的所需的3次握手。

要对长连接进行管理,特别是对闲置的长连接进行管理,就不可避免的引入连接池。

特征

  • 需要一个连接时,并不一定真的创建新连接,而是优先尝试从连接池选出空闲连接;如果连接池对应的连接为空,才创建新连接。
  • 销毁并不是真的销毁,而是将使用完毕的连接放回连接池中(逻辑关闭)。

这里引出了几个问题。

  • Q1:获取连接阶段,我们有没有办法知道从连接池中取出的空闲连接(复用)是有效的,还是无效的?
  • Q2:把使用完毕的连接放回连接池的阶段,空闲连接数量是否要做上限的约束。如果空闲连接数量有上限约束且空闲连接的数量已经达到上限。那么把连接放回连接池的过程,必然需要将之前的某个空闲连接进行关闭,那么按照什么规则选择这个需要关闭的连接。
  • Q3:放置在连接池中的连接,随着时间的流逝,它可能会变成无效连接(stale)。比如由于Server端定时清理空闲连接。那么为了确保连接池中连接的有效性,是否需要引入定时的检查逻辑?

3.net/http中连接池的实现

net/http中连接池的实现代码在

  net/http/transport.go中

获取连接

Transport.RoundTrip() -> Transport.getConn()

放回连接(逻辑关闭)

Response.Body.Close() -> bodyEOFSignal.Close() -> Transport.tryPutIdleConn()

MaxIdleConnsMaxIdleConnsPerHostMaxIdleConnsMaxConnsPerHost

有意思的点

transport中的连接是按照key存储,key可以对应到下面的结构

type connectMethod struct {
    _            incomparable
    proxyURL     *url.URL // nil for no proxy, else full proxy URL
    targetScheme string   // "http" or "https"
    // If proxyURL specifies an http or https proxy, and targetScheme is http (not https),
    // then targetAddr is not included in the connect method key, because the socket can
    // be reused for different targetAddr values.
    targetAddr string
    onlyH1     bool // whether to disable HTTP/2 and force HTTP/1
}

 

注意: 目标地址如果是同一个域名则算作同一个Host

Q2:把连接放回连接池的过程,必然需要将之前的某个空闲连接进行关闭,那么按照什么规则选择这个需要关闭的连接。

A2:

如果连接池已经满了(MaxIdleConns),那么放回空闲连接的同时,还需要从连接池中选出一个旧连接进行关闭。这个选择的规则依据LRU进行筛选。net/http使用的双向链表。

Q3: 为了确保连接池中连接的有效性,是否需要引入定时的检查逻辑?

A3:

IdleConnTimeoutIdleConnTimeout

这个逻辑可能是:”过长时间的空闲连接都是不可信赖的”

一个从连接池中获得的连接只有在真正使用时,才能确定它是否有效。
如果连接在使用时报错,需要执行shouldRetryRequest()以确定是否需要获取新连接来执行失败的HTTP请求。

func (pc *persistConn) shouldRetryRequest(req *Request, err error) bool {
        ...
    if err == errServerClosedIdle {
        // The server replied with io.EOF while we were trying to
        // read the response. Probably an unfortunately keep-alive
        // timeout, just as the client was writing a request.
        return true
    }
    return false // conservatively
}
errServerClosedIdle

4. 总结

看net/http的连接池,萌叔发现它与数据库的连接池、甚至与进程内的本地缓存在设计要点有很多相似之处。比如

  • 连接池大小的限制 <--> 缓存大小的限制
  • stale空闲连接的检查 <--> 过期key的清理
  • 达到空闲连接上限后,连接的换入换出 <--> 缓存满了之后,数据的换入换出