Go語言 http-Client 的連接行爲控制詳解

1. http 包默認客戶端

Go 語言以 “自帶電池” 聞名 [1],很多開發者對 Go 自帶的功能豐富的標準庫喜愛有加。而在 Go 標準庫中,net/http 包又是最受歡迎和最常用的包之一,我們用幾行代碼就能生成一個支持大併發、性能中上的 http server。而 http.Client 也是用途最爲廣泛的 http 客戶端,其性能也可以滿足多數情況下的需求。知名女 gopherJaana Dogan[2] 開源的類 apache ab[3] 的 http 性能測試工具 hey[4] 也是直接使用的 http.Client,而沒有用一些性能更好的第三方庫(比如:fasthttp[5])。

使用 http 包實現 http 客戶端的最簡單方法如下 (來自 http 包的官方文檔 [6]):

resp, err := http.Get("http://example.com/")
...
resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf)
...

注:別忘了在 Get 或 Post 成功後,調用 defer resp.Body.Close()。

在 http 包的 Get 和 Post 函數背後,真正完成 http 客戶端操作的是 http 包原生內置的 DefaultClient:

// $GOROOT/src/net/http/client.go
// DefaultClient is the default Client and is used by Get, Head, and Post.
var DefaultClient = &Client{}

下面是一個使用 DefaultClient 的例子,我們先來創建一個特殊的 http server:

// github.com/bigwhite/experiments/blob/master/http-client/default-client/server.go

package main

import (
 "log"
 "net/http"
 "time"
)

func Index(w http.ResponseWriter, r *http.Request) {
 log.Println("receive a request from:", r.RemoteAddr, r.Header)
 time.Sleep(10 * time.Second)
 w.Write([]byte("ok"))
}

func main() {
 var s = http.Server{
  Addr:    ":8080",
  Handler: http.HandlerFunc(Index),
 }
 s.ListenAndServe()
}

我們看到這個 http server 的 “不同之處” 在於它不急於回覆 http 應答,而是在接收請求 10 秒後再回復應答。下面是我們的 http client 端的代碼:

// github.com/bigwhite/experiments/blob/master/http-client/default-client/client.go

package main

import (
 "fmt"
 "io"
 "net/http"
 "sync"
)

func main() {
 var wg sync.WaitGroup
 wg.Add(256)
 for i := 0; i < 256; i++ {
  go func() {
   defer wg.Done()
   resp, err := http.Get("http://localhost:8080")
   if err != nil {
    panic(err)
   }
   defer resp.Body.Close()
   body, err := io.ReadAll(resp.Body)
   fmt.Println(string(body))
  }()
 }
 wg.Wait()
}

上面的客戶端創建了 256 個 goroutine,每個 goroutine 向 server 建立一條連接,我們先啓動 server,然後再運行一下上面的這個客戶端程序:

$go run server.go
$$go run client.go
panic: Get "http://localhost:8080": dial tcp [::1]:8080: socket: too many open files

goroutine 25 [running]:
main.main.func1(0xc000128280)
 /Users/tonybai/Go/src/github.com/bigwhite/experiments/http-client/default-client/client.go:18 +0x1c7
created by main.main
 /Users/tonybai/Go/src/github.com/bigwhite/experiments/http-client/default-client/client.go:14 +0x78
exit status 2

我們看到上面的客戶端拋出了一個 panic,提示:打開文件描述符過多。

上面演示環境的 ulimit -n 的值爲 256

我們用一幅示意圖來描述上面例子中的情況:

儘管根據《通過實例理解 Go 標準庫 http 包是如何處理 keep-alive 連接的》一文我們知道,默認情況下,http 客戶端是會保持連接並複用到同一主機的服務的連接的。但由於上述示例中 server 的延遲 10s 迴應答的上下文,客戶端在默認情況下不會等待應答回來,而是嘗試建立新的連接去發送新的 http 請求。由於示例運行環境最大允許每個進程打開 256 個文件描述符,因此在客戶端後期向服務端建立連接時,就會出現 “socket: too many open files” 的錯誤。

2. 定義在小範圍應用的 http 客戶端實例

那麼我們該如何控制客戶端的行爲以避免在資源受限的上下文情況下完成客戶端的發送任務呢?我們通過設置 http.DefaultClient 的相關屬性來實現這一點,但 DefaultClient 是包級變量,在整個程序中是共享的,一旦修改其屬性,其他使用 http 默認客戶端的包也會受到影響。因此更好的方案是定義一個在小範圍應用的 http 客戶端實例。

代碼:

resp, err := http.Get("http://example.com/")
...
resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf)
...

等價於如下代碼:

client := &http.Client{} // 自定義一個http客戶端實例
resp, err := client.Get("http://example.com/")
...
resp, err := client.Post("http://example.com/upload", "image/jpeg", &buf)
...

不同的是我們自定義的 http.Client 實例的應用範圍僅限於上述特定範圍,不會對其他使用 http 默認客戶端的包產生任何影響。不過此時我們自定義的 http.Client 實例 client 的行爲與 DefaultClient 的無異,要想解決上面示例 panic 的問題,我們還需對自定義的新客戶端實例做一進步行爲定製。

3. 定製到某一 host 的最大連接數

上述示例的最大問題在於向 server 端建立的連接數不受控制,即便將每個進程可以打開的最大文件描述符個數調大,客戶端還可能會遇到最大向外建立的 65535 個連接的極限瓶頸 (客戶端 socket 端口用盡),因此一個嚴謹的客戶端需要設置到某個 host 的最大連接數限制。

那麼,http.Client 是如何控制到某個 host 的最大連接數的呢?http 包的 Client 結構如下:

//$GOROOT/src/net/http/client.go

type Client struct {
        // Transport specifies the mechanism by which individual
        // HTTP requests are made.
        // If nil, DefaultTransport is used.
        Transport RoundTripper

 CheckRedirect func(req *Request, via []*Request) error
 Jar CookieJar
 Timeout time.Duration

Client 結構體一共四個字段,能控制 Client 連接行爲的是 Transport 字段。如果 Transport 的值爲 nil,那麼 Client 的連接行爲遵守 DefaultTransport 的設置:

// $GOROOT/src/net/http/transport.go

var DefaultTransport RoundTripper = &Transport{
        Proxy: ProxyFromEnvironment,
        DialContext: (&net.Dialer{
                Timeout:   30 * time.Second,
                KeepAlive: 30 * time.Second,
        }).DialContext,
        ForceAttemptHTTP2:     true,
        MaxIdleConns:          100,
        IdleConnTimeout:       90 * time.Second,
        TLSHandshakeTimeout:   10 * time.Second,
        ExpectContinueTimeout: 1 * time.Second,
}

不過在這份 DefaultTransport 的 “配置” 中,並沒有有關向某個 host 建立最大連接數的設置,因爲在 Transport 結構體中,起到這個作用的字段是 MaxConnsPerHost:

// $GOROOT/src/net/http/transport.go

type Transport struct {
 ... ...

 // 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
 ... ...
}

我們來改造一下上面的示例:

// github.com/bigwhite/experiments/blob/master/http-client/client-with-maxconnsperhost/client.go

package main

import (
 "fmt"
 "io"
 "net/http"
 "sync"
)

func main() {
 var wg sync.WaitGroup
 wg.Add(256)
 tr := &http.Transport{
  MaxConnsPerHost: 5,
 }
 client := http.Client{
  Transport: tr,
 }
 for i := 0; i < 256; i++ {
  go func(i int) {
   defer wg.Done()
   resp, err := client.Get("http://localhost:8080")
   if err != nil {
    panic(err)
   }
   defer resp.Body.Close()
   body, err := io.ReadAll(resp.Body)
   fmt.Printf("g-%d: %s\n", i, string(body))
  }(i)
 }
 wg.Wait()
}

上面的代碼不再使用 DefaultClient,而是自定義了一個新 Client 實例,並設置該實例的 Transport 字段爲我們新建的設置了 MaxConsPerHost 字段的 Transport 實例。將 server 啓動,並執行上面 client.go,我們從 server 端看到如下結果:

$go run server.go


receive a request from: [::1]:63677 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:63675 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:63676 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:63673 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:63674 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]


receive a request from: [::1]:63673 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:63675 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:63674 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:63676 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:63677 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]


receive a request from: [::1]:63677 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:63674 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:63676 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:63675 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:63673 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]

我們看到:客戶端一共向 server 端建立了 5 條連接 (客戶端端口號從 63673 到 63677),並且每隔 10s,客戶端複用這 5 條連接發送下一批請求。

http.Transport 維護了到每個 server host 的計數器 connsPerHost 和請求等待隊列:

// $GOROOT/src/net/http/transport.go
type Transport struct {
 ... ...
        connsPerHostMu   sync.Mutex
        connsPerHost     map[connectMethodKey]int
        connsPerHostWait map[connectMethodKey]wantConnQueue // waiting getConns
 ... ...
}

Transport 結構體使用了一個 connectMethodKey 結構作爲 key:

// $GOROOT/src/net/http/transport.go
type connectMethodKey struct {
        proxy, scheme, addr string
        onlyH1              bool
}

我們看到 connectMethodKey 使用一個四元組 (proxy,scheme,addr, onlyH1) 來唯一標識一個“host”。通常對一個 Client 實例而言,proxy,scheme 和 onlyH1 都是相同的,不同的是 addr(ip+port),因此實際上也就是按 addr 區分 host。我們同樣用一幅示意圖描示意一下這種情況:

4. 設定 idle 池的大小

不知道大家是否想到這點:當上面示例中的到某一個 host 的五個鏈接沒那麼繁忙時,依舊保持這個五個鏈接是不是有些浪費資源呢?至少佔用着客戶端端口以及服務端的文件描述符資源。我們是否能讓客戶端在閒時減少保持的到服務端的鏈接數量呢?我們可以通過 Transport 結構體類型中的 MaxIdleConnsPerHost 字段實現這一點。

其實如果你不顯式設置 MaxIdleConnsPerHost,http 包也會使用其默認值 (2):

// $GOROOT/src/net/http/transport.go

// DefaultMaxIdleConnsPerHost is the default value of Transport's
// MaxIdleConnsPerHost.
const DefaultMaxIdleConnsPerHost = 2

我們用一個例子來驗證 http.Client 的這一行爲!

首先我們改變一下 server 端的行爲,將原先的 “等待 10s” 改爲立即返回應答:

// github.com/bigwhite/experiments/blob/master/http-client/client-with-maxidleconnsperhost/server.go

package main

import (
 "fmt"
 "net/http"
)

func Index(w http.ResponseWriter, r *http.Request) {
 fmt.Println("receive a request from:", r.RemoteAddr, r.Header)
 w.Write([]byte("ok"))
}

func main() {
 var s = http.Server{
  Addr:    ":8080",
  Handler: http.HandlerFunc(Index),
 }
 s.ListenAndServe()
}

而對於 client,我們需要精心設計一下:

// github.com/bigwhite/experiments/blob/master/http-client/client-with-maxidleconnsperhost/client.go
package main

import (
 "fmt"
 "io"
 "net/http"
 "sync"
 "time"
)

func main() {
 var wg sync.WaitGroup
 wg.Add(5)
 tr := &http.Transport{
  MaxConnsPerHost:     5,
  MaxIdleConnsPerHost: 3,
 }
 client := http.Client{
  Transport: tr,
 }
 for i := 0; i < 5; i++ {
  go func(i int) {
   defer wg.Done()
   resp, err := client.Get("http://localhost:8080")
   if err != nil {
    panic(err)
   }
   defer resp.Body.Close()
   body, err := io.ReadAll(resp.Body)
   fmt.Printf("g-%d: %s\n", i, string(body))
  }(i)
 }
 wg.Wait()

 time.Sleep(10 * time.Second)

 wg.Add(5)
 for i := 0; i < 5; i++ {
  go func(i int) {
   defer wg.Done()

   for i := 0; i < 100; i++ {
    resp, err := client.Get("http://localhost:8080")
    if err != nil {
     panic(err)
    }
    defer resp.Body.Close()
    body, err := io.ReadAll(resp.Body)
    fmt.Printf("g-%d: %s\n", i+10, string(body))
    time.Sleep(time.Second)
   }
  }(i)
 }
 wg.Wait()
}

我們首先製造一次忙碌的發送行爲 (21~32 行),使得 client 端建滿 5 個連接;然後等待 10s,即讓 client 閒下來;之後再建立 5 個 groutine,以每秒一條的速度向 server 端發送請求 (不忙的節奏),我們來看看程序運行後服務端的輸出:

$go run server.go
receive a request from: [::1]:56244 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56246 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56242 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56243 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56245 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]

receive a request from: [::1]:56242 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56243 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56244 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56242 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56244 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]

receive a request from: [::1]:56243 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56242 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56244 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56243 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56244 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]

receive a request from: [::1]:56244 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56242 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56243 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56244 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56243 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]


receive a request from: [::1]:56244 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56242 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56243 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56244 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56243 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]

... ...

我們來分析一下:

這就是 MaxIdleConnsPerHost 的作用:最初 “忙時” 建立的 5 條連接,在 client 進入閒時時要進入 idle 狀態。但 MaxIdleConnsPerHost 的值爲 3,也就是說只有 3 條連接可以進入 idle 池,而另外兩個會被 close 掉。於是源端口號爲 56242、56243 和 56244 的三條連接被保留了下來。

下面是這節例子的示意圖:

Transport 結構體還有一個字段與 idle 池有關,那就是 MaxIdleConns,不同於 MaxIdleConnsPerHost 只針對某個 host,MaxIdleConns 是針對整個 Client 的所有 idle 池中的連接數的和,這個和不能超過 MaxIdleConns。

5. 清理 idle 池中的連接

如果沒有其他設定,那麼一個 Client 到一個 host 在閒時至少會保持 DefaultMaxIdleConnsPerHost 個 idle 連接 (前提是之前已經建立了 2 條或 2 條以上的連接),但如果 Client 針對這個 host 一直就保持無流量的狀態,那麼 idle 池中的連接也是一種資源浪費。於是 Transport 又提供了 IdleConnTimeout 字段用於超時清理 idle 池中的長連接。下面的示例複用上面的 server,但 client.go 改爲如下形式:

// github.com/bigwhite/experiments/blob/master/http-client/client-with-idleconntimeout/client.go

package main

import (
 "fmt"
 "io"
 "net/http"
 "sync"
 "time"
)

func main() {
 var wg sync.WaitGroup
 wg.Add(5)
 tr := &http.Transport{
  MaxConnsPerHost:     5,
  MaxIdleConnsPerHost: 3,
  IdleConnTimeout:     10 * time.Second,
 }
 client := http.Client{
  Transport: tr,
 }
 for i := 0; i < 5; i++ {
  go func(i int) {
   defer wg.Done()
   resp, err := client.Get("http://localhost:8080")
   if err != nil {
    panic(err)
   }
   defer resp.Body.Close()
   body, err := io.ReadAll(resp.Body)
   fmt.Printf("g-%d: %s\n", i, string(body))
  }(i)
 }
 wg.Wait()

 time.Sleep(5 * time.Second)

 wg.Add(5)
 for i := 0; i < 5; i++ {
  go func(i int) {
   defer wg.Done()
   for i := 0; i < 2; i++ {
    resp, err := client.Get("http://localhost:8080")
    if err != nil {
     panic(err)
    }
    defer resp.Body.Close()
    body, err := io.ReadAll(resp.Body)
    fmt.Printf("g-%d: %s\n", i+10, string(body))
    time.Sleep(time.Second)
   }
  }(i)
 }

 time.Sleep(15 * time.Second)
 wg.Add(5)

 for i := 0; i < 5; i++ {
  go func(i int) {
   defer wg.Done()
   for i := 0; i < 100; i++ {
    resp, err := client.Get("http://localhost:8080")
    if err != nil {
     panic(err)
    }
    defer resp.Body.Close()
    body, err := io.ReadAll(resp.Body)
    fmt.Printf("g-%d: %s\n", i+20, string(body))
    time.Sleep(time.Second)
   }
  }(i)
 }
 wg.Wait()
}

這個 client.go 代碼分爲三部分:首先和上個示例一樣,我們首先製造一次忙碌的發送行爲 (22~33 行),使得 client 端建滿 5 個連接;然後等待 5s,即讓 client 閒下來;之後再建立 5 個 groutine,以每秒一條的速度向 server 端發送請求 (不忙的節奏);第三部分同樣是先等待 15s,然後創建 5 個 goroutine 分別以不忙的節奏向 server 端發送請求。我們來看看程序運行後服務端的輸出:

$go run server.go

receive a request from: [::1]:52484 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52488 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52486 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52485 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52487 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]

receive a request from: [::1]:52487 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52488 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52484 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52484 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52487 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]

receive a request from: [::1]:52487 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52488 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52484 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52487 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52484 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]

receive a request from: [::1]:52542 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52544 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52545 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52543 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52546 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]

receive a request from: [::1]:52542 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52544 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52545 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52542 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52544 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]

... ...

這裏摘錄 5 段輸出。和預想的一樣,第一段 client 向 server 建立了 5 條連接 (客戶端的端口號從 52484~52487);暫停 5s 後,新創建的 5 個 goroutine 通過 idle 池中的三條保持的連接向 server 發送請求 (第二段和第三段,端口號: 52484、52487、52488);之後暫停 15s,由於設置了 IdleConnTimeout,idle 池中的三條連接也被 close 掉了。這時再發送請求,client 會重新建立連接(第四段,端口號 52542~52546),最後一段則又開始通過 idle 池中的三條保持的連接向 server 發送請求了 (端口號:52542、52544 和 52545)。

6. 其他控制項

如果覺得 idle 池超時清理依舊會佔用 “資源” 一小會兒,那麼可以利用 Transport 的 DisableKeepAlives 使得每個請求都創建一個新連接,即不復用 keep-alive 連接。當然這種控制設定在忙時導致的頻繁建立新連接的損耗可是要比佔用一些 “資源” 來的更大。示例可參考 github.com/bigwhite/experiments/blob/master/http-client/client-with-disablekeepalives,這裏就不貼出來了。

另外像本文開始示例中 server 那樣等待 10s 纔回應答的行爲可不是所有 client 端都能接受的,爲了限定應答及時返回,client 端可以設定等待應答的超時時間,如果超時,client 將返回失敗。http.Client 結構中的 Timeout 可以實現這一特性。示例可參考 github.com/bigwhite/experiments/blob/master/http-client/client-with-timeout,這裏同樣不貼出來了。

本文涉及的代碼可以在這裏 [7] 下載:https://github.com/bigwhite/experiments/blob/master/http-client。


參考資料

[1]  Go 語言以 “自帶電池” 聞名: https://www.imooc.com/read/87/article/2341

[2]  Jaana Dogan: https://github.com/rakyll

[3]  apache ab: https://httpd.apache.org/docs/2.4/programs/ab.html

[4]  http 性能測試工具 hey: https://github.com/rakyll/hey

[5]  fasthttp: https://github.com/valyala/fasthttp

[6]  http 包的官方文檔: https://tip.golang.org/pkg/net/http/

[7]  這裏: https://github.com/bigwhite/experiments/blob/master/http-client

[8]  改善 Go 語⾔編程質量的 50 個有效實踐: https://www.imooc.com/read/87

[9]  Kubernetes 實戰:高可用集羣搭建、配置、運維與應用: https://coding.imooc.com/class/284.html

[10]  我愛發短信: https://51smspush.com/

[11]  鏈接地址: https://m.do.co/c/bff6eed92687

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/_2NI_X6dXhGYlgZuiSWskA