說明
Http 連接是很珍貴的資源,其基于 Tcp ,Http1.1 的 Connection: keep-alive 使得 Http 客戶端在發(fā)完一個請求后并不會立即關(guān)閉 Tcp 連接,它會繼續(xù)等待一段時間,如果該連接上又有新的 Http 請求,就復(fù)用這個連接。
Tcp 主動關(guān)閉的一方會有 TIME_WAIT 狀態(tài),該狀態(tài)會占用端口資源,如果服務(wù)器在短時間內(nèi)發(fā)起大量的外部 Http 請求,將會積壓很多 TIME_WAIT 連接,消耗服務(wù)器資源,甚至可能導(dǎo)致一些不可預(yù)料的bug。
所以,復(fù)用 Http 請求連接就顯得格外重要,當(dāng)然,Golang 是支持我們復(fù)用這個連接的,原理就是利用 http.Client 的參數(shù) Transport 。
Transport is an implementation of RoundTripper that supports HTTP, HTTPS, and HTTP proxies (for either HTTP or HTTPS with CONNECT).
By default, Transport caches connections for future re-use.
客戶端測試
type Transport struct {
.......
// MaxIdleConns controls the maximum number of idle (keep-alive)
// connections across all hosts. Zero means no limit.
MaxIdleConns int
// MaxIdleConnsPerHost, if non-zero, controls the maximum idle
// (keep-alive) connections to keep per-host. If zero,
// DefaultMaxIdleConnsPerHost is used.
MaxIdleConnsPerHost int
// MaxConnsPerHost optionally limits the total number of
// connections per host, including connections in the dialing,
// active, and idle states. On limit violation, dials will block.
//
// Zero means no limit.
MaxConnsPerHost int
// IdleConnTimeout is the maximum amount of time an idle
// (keep-alive) connection will remain idle before closing
// itself.
// Zero means no limit.
IdleConnTimeout time.Duration
......
}
我們自定義 Transport ,設(shè)置 MaxConnsPerHost 參數(shù)即可限制每個 Host 的最大連接數(shù)。這里需要注意的是,IP:PORT 這樣一個元組區(qū)分不同的 Host 。
在多提一點,默認(rèn)的 Transport :
// DefaultTransport is the default implementation of Transport and is
// used by DefaultClient. It establishes network connections as needed
// and caches them for reuse by subsequent calls. It uses HTTP proxies
// as directed by the $HTTP_PROXY and $NO_PROXY (or $http_proxy and
// $no_proxy) environment variables.
var DefaultTransport RoundTripper = &Transport{
Proxy: ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
即默認(rèn) MaxConnsPerHost 是沒有限制的。
main.go:
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"sync"
"time"
)
func main() {
client := &http.Client{
Timeout: time.Second * 10,
Transport: &http.Transport{
MaxConnsPerHost:1,
},
}
var wg sync.WaitGroup
wg.Add(10)
for i := 0;i < 10 ;i++ {
go NewRequest(&wg,client)
}
wg.Wait()
fmt.Println("全部完成")
}
func NewRequest(wg *sync.WaitGroup,client *http.Client) {
defer wg.Done()
req,err := http.NewRequest("POST","http://*********:9502",nil)
if err != nil {
log.Fatal(err)
}
res,err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
content,err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(content))
}
抓包驗證
使用 Wireshark 抓包,tcp.port == 9502 過濾之后可以清晰的觀察到整個過程只有一次 TCP 的三次握手和四次揮手過程:

Follow Http 可以看到整個過程是串行的:

前提
復(fù)用連接的前提是客戶端,服務(wù)端同時支持,如果任意一方不支持,連接將不會被復(fù)用。調(diào)整服務(wù)端代碼:
s := &http.Server{
Addr: ":9502",
Handler: r, // < here Gin is attached to the HTTP server
// ReadTimeout: 10 * time.Second,
// WriteTimeout: 10 * time.Second,
// MaxHeaderBytes: 1 << 20,
}
s.SetKeepAlivesEnabled(false)
err := s.ListenAndServe()
if err != nil {
fmt.Println(err)
}
客戶端再測試抓包:

很明顯,連接沒有復(fù)用,仔細(xì)數(shù)的話有10次連接重建的過程。但在客戶端這邊是始終只保持一個連接的,后續(xù)請求都是在前面的連接關(guān)閉之后再發(fā)起的。
其他參數(shù)
MaxIdleConns 限制最大空閑連接數(shù),MaxIdleConnsPerHost 限制單個 Host 最大空閑連接數(shù)。
調(diào)整客戶端代碼:
client := &http.Client{
Timeout: time.Second * 10,
Transport: &http.Transport{
MaxIdleConnsPerHost:1,
MaxConnsPerHost:2,
IdleConnTimeout:time.Second * 2,
},
}
var wg sync.WaitGroup
wg.Add(6)
for i := 0;i < 2 ;i++ {
go NewRequest(&wg,client)
}
time.Sleep(time.Second * 3)
for i := 0;i < 2 ;i++ {
go NewRequest(&wg,client)
}
time.Sleep(time.Second * 3)
for i := 0;i < 2 ;i++ {
go NewRequest(&wg,client)
}
wg.Wait()
同時恢復(fù)服務(wù)端,支持連接復(fù)用,再抓包觀察:

可以明顯看到是兩個連接建立,各發(fā)起一個請求,然后兩個連接關(guān)閉;再重復(fù)該過程。
調(diào)整請求間隔:
client := &http.Client{
Timeout: time.Second * 10,
Transport: &http.Transport{
MaxIdleConnsPerHost:1,
MaxConnsPerHost:2,
IdleConnTimeout:time.Second * 2,
},
}
var wg sync.WaitGroup
wg.Add(6)
for i := 0;i < 2 ;i++ {
go NewRequest(&wg,client)
}
time.Sleep(time.Second)
for i := 0;i < 2 ;i++ {
go NewRequest(&wg,client)
}
time.Sleep(time.Second * 3)
for i := 0;i < 2 ;i++ {
go NewRequest(&wg,client)
}
wg.Wait()

這一次又有不同,這個是只 Sleep 1秒,由于 MaxIdleConnsPerHost=1 所以第一波兩個請求發(fā)完,必須關(guān)閉其中一個連接,沒有到達 IdleConnTimeout ,所以第一波的另外一個連接可以復(fù)用,只需創(chuàng)建一個新連接即可完成第二波請求。后面 Sleep 3秒,顯然都過了 Idle 時間,兩個連接都關(guān)閉了再發(fā)起最后一波請求。
總結(jié)
服務(wù)間接口調(diào)用,尤其是需要調(diào)用第三方接口的場景,復(fù)用連接對性能非常有幫助。合理設(shè)置相關(guān)參數(shù),做到”心中有數(shù)“。
2021-12-31