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 客戶端的一些默認設置,以達到高性能的目的。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://gocn.vip/topics/11970