Golang你一定要懂的連接池實(shí)現(xiàn)
問題引入
作為一名Golang開發(fā)者,線上環(huán)境遇到過好幾次連接數(shù)暴增問題(mysql/redis/kafka等)。
糾其原因,Golang作為常駐進(jìn)程,請(qǐng)求第三方服務(wù)或者資源完畢后,需要手動(dòng)關(guān)閉連接,否則連接會(huì)一直存在。而很多時(shí)候,開發(fā)者不一定記得關(guān)閉這個(gè)連接。
這樣是不是很麻煩?于是有了連接池。顧名思義,連接池就是管理連接的;我們從連接池獲取連接,請(qǐng)求完畢后再將連接還給連接池;連接池幫我們做了連接的建立、復(fù)用以及回收工作。
在設(shè)計(jì)與實(shí)現(xiàn)連接池時(shí),我們通常需要考慮以下幾個(gè)問題:
- 連接池的連接數(shù)目是否有限制,最大可以建立多少個(gè)連接?
- 當(dāng)連接長時(shí)間沒有使用,需要回收該連接嗎?
- 業(yè)務(wù)請(qǐng)求需要獲取連接時(shí),此時(shí)若連接池?zé)o空閑連接且無法新建連接,業(yè)務(wù)需要排隊(duì)等待嗎?
- 排隊(duì)的話又存在另外的問題,隊(duì)列長度有無限制,排隊(duì)時(shí)間呢?
Golang連接池實(shí)現(xiàn)原理
我們以Golang HTTP連接池為例,分析連接池的實(shí)現(xiàn)原理。
結(jié)構(gòu)體Transport
Transport結(jié)構(gòu)定義如下:
type Transport struct { //操作空閑連接需要獲取鎖 idleMu sync.Mutex //空閑連接池,key為協(xié)議目標(biāo)地址等組合 idleConn map[connectMethodKey][]*persistConn // most recently used at end //等待空閑連接的隊(duì)列,基于切片實(shí)現(xiàn),隊(duì)列大小無限制 idleConnWait map[connectMethodKey]wantConnQueue // waiting getConns //排隊(duì)等待建立連接需要獲取鎖 connsPerHostMu sync.Mutex //每個(gè)host建立的連接數(shù) connsPerHost map[connectMethodKey]int //等待建立連接的隊(duì)列,同樣基于切片實(shí)現(xiàn),隊(duì)列大小無限制 connsPerHostWait map[connectMethodKey]wantConnQueue // waiting getConns //最大空閑連接數(shù) MaxIdleConns int //每個(gè)目標(biāo)host最大空閑連接數(shù);默認(rèn)為2(注意默認(rèn)值) MaxIdleConnsPerHost int //每個(gè)host可建立的最大連接數(shù) MaxConnsPerHost int //連接多少時(shí)間沒有使用則被關(guān)閉 IdleConnTimeout time.Duration //禁用長連接,使用短連接 DisableKeepAlives bool }
可以看到,連接護(hù)著隊(duì)列,都是一個(gè)map結(jié)構(gòu),而key為協(xié)議目標(biāo)地址等組合,即同一種協(xié)議與同一個(gè)目標(biāo)host可建立的連接或者空閑連接是有限制的。
需要特別注意的是,MaxIdleConnsPerHost默認(rèn)等于2,即與目標(biāo)主機(jī)最多只維護(hù)兩個(gè)空閑連接。這會(huì)導(dǎo)致什么呢?
如果遇到突發(fā)流量,瞬間建立大量連接,但是回收連接時(shí),由于最大空閑連接數(shù)的限制,該聯(lián)機(jī)不能進(jìn)入空閑連接池,只能直接關(guān)閉。結(jié)果是,一直新建大量連接,又關(guān)閉大量連,業(yè)務(wù)機(jī)器的TIME_WAIT連接數(shù)隨之突增。
線上有些業(yè)務(wù)架構(gòu)是這樣的:客戶端 ===> LVS ===> Nginx ===> 服務(wù)。LVS負(fù)載均衡方案采用DR模式,LVS與Nginx配置統(tǒng)一VIP。此時(shí)在客戶端看來,只有一個(gè)IP地址,只有一個(gè)Host。上述問題更為明顯。
最后,Transport也提供了配置DisableKeepAlives,禁用長連接,使用短連接訪問第三方資源或者服務(wù)。
連接獲取與回收
Transport結(jié)構(gòu)提供下面兩個(gè)方法實(shí)現(xiàn)連接的獲取與回收操作。
func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persistConn, err error) {} func (t *Transport) tryPutIdleConn(pconn *persistConn) error {}
連接的獲取主要分為兩步走:1)嘗試獲取空閑連接;2)嘗試新建連接:
//getConn方法內(nèi)部實(shí)現(xiàn) if delivered := t.queueForIdleConn(w); delivered { return pc, nil } t.queueForDial(w)
當(dāng)然,可能獲取不到連接而需要排隊(duì),此時(shí)怎么辦呢?當(dāng)前會(huì)阻塞當(dāng)前協(xié)程了,直到獲取連接為止,或者h(yuǎn)ttpclient超時(shí)取消請(qǐng)求:
select { case <-w.ready: return w.pc, w.err //超時(shí)被取消 case <-req.Cancel: return nil, errRequestCanceledConn …… } var errRequestCanceledConn = errors.New("net/http: request canceled while waiting for connection") // TODO: unify?
排隊(duì)等待空閑連接的邏輯如下:
func (t *Transport) queueForIdleConn(w *wantConn) (delivered bool) { //如果配置了空閑超時(shí)時(shí)間,獲取到連接需要檢測(cè),超時(shí)則關(guān)閉連接 if t.IdleConnTimeout > 0 { oldTime = time.Now().Add(-t.IdleConnTimeout) } if list, ok := t.idleConn[w.key]; ok { for len(list) > 0 && !stop { pconn := list[len(list)-1] tooOld := !oldTime.IsZero() && pconn.idleAt.Round(0).Before(oldTime) //超時(shí)了,關(guān)閉連接 if tooOld { go pconn.closeConnIfStillIdle() } //分發(fā)連接到wantConn delivered = w.tryDeliver(pconn, nil) } } //排隊(duì)等待空閑連接 q := t.idleConnWait[w.key] q.pushBack(w) t.idleConnWait[w.key] = q }
排隊(duì)等待新建連接的邏輯如下:
func (t *Transport) queueForDial(w *wantConn) { //如果沒有限制最大連接數(shù),直接建立連接 if t.MaxConnsPerHost <= 0 { go t.dialConnFor(w) return } //如果沒超過連接數(shù)限制,直接建立連接 if n := t.connsPerHost[w.key]; n < t.MaxConnsPerHost { go t.dialConnFor(w) return } //排隊(duì)等待連接建立 q := t.connsPerHostWait[w.key] q.pushBack(w) t.connsPerHostWait[w.key] = q }
連接建立完成后,同樣會(huì)調(diào)用tryDeliver分發(fā)連接到wantConn,同時(shí)關(guān)閉通道w.ready,這樣主協(xié)程糾接觸阻塞了。
func (w *wantConn) tryDeliver(pc *persistConn, err error) bool { w.pc = pc close(w.ready) }
請(qǐng)求處理完成后,通過tryPutIdleConn將連接放回連接池;這時(shí)候如果存在等待空閑連接的協(xié)程,則需要分發(fā)復(fù)用該連接。另外,在回收連接時(shí),還需要校驗(yàn)空閑連接數(shù)目是否超過限制:
func (t *Transport) tryPutIdleConn(pconn *persistConn) error { //禁用長連接;或者最大空閑連接數(shù)不合法 if t.DisableKeepAlives || t.MaxIdleConnsPerHost < 0 { return errKeepAlivesDisabled } if q, ok := t.idleConnWait[key]; ok { //如果等待隊(duì)列不為空,分發(fā)連接 for q.len() > 0 { w := q.popFront() if w.tryDeliver(pconn, nil) { done = true break } } } //空閑連接數(shù)目超過限制,默認(rèn)為DefaultMaxIdleConnsPerHost=2 idles := t.idleConn[key] if len(idles) >= t.maxIdleConnsPerHost() { return errTooManyIdleHost } }
空閑連接超時(shí)關(guān)閉
Golang HTTP連接池如何實(shí)現(xiàn)空閑連接的超時(shí)關(guān)閉邏輯呢?從上述queueForIdleConn邏輯可以看到,每次在獲取到空閑連接時(shí),都會(huì)檢測(cè)是否已經(jīng)超時(shí),超時(shí)則關(guān)閉連接。
那如果沒有業(yè)務(wù)請(qǐng)求到達(dá),一直不需要獲取連接,空閑連接就不會(huì)超時(shí)關(guān)閉嗎?其實(shí)在將空閑連接添加到連接池時(shí),Golang同時(shí)還設(shè)置了定時(shí)器,定時(shí)器到期后,自然會(huì)關(guān)閉該連接。
pconn.idleTimer = time.AfterFunc(t.IdleConnTimeout, pconn.closeConnIfStillIdle)
排隊(duì)隊(duì)列怎么實(shí)現(xiàn)
怎么實(shí)現(xiàn)隊(duì)列模型呢?很簡單,可以基于切片:
queue []*wantConn //入隊(duì) queue = append(queue, w) //出隊(duì) v := queue[0] queue[0] = nil queue = queue[1:]
這樣有什么問題嗎?隨著頻繁的入隊(duì)與出隊(duì)操作,切片queue的底層數(shù)組,會(huì)有大量空間無法復(fù)用而造成浪費(fèi)。除非該切片執(zhí)行了擴(kuò)容操作。
Golang在實(shí)現(xiàn)隊(duì)列時(shí),使用了兩個(gè)切片head和tail;head切片用于出隊(duì)操作,tail切片用于入隊(duì)操作;出隊(duì)時(shí),如果head切片為空,則交換head與tail。通過這種方式,Golang實(shí)現(xiàn)了底層數(shù)組空間的復(fù)用。
func (q *wantConnQueue) pushBack(w *wantConn) { q.tail = append(q.tail, w) } func (q *wantConnQueue) popFront() *wantConn { if q.headPos >= len(q.head) { if len(q.tail) == 0 { return nil } // Pick up tail as new head, clear tail. q.head, q.headPos, q.tail = q.tail, 0, q.head[:0] } w := q.head[q.headPos] q.head[q.headPos] = nil q.headPos++ return w }
到此這篇關(guān)于Golang你一定要懂的連接池實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Golang 連接池內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
GO語言創(chuàng)建錢包并遍歷錢包(wallet)的實(shí)現(xiàn)代碼
比特幣錢包實(shí)際上是一個(gè)密鑰對(duì),當(dāng)你安裝 一個(gè)錢包應(yīng)用,或者是使用一個(gè)比特幣客戶端來生成一個(gè)新地址是,他就會(huì)為你生成一個(gè)密鑰對(duì),今天通過本文給大家分享go語言遍歷錢包的相關(guān)知識(shí),一起看看吧2021-05-05linux下通過go語言獲得系統(tǒng)進(jìn)程cpu使用情況的方法
這篇文章主要介紹了linux下通過go語言獲得系統(tǒng)進(jìn)程cpu使用情況的方法,實(shí)例分析了Go語言使用linux的系統(tǒng)命令ps來分析cpu使用情況的技巧,需要的朋友可以參考下2015-03-03Golang中ringbuffer的實(shí)現(xiàn)與應(yīng)用場(chǎng)景詳解
ringbuffer因?yàn)樗軓?fù)用緩沖空間,通常用于網(wǎng)絡(luò)通信連接的讀寫,雖然市面上已經(jīng)有了go寫的諸多版本的ringbuffer組件,但還是自己造一個(gè)吧2023-06-06golang 的string與[]byte轉(zhuǎn)換方式
這篇文章主要介紹了golang 的string與[]byte轉(zhuǎn)換方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-04-04關(guān)于Golang變量初始化/類型推斷/短聲明的問題
這篇文章主要介紹了關(guān)于Golang變量初始化/類型推斷/短聲明的問題,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-02-02