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的清理
 - 达到空闲连接上限后,连接的换入换出 <--> 缓存满了之后,数据的换入换出
 
