Go HTTP 重用底層 TCP 連線需要注意的關鍵點

jemygraw發表於2020-06-29

前言

在寫這篇文章之前,我在社群搜尋了一下,找到了一個相關的帖子 can't assign requested address 錯誤解決,還是 @astaxie 自己寫的。當然這裡我之所以重複再寫一個新帖子,是希望給大家提供一種新的驗證的方式。

問題

有一次我在看某個專案(可能是 kafka 吧,記不清楚了)的原始碼的時候,我發現它的註釋裡面特別提到一句話,說是要讀取完 http.ResponseBody並關閉它,否則不會重用底層的 TCP 連線。我想了想為什麼它這裡一定要特別提出來呢?關閉 http.Response 不是一個常識性動作麼?比如一般寫程式碼我們都會遵循下面的模式:

resp, err := http.Get("http://www.example.com")
if err != nil {
    return err
}
defer resp.Body.Close()

respBody, err := ioutil.ReadAll(resp.Body)
// ...

在結合實際的場景之後,我發現其實有的時候問題出在我們並不總是會去讀取完整個http.ResponseBody。為什麼這麼說呢?

在常見的 API 開發的業務邏輯中,我們會定義一個 JSON 的物件來反序列化 http.ResponseBody,但是通常在反序列化這個回覆之前,我們會做一些 http 的 StatusCode 檢查,比如當 StatusCode200 的時候,我們才去讀取 http.ResponseBody,如果不是 200,我們就直接返回一個包裝好的錯誤。比如下面的模式:

resp, err := http.Get("http://www.example.com")
if err != nil {
    return err
}
defer resp.Body.Close()

if resp.StatusCode == http.StatusOK {
    var apiRet APIRet
    decoder := json.NewDecoder(resp.Body)
    err := decoder.Decode(&apiRet)
    // ...
}

如果程式碼是按照上面的這種方式寫的話,那麼在請求異常的時候,會導致大量的底層 TCP 無法重用,所以我們稍微改進下就可以了。

resp, err := http.Get("http://www.example.com")
if err != nil {
    return err
}
defer resp.Body.Close()

if resp.StatusCode == http.StatusOK {
    var apiRet APIRet
    decoder := json.NewDecoder(resp.Body)
    err := decoder.Decode(&apiRet)
    // ...
}else{
    io.Copy(ioutil.Discard, resp.Body)
    // ...
}

我們通過直接將 http.ResponseBody 丟棄掉就可以了。

原因

在 Go 的原始碼中,關於這個問題有特別的註釋。

// Body represents the response body.
//
// The response body is streamed on demand as the Body field
// is read. If the network connection fails or the server
// terminates the response, Body.Read calls return an error.
//
// The http Client and Transport guarantee that Body is always
// non-nil, even on responses without a body or responses with
// a zero-length body. It is the caller's responsibility to
// close Body. The default HTTP client's Transport may not
// reuse HTTP/1.x "keep-alive" TCP connections if the Body is
// not read to completion and closed.
//
// The Body is automatically dechunked if the server replied
// with a "chunked" Transfer-Encoding.
//
// As of Go 1.12, the Body will also implement io.Writer
// on a successful "101 Switching Protocols" response,
// as used by WebSockets and HTTP/2's "h2c" mode.
Body io.ReadCloser

其中提到了必須將 http.ResponseBody 讀取完畢並且關閉後,才會重試底層的 TCP 連線。

實驗

為了驗證一把上面的問題,我們寫了一個簡單的對比實驗,並且通過 Wireshark 抓包分析了一下。這裡使用的是 https://www.oschina.net 作為例子,由於這個站點用的是 HTTPS,所以重用了 TCP 的話,那麼一次建立 TLS 連線後面就不用重建了,非常方便觀察。

重用了 TCP 連線

package main

import (
    "net/http"
)

func main() {
    count := 100
    for i := 0; i < count; i++ {
        resp, err := http.Get("https://www.oschina.net")
        if err != nil {
            panic(err)
        }
        defer resp.Body.Close()
        io.Copy(ioutil.Discard, resp.Body)
    }
}

未重用 TCP 連線

package main

import (
    "net/http"
)

func main() {
    count := 100
    for i := 0; i < count; i++ {
        resp, err := http.Get("https://www.oschina.net")
        if err != nil {
            panic(err)
        }
        defer resp.Body.Close()
        //io.Copy(ioutil.Discard, resp.Body)
    }
}

小結

學無止境,小心翼翼。

備份連結:Go HTTP 重用底層 TCP 連線需要注意的關鍵點

更多原創文章乾貨分享,請關注公眾號
  • Go HTTP 重用底層 TCP 連線需要注意的關鍵點
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章