原文:https://blog.youkuaiyun.com/qq_20996105/article/details/83273500
1.背景
对于上一次的map连接池实现,其效率与线程安全是没有问题的。但是在实际的使用中,当并发量很大的时候,其依然会出现问题。
2 .出现的问题
仔细查看get代码,不难发现在获取连接时,由于没有设置连接上限,我们默认总会获取到连接(无论是从连接池获取还是新建连接)。本人使用的是rpc连接,做压力测试时,连接池大小为100,但每秒的并发请求数为10000,连接的使用时间为1s。导致大部分的连接无法从连接池获取,只能采用新建的方式。而这些新建之后的连接也无法真正的放回连接池,必须关闭,所以就会有大量的连接不停的创建和关闭。首先,这是和连接池的设计初衷相左的。其次,大量的连接就会占用大量不同的端口,由于关闭并不是瞬时的,会存在一个time_wait时间差。一般linux系统为60秒。这就导致在实际的高并发下端口号立即被占用完毕而无法再创建新的连接而服务无法正常完成,这也是我遇到的问题。通过以下命令,我们可以看到有大量的time_wait端口。
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
3. 解决办法
我们已经明确了由于没有设置连接上限而导致新建大量连接的问题,所以我们必须设置一个连接上限,而连接数量的多少应该根据服务器的状态和实际的使用场景来配置。
const(
PoolConntctions = 100
MaxConnections = 10000
WaitTimeOut = 2* time.Second
)
type Conn interface {
Close() error
}
type Pool struct {
maxConns int32
curConns int32
bp map[string]*boundPool
mu sync.RWMutex
}
func (p *Pool)Get(target string)(Conn,error){
var bp *boundPool
p.mu.RLock()
bp = p.bp[target]
if bp == nil{
p.mu.RUnlock()
tmp := NewBoundPool(p)
p.mu.Lock()
p.bp[target] = tmp
p.mu.Unlock()
bp = tmp
}else{
p.mu.RUnlock()
}
return bp.Get()
}
type boundPool struct {
p *Pool
conns chan Conn
}
func NewBoundPool(p *Pool)*boundPool{
return &boundPool{
p:p,
conns:make(chan Conn,PoolConntctions),
}
}
func (bp *boundPool)Get()(Conn,error){
select {
case c := <-bp.conns:
return c,nil
default:
if atomic.LoadInt32(&bp.p.curConns) < bp.p.maxConns{
c,err := bp.new()
if err != nil{
return nil,err
}
return c,nil
}
}
select {
case c := <-bp.conns:
return c,nil
case <- time.After(WaitTimeOut):
return nil,fmt.Errorf("time out waiting for free connections")
}
}
func (bp *boundPool)new()(Conn,error){
return net.Dial("tpc",":8086")
}
这里没有给出全部代码,为了更加清晰的连接池结构和避免多次map的锁操作。我将pool分为了两部分,第一部分的map映射和第二部分实际的连接池。当然给外部的接口还是pool。
看一下boundpool的get实现,我们要么从连接池获取,要么连接数量允许,我们新建连接。而当两者都不可满足时,我们仍然不能返回错误,因为当并发量很大时,大多数情况都会如此。所以我们引入了第二次select方式的获取,我们等待一定的时间,在这段时间里如果我们仍然不能获取到可用的连接,那么只能说明我们的配置参数,或者服务器的状态不能满足巨大的请求量。对了,别忘了在关闭连接和新建连接时对当前连接数做相应的加减操作。并且我们可以在服务启动时初始化一定数量连接,看起来很简单,其实实现起来仍然有一些可以值得注意的地方。
4. 写在后边
其实关于获取连接等待一定时间,在我最初写连接池时就有如此的考虑,但那时候我陷入了一个思维怪圈,首先,我不知道连接池大小设置多少合适。其次,我无法权衡从通道获取,等待超时和新建连接这三者的关系。那个时候我总想着用一个select 完成这三者操作,但很明显,这样是不符合逻辑的,也不可能实现。我居然没想到重写整理一下思路,用两个select来完成这个动作。真的笨啊。。