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來完成這個動作。真的笨啊。。