Golang Http連接池

說明

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 的三次握手和四次揮手過程:

47-1.png

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

47-2.png

前提

復(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)
    }

客戶端再測試抓包:

47-3.png

很明顯,連接沒有復(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ù)用,再抓包觀察:

47-4.png

可以明顯看到是兩個連接建立,各發(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()
47-5.png

這一次又有不同,這個是只 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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容