Golang net/http 效能優化

wxj95發表於2021-04-22

Golang net/http 效能優化

Go 語言內建net\http包十分優秀,我們通過這個包可以很方便的去實現 HTTP 的客戶端和服務端。

但是在高併發的情況下,如果我們使用預設的配置,會引發一些問題,嚴重的話可能會使伺服器崩潰。這裡講述以下兩種預設配置情況下帶來的一些問題。

  • #### 使用 DefaultClient
_, err := http.Get("http://www.baidu.com")
if err != nil {
   log.Fatal(err)
}
var DefaultClient = &Client{}

如果我們直接使用預設的 http,那麼它是沒有超時時間的。這樣就會帶來如下問題:

假設我們向服務端發起請求,但是服務端因為某些情況沒有及時返回或者說連線中斷了,那麼客戶端就會很長時間得不到服務端的 response。所以這個時候客戶端為這一個 tcp 連線申請的資源就得不到釋放,造成資源的浪費。如果在高併發的情況下,客戶端可能會因為資源的限制使得伺服器崩潰,比如達到最大檔案描述符或者達到埠號限制等等。

解決辦法是自己設定超時時間:

client := http.Client{
   Timeout: 10 * time.Second,
}
  • #### 使用預設的 DefaultTransport

如果我們在 http client 中沒有設定 transport 屬性,那麼它就會使用預設的 transport:

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,
}

從這個的配置中我們可以看到,http 使用了預設的連線池,關鍵的兩個屬性:

MaxIdleConns:最大空閒連線數量,預設為 100

IdleConnTimeout:空閒連線超時時間,預設為 90s

當一個 request 請求完成後,這個連線會保留,直到達到 IdleConnTimeout 設定的超時時間。如果沒有達到,那麼下一個請求就會複用這個連線。

這樣的空閒連線最大數量是 100 個,超過 100 的還是會建立新的連線。

建立連線池的好處是能夠儘可能減少伺服器的資源。這個配置看上去很好啊,那為什麼還是說會有問題呢?

檢視原始碼,它還有另外一個預設配置:

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

DefaultMaxIdleConnsPerHost 為每個 host 的設定的空閒連線數量為 2。

DefaultMaxIdleConnsPerHost 設定的太小就會導致一個問題,在大量請求的情況下去訪問特定的 host 的時候,長連線會退化成短連結。看如下原始碼:

idles := t.idleConn[key]
if len(idles) >= t.maxIdleConnsPerHost() {
   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)

從原始碼中我們可以看出,如果當併發量大的情況下,連線池會建立較多的 TCP 連線,並且在請求完成以後連線池嘗試通過 tryPutIdleConn 歸還空閒連線,對於超出 maxIdleConnsPerHost 數量的空閒長連線都不能再放回連線池了,這些連線會進入 TIME_WAIT 狀態,這些 TIME_WAIT 的連線在達到 2MSL 時間後就會自動關閉。

在這種情況下,我們在伺服器上就會看到大量的 TIME_WAIT 狀態的 tcp 連線。在極限的情況下,伺服器也可能會崩潰。

解決辦法是自己設定 DefaultMaxIdleConnsPerHost

t := http.DefaultTransport.(*http.Transport).Clone()
t.MaxIdleConns = 100
t.MaxConnsPerHost = 100
t.MaxIdleConnsPerHost = 100
client := http.Client{
   Timeout:   10 * time.Second,
   Transport: t,
}

程式碼演示

這裡我用程式碼演示使用 DefaultTransport 和不使用兩者的 tcp 連線狀態的區別,從而來驗證這個邏輯。

客戶端同時向服務端發起 100 個請求。

  • ###### server

這裡我用 gin web 框架快速起了一個服務端

package main

import (
   "github.com/gin-gonic/gin"
)

func main() {
   r := gin.Default()
   r.GET("/ping", func(c *gin.Context) {
      c.JSON(200, gin.H{
         "message": "pong",
      })
   })
   r.Run() 
}
  • ###### client1 使用預設的 DefaultTransport
package main

import (
  "fmt"
  "io/ioutil"
  "log"
  "net/http"
)

func main() {
  client := http.Client{
  }
  for i := 0; i < 100; i++ {
      go func() {
          resp, err := client.Get("http://127.0.0.1:8080/ping")
          if err != nil {
              log.Fatal(err)
          }
          b, err := ioutil.ReadAll(resp.Body)
          if err != nil {
              log.Fatal(err)
          }
          defer resp.Body.Close()
          fmt.Printf(string(b))
      }()
  }
  select {}
}

  • ###### client2 不使用預設的 DefaultTransport
package main

import (
  "fmt"
  "io/ioutil"
  "log"
  "net/http"
  "time"
)

func main() {
  t := http.DefaultTransport.(*http.Transport).Clone()
  t.MaxIdleConns = 100
  t.MaxIdleConnsPerHost = 100
  client := http.Client{
      Timeout:   10 * time.Second,
      Transport: t,
  }
  for i := 0; i < 100; i++ {
      go func() {
          resp, err := client.Get("http://127.0.0.1:8080/ping")
          if err != nil {
              log.Fatal(err)
          }
          b, err := ioutil.ReadAll(resp.Body)
          if err != nil {
              log.Fatal(err)
          }
          defer resp.Body.Close()
          fmt.Printf(string(b))
      }()
  }
  select {}
}

使用 DefaultTransport 的 tcp 連線情況:

如圖所示,在請求發出之後,我們可以看到只有兩個 tcp 連線是處於 ESTABLISHED 狀態,其他的都是處於 TIME_WAIT 狀態。且兩個處於 ESTABLISHED 狀態的 tcp 連線會在 90s 之後變成 TIME_WAIT 狀態。

90s 後:

使用 DefaultTransport 的 tcp 連線情況:

如圖所示,可以看出所有的 100 個 tcp 連線都是處於 ESTABLISHED 狀態,這些狀態在 90s 之後全部變成 TIME_WAIT 狀態。

90s 之後:

總結:

雖然我們平時開發中使用預設的配置也沒有遇到什麼問題,但是在高併發的條件下還是會帶來很多問題。

所以我們在高併發的情況下,儘量不要使用預設的配置,通過更改 HTTP 客戶端的一些預設設定,以達到高效能的目的。

更多原創文章乾貨分享,請關注公眾號
  • Golang net/http 效能優化
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章