Golang net-http 性能優化

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

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

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

如果我們在 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 個請求。

這裏我用 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() 
}
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 {}
}
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 客戶端的一些默認設置,以達到高性能的目的。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://gocn.vip/topics/11970