連接池
碎碎念
其實所謂的“連接池”,個人觀點是一種在工程實踐中以空間換時間的優化方案。
我們在實際的開發中,常見的資源表現形式:一種是存儲(內存+磁盤存儲)資源,還有是IO(磁盤IO+網絡IO)資源,另外當然還有操作系統CPU的調度/計算等等。
而在實際中,存儲資源相對於IO及計算來說便宜很多,比如說當我們的服務遇到瓶頸的時候,最直接的方案就是升級機器、增加存儲,先讓線上的服務恢復穩定然后再去考慮優化。
什么是連接池
連接池其實是基於“連接”的概念而來的。所謂連接,在絕大多數情況下其實就是Client-Server模式的一種資源傳輸的通道。當客戶端與服務端想要創建通信時,首先需要在二者之間創建連接(TCP、UDP),然后二者進行數據通信、資源傳輸等等操作。
而客戶端與服務端每創建一次連接需要很多次的網絡IO操作,期間也會使用CPU的計算、調度開銷。而且實際中如果二者需要長時間不斷創建、斷開鏈接資源,會浪費大量的CPU調度資源。
而如果使用連接池的話:我們在客戶端與服務端之間“提前”維護好“固定數量的鏈接”,這些鏈接整合到一起會占用操作系統的內存資源,但是這個連接池中的連接使用完以后會重新放入連接池中而不是直接斷開,等需要處理后續的業務的時候還能使用原來的連接資源,這樣就大大降低了網絡IO的開銷(因為每次創建、釋放連接的時候都需要握手、揮手的過程);而且上面也提到了內存資源相對於IO及計算資源相對便宜一些,雖然連接池占用了一定的內存資源,但是通過這種方式我們節約了網絡IO的開銷,在很大程度上能使系統保持穩定。
常見的連接池:數據庫連接池、redis連接池、HTTP連接池;當然代碼級別中我們常見的有進程池、線程池(線程與進程嚴格意義上不算是連接啦,是操作系統中的資源)等等。
HTTP連接池使用前提
HTTP連接池使實現的效果其實是連接可以復用,那復用的前提是連接一直存在,所以:需要服務端與客戶端都支持長鏈接!只要有一方斷開了連接那么這個連接就不能復用了!
實際業務中的一個案例
使用go-retryablehttp包實現http“鏈接池”效果
HTTP連接池的參數實驗(一)
默認值說明
這里用一個例子簡單說明一下:

package main import ( "flag" "fmt" "net/http" "os" "os/signal" "syscall" "time" "github.com/davecgh/go-spew/spew" "github.com/sirupsen/logrus" ) // TODO 參考文章鏈接:https://xujiahua.github.io/posts/20200723-golang-http-reuse/ // TODO 計算客戶端host(IP+Port)的數量 var m = make(map[string]int) var ch = make(chan string, 10) // TODO 計算鏈接數量 func count() { for s := range ch { m[s]++ } } func home(w http.ResponseWriter, r *http.Request) { logrus.Info(r.RemoteAddr) // TODO 最后打印的是 remoteAddr ch <- r.RemoteAddr // time.Sleep(time.Second) w.Write([]byte("helloworld")) } func init() { logrus.SetFormatter(&logrus.TextFormatter{ DisableColors: true, FullTimestamp: true, }) } func graceClose() { c := make(chan os.Signal) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { <-c close(ch) time.Sleep(time.Second) spew.Dump(m) os.Exit(0) }() } func main() { graceClose() go count() port := flag.Int("port", 8087, "") flag.Parse() logrus.Println("Listen port:", *port) http.HandleFunc("/", home) if err := http.ListenAndServe(fmt.Sprintf(":%d", *port), nil); err != nil { panic(err) } }

package main import ( "io/ioutil" "net/http" "testing" "time" ) var _httpCli = &http.Client{ Timeout: time.Duration(15) * time.Second, Transport: &http.Transport{ MaxIdleConns: 1, MaxIdleConnsPerHost: 1, MaxConnsPerHost: 1, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, }, } func get(url string) { resp, err := _httpCli.Get(url) if err != nil { // do nothing return } defer resp.Body.Close() _, err = ioutil.ReadAll(resp.Body) if err != nil { // do nothing return } } func TestLongShort(t *testing.T) { go func() { for i := 0; i < 100; i++ { if i%10 == 0 { time.Sleep(time.Second) } go get("http://127.0.0.1:8087") } }() go func() { for i := 0; i < 100; i++ { if i%10 == 0 { time.Sleep(time.Second) } go get("http://127.0.0.1:8088") } }() time.Sleep(time.Second * 10) } func TestLongLong(t *testing.T) { go func() { for i := 0; i < 100; i++ { if i%10 == 0 { time.Sleep(time.Second) } go get("http://127.0.0.1:8087") } }() go func() { for i := 0; i < 100; i++ { if i%10 == 0 { time.Sleep(time.Second) } go get("http://127.0.0.1:8089") } }() time.Sleep(time.Second * 10) } func TestLong(t *testing.T) { go func() { for i := 0; i < 100; i++ { if i%10 == 0 { time.Sleep(time.Second) } go get("http://127.0.0.1:8087") } }() time.Sleep(time.Second * 10) }
案例一:
客戶端連接池參數如下:
啟動項目:
運行 TestLong 這個函數,可以看到,客戶端只用了一個TCP連接去處理請求:
案例二:
客戶端連接池設置參數如下:
再運行 TestLong 結果如下:
在客戶端程序運行期間,也可以使用netstat命令看看效果:
案例三:
客戶端連接池設置如下:最大連接數設置為2,兩個空閑的配置為1
再運行 TestLong 結果如下:也就是說,超過了最大連接數,設置了空閑連接數的話,會自動再開一個鏈接處理請求,而不是等“最大連接數的計數-1”~
案例四:多個host連接(一)
MaxIdleConns vs MaxIdleConnsPerHost 兩個連接池
如下源碼,先檢查 PerHost 的池子有沒有滿,再檢查總的池子有沒有滿。也就是說,MaxIdleConns設置不合理,會對MaxIdleConnsPerHost有影響。

// tryPutIdleConn adds pconn to the list of idle persistent connections awaiting // a new request. // If pconn is no longer needed or not in a good state, tryPutIdleConn returns // an error explaining why it wasn't registered. // tryPutIdleConn does not close pconn. Use putOrCloseIdleConn instead for that. func (t *Transport) tryPutIdleConn(pconn *persistConn) error { ... idles := t.idleConn[key] if len(idles) >= t.maxIdleConnsPerHost() { // 如果超過了maxIdleConnsPerHost,報連接太多,當前pconn被關掉。 return errTooManyIdleHost } for _, exist := range idles { if exist == pconn { log.Fatalf("dup idle pconn %p in freelist", pconn) } } t.idleConn[key] = append(idles, pconn) t.idleLRU.add(pconn) if t.MaxIdleConns != 0 && t.idleLRU.len() > t.MaxIdleConns { oldest := t.idleLRU.removeOldest() // 如果超過了MaxIdleConns,殺掉老的idle connection oldest.close(errTooManyIdle) t.removeIdleConnLocked(oldest) } ...
客戶端參數配置如下:
現在需要運行2個服務端:
然后客戶端執行 TestLongLong 結果如下,都有不斷重建的情況:
案例五:多個host連接(二)
客戶端這樣配置連接池參數:
現在需要運行2個服務端:
然后客戶端執行 TestLongLong 結果如下,兩個客戶都端維持一個鏈接:
HTTP連接池的參數實驗(二)客戶端連接復用需要Client與Server同時支持
服務間接口調用,維持穩定數量的長連接,對性能非常有幫助。
幾個參數:
MaxIdleConnsPerHost:優先設置這個,決定了對於單個Host需要維持的連接池大小。該值的合理確定,應該根據性能測試的結果調整。
MaxIdleConns:客戶端連接單個Host,不少於MaxIdleConnsPerHost大小,不然影響MaxIdleConnsPerHost控制的連接池;客戶端連接 n 個Host,少於 n X MaxIdleConnsPerHost 會影響MaxIdleConnsPerHost控制的連接池(導致連接重建)。嫌麻煩,建議設置為0,不限制。
MaxConnsPerHost:對於單個Host允許的最大連接數,包含IdleConns,所以一般大於等於MaxIdleConnsPerHost。設置為等於MaxIdleConnsPerHost,也就是盡可能復用連接池中的連接。另外設置過小,可能會導致並發下降,超過這個值會 block 請求,直到有空閑連接。(所以默認值是不限制的)
服務端不支持長鏈接的情況
任何一方主動關閉連接,連接就無法復用。
客戶端參數:
運行服務端的8088:(會主動斷開鏈接!)
然后運行一下8087(不會主動斷開鏈接):
然后客戶端執行 TestLongShort 結果如下,8087這個客戶端還是維持一個鏈接:
很明顯:8088端口服務端每次鏈接都斷開的話~客戶端每次會用不同的鏈接:
客戶端不獲取響應體數據鏈接也無法復用
連接池設置的沒問題:
但是,客戶端不獲取響應數據:
看一下8087端口的結果:最終還是創建了好多個鏈接!!!
參考文章
Tuning the Go HTTP Client Settings for Load Testing
Build a TCP Connection Pool From Scratch With Go
查看 mac os 系統本地端口連接數,記一次ES client驗證
通過實例理解Go標准庫http包是如何處理keep-alive連接的
LINUX下解決netstat查看TIME_WAIT狀態過多問題 ** netstat命令
netstat -an|awk '/tcp/ {print $6}'|sort|uniq -c
~~~