车驰夜幕|Gopher 你一定要懂的连接池,作为

问题引入作为一名Golang开发者 , 线上环境遇到过好几次连接数暴增问题(mysql/redis/kafka等) 。
纠其原因 , Golang作为常驻进程 , 请求第三方服务或者资源完毕后 , 需要手动关闭连接 , 否则连接会一直存在 。 而很多时候 , 开发者不一定记得关闭这个连接 。
这样是不是很麻烦?于是有了连接池 。 顾名思义 , 连接池就是管理连接的;我们从连接池获取连接 , 请求完毕后再将连接还给连接池;连接池帮我们做了连接的建立、复用以及回收工作 。
在设计与实现连接池时 , 我们通常需要考虑以下几个问题:
连接池的连接数目是否有限制 , 最大可以建立多少个连接?当连接长时间没有使用 , 需要回收该连接吗?业务请求需要获取连接时 , 此时若连接池无空闲连接且无法新建连接 , 业务需要排队等待吗?排队的话又存在另外的问题 , 队列长度有无限制 , 排队时间呢?Golang连接池实现原理我们以GolangHTTP连接池为例 , 分析连接池的实现原理 。
结构体TransportTransport结构定义如下:
typeTransportstruct{//操作空闲连接需要获取锁idleMusync.Mutex//空闲连接池 , key为协议目标地址等组合idleConnmap[connectMethodKey][]*persistConn//mostrecentlyusedatend//等待空闲连接的队列 , 基于切片实现 , 队列大小无限制idleConnWaitmap[connectMethodKey]wantConnQueue//waitinggetConns//排队等待建立连接需要获取锁connsPerHostMusync.Mutex//每个host建立的连接数connsPerHostmap[connectMethodKey]int//等待建立连接的队列 , 同样基于切片实现 , 队列大小无限制connsPerHostWaitmap[connectMethodKey]wantConnQueue//waitinggetConns//最大空闲连接数MaxIdleConnsint//每个目标host最大空闲连接数;默认为2(注意默认值)MaxIdleConnsPerHostint//每个host可建立的最大连接数MaxConnsPerHostint//连接多少时间没有使用则被关闭IdleConnTimeouttime.Duration//禁用长连接 , 使用短连接DisableKeepAlivesbool}可以看到 , 连接护着队列 , 都是一个map结构 , 而key为协议目标地址等组合 , 即同一种协议与同一个目标host可建立的连接或者空闲连接是有限制的 。
需要特别注意的是 , MaxIdleConnsPerHost默认等于2 , 即与目标主机最多只维护两个空闲连接 。 这会导致什么呢?
如果遇到突发流量 , 瞬间建立大量连接 , 但是回收连接时 , 由于最大空闲连接数的限制 , 该联机不能进入空闲连接池 , 只能直接关闭 。 结果是 , 一直新建大量连接 , 又关闭大量连 , 业务机器的TIME_WAIT连接数随之突增 。
线上有些业务架构是这样的:客户端===>LVS===>Nginx===>服务 。 LVS负载均衡方案采用DR模式 , LVS与Nginx配置统一VIP 。 此时在客户端看来 , 只有一个IP地址 , 只有一个Host 。 上述问题更为明显 。
最后 , Transport也提供了配置DisableKeepAlives , 禁用长连接 , 使用短连接访问第三方资源或者服务 。
连接获取与回收Transport结构提供下面两个方法实现连接的获取与回收操作 。
func(t*Transport)getConn(treq*transportRequest,cmconnectMethod)(pc*persistConn,errerror){}func(t*Transport)tryPutIdleConn(pconn*persistConn)error{}连接的获取主要分为两步走:1)尝试获取空闲连接;2)尝试新建连接:
//getConn方法内部实现ifdelivered:=t.queueForIdleConn(w);delivered{returnpc,nil}t.queueForDial(w)当然 , 可能获取不到连接而需要排队 , 此时怎么办呢?当前会阻塞当前协程了 , 直到获取连接为止 , 或者httpclient超时取消请求:
select{case排队等待空闲连接的逻辑如下:
func(t*Transport)queueForIdleConn(w*wantConn)(deliveredbool){//如果配置了空闲超时时间 , 获取到连接需要检测 , 超时则关闭连接ift.IdleConnTimeout>0{oldTime=time.Now().Add(-t.IdleConnTimeout)}iflist,ok:=t.idleConn[w.key];ok{forlen(list)>0&&!stop{pconn:=list[len(list)-1]tooOld:=!oldTime.IsZero()&&pconn.idleAt.Round(0).Before(oldTime)//超时了 , 关闭连接iftooOld{gopconn.closeConnIfStillIdle()}//分发连接到wantConndelivered=w.tryDeliver(pconn,nil)}}//排队等待空闲连接q:=t.idleConnWait[w.key]q.pushBack(w)t.idleConnWait[w.key]=q}排队等待新建连接的逻辑如下:


推荐阅读