Go net 超時處理

這篇文章詳細介紹了,net/http包中對應HTTP的各個階段,如何使用timeout來進行讀/寫超時控制以及服務端和客戶端支持設置的timeout類型。本質上,這些timeout都是代碼層面對各個函數設置的處理時間。好比,讀取客戶端讀取請求頭、讀取響應體的時間,本質上都是響應函數的超時時間。git

做者強烈不建議,在工做中使用net/http包上層封裝的默認方法(沒有明確設置timeout),很容易出現系統文件套接字被耗盡等使人悲傷的狀況。好比:github

// 相信工做中也不會出現這樣的代碼
func main() {
    http.ListenAndServe("127.0.0.1:3900", nil)
}

正文

在使用Go開發HTTP Serverclient的過程當中,指定timeout很常見,但也很容易犯錯。timeout錯誤通常還不容易被發現,可能只有當系統出現請求超時、服務掛起時,錯誤才被嚴肅暴露出來。golang

HTTP是一個複雜的多階段協議,因此也不存在一個timeout值適用於全部場景。想一下StreamingEndpointJSON APIComet, 不少狀況下,默認值根本不是咱們所須要的值。api

這篇博客中,我會對HTTP請求的各個階段進行拆分,列舉可能須要設置的timeout值。而後從客戶端和服務端的角度,分析它們設置timeout的不一樣方式。緩存

SetDeadline

首先,你須要知道Go所暴露出來的,用於實現timeout的方法:Deadline網絡

timeout自己經過 net.Conn包中的Set[Read|Write]Deadline(time.Time)方法來控制。Deadline是一個絕對的時間點,當鏈接的I/O操做超過這個時間點而沒有完成時,便會由於超時失敗。app

Deadlines不一樣於timeouts. 對一個鏈接而言,設置Deadline以後,除非你從新調用SetDeadline,不然這個Deadline不會變化。前面也提了,Deadline是一個絕對的時間點。所以,若是要經過SetDeadline來設置timeout,就不得不在每次執行Read/Write前從新調用它。tcp

你可能並不想直接調用SetDeadline方法,而是選擇 net/http提供的更上層的方法。但你要時刻記住:全部timeout操做都是經過設置Deadline實現的。每次調用,它們並不會去重置的deadlineide

Server Timeouts

關於服務端超時,這篇帖子So you want to expose Go on the Internet也介紹了不少信息,特別是關於HTTP/2Go 1.7 bugs的部分.函數

HTTP server phases

對於服務端而言,指定timeout相當重要。不然,一些請求很慢或消失的客戶端極可能致使系統文件描述符泄漏,最終服務端報錯:

http: Accept error: accept tcp [::]:80: accept4: too many open files; retrying in 5ms

在建立http.Server的時候,能夠經過ReadTimeoutWriteTimeout來設置超時。你須要明確的聲明它們:

srv := &http.Server{
    ReadTimeout: 5 * time.Second,
    WriteTimeout: 10 * time.Second,
}
log.Println(srv.ListenAndServe())

ReadTimeout指從鏈接被Accept開始,到request body被徹底讀取結束(若是讀取body的話,不然是讀取完header頭的時間)。內部是net/http經過在Accept調用SetReadDeadline實現的。

WriteTimeout通常指從讀取完header頭以後到寫完response的時間(又稱ServerHTTP的處理時間),內部經過在 readRequest以後調用SetWriteDeadline實現。

然而,若是是HTTPS的話,SetWriteDeadline方法在Accept後就被調用,因此TLS handshake也是WriteTimeout的一部分。同時,這也意味着(僅僅HTTPSWriteTimeout包括了讀header頭以及握手的時間。

爲了不不信任的client端或者網絡鏈接的影響,你應該同時設置這兩個值,來保證鏈接不被client長時間佔用。

最後,介紹一下http.TimeoutHandler,它並非一個Server屬性,它被用來Wrap http.Handler ,限制Handler處理請求的時長。它主要依賴緩存的response來工做,當超時發生時,響應503 Service Unavailable的錯誤。它在1.6存在問題,在1.6.2進行了修復

http.ListenAndServe is doing it wrong

順帶說一句,這也意味:使用一些內部封裝http.Server的包函數,好比http.ListenAndServe, http.ListenAndServeTLS以及http.Serve是不正規的,尤爲是直接面向外網提供服務的場合。

這種方法默認缺省配置timeout值,也沒有提供配置timeout的功能。若是你使用它們,可能就會面臨鏈接泄漏和文件描述符耗盡的風險。我也好幾回犯過這樣的錯誤。

相反的,建立一個http.Server應該像文章開頭例子中那樣,明確設置ReadTimeoutWriteTimeout,並使用相應的方法來使server更完善。

About streaming

Very annoyingly, there is no way of accessing the underlying net.Conn from ServeHTTPso a server that intends to stream a response is forced to unset the WriteTimeout (which is also possibly why they are 0 by default). This is because without net.Conn access, there is no way of calling SetWriteDeadline before each Write to implement a proper idle (not absolute) timeout.

Also, there's no way to cancel a blocked ResponseWriter.Write since ResponseWriter.Close (which you can access via an interface upgrade) is not documented to unblock a concurrent Write. So there's no way to build a timeout manually with a Timer, either.

Sadly, this means that streaming servers can't really defend themselves from a slow-reading client.

I submitted an issue with some proposals, and I welcome feedback there.

Client Timeouts

HTTP Client phases

client端的timeout能夠很簡單,也能夠很複雜,這徹底取決於你如何使用。但對於阻止內存泄漏或長時間鏈接佔用的問題上,相對於Server端來講,它一樣特別重要。

下面是使用http.Client指定timeout的最簡單例子。timeout覆蓋了整個請求的時間:從Dial(若是非鏈接重用)到讀取response body

c := &http.Client{
    Timeout: 15 * time.Second,
}
resp, err := c.Get("https://blog.filippo.io/")

像上面列舉的那些server端方法同樣,client端也封裝了相似的方法,好比http.Get。他內部用的就是一個沒有設置超時時間的Client

下面提供了不少類型的timeout,可讓你更精細的控制超時:

  • net.Dialer.Timeout用於限制創建TCP鏈接的時間,包括域名解析的時間在內(若是須要建立的話)
  • http.Transport.TLSHandshakeTimeout用於限制TLS握手的時間
  • http.Transport.ResponseHeaderTimeout用於限制讀取響應頭的時間(不包括讀取response body的時間)
  • http.Transport.ExpectContinueTimeout用於限制從客戶端在發送包含Expect: 100-continue請求頭開始,到接收到響應去繼續發送post data的間隔時間。注意:在1.6 HTTP/2 不支持這個設置(DefaultTransport從1.6.2起是一個例外 1.6.2).
c := &http.Client{
    Transport: &http.Transport{
        Dial: (&net.Dialer{
                Timeout:   30 * time.Second,
                KeepAlive: 30 * time.Second,
        }).Dial,
        TLSHandshakeTimeout:   10 * time.Second,
        ResponseHeaderTimeout: 10 * time.Second,
        ExpectContinueTimeout: 1 * time.Second,
    }
}

到目前爲止,尚未一種方式來限制發送請求的時間。讀響應體的時間能夠手動的經過設置time.Timer來實現,由於這個過程是在client方法返回以後發生的(後面介紹如何取消一個請求)。

最後,在1.7的版本中增長了http.Transport.IdleConnTimeout,用於限制鏈接池中空閒連持的存活時間。它不能用於控制阻塞階段的客戶端請求,

注:客戶端默認執行請求重定向(302等)。能夠爲每一個請求指定細粒度的超時時間,其中http.Client.Timeout包括了重定向在內的請求花費的所有時間。而http.Transport是一個底層對象,沒有跳轉的概念。

Cancel and Context

net/http 提供了兩種取消客戶端請求的方法: Request.Cancel以及在1.7版本中引入的Context.

Request.Cancel是一個可選的channel,若是設置了它,即可以經過關閉該channel來終止請求,就跟請求超時了同樣(它們的實現機制是相同的。在寫這篇博客的時候,我還發現了一個 1.7的 bug :全部被取消請求,返回的都是timeout超時錯誤)。

type Request struct {

    // Cancel is an optional channel whose closure indicates that the client
    // request should be regarded as canceled. Not all implementations of
    // RoundTripper may support Cancel.
    //
    // For server requests, this field is not applicable.
    //
    // Deprecated: Use the Context and WithContext methods
    // instead. If a Request's Cancel field and context are both
    // set, it is undefined whether Cancel is respected.
    Cancel <-chan struct{}
}

咱們可結合Request.Canceltime.Timertimeout進行更細的控制。好比,在咱們每次從response body中讀取數據後,延長timeout的時間。

package main

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

func main() {
    
    //定義一個timer:5s後取消該請求,即關閉該channel
    c := make(chan struct{})
    timer := time.AfterFunc(5*time.Second, func() {
        close(c)
    })

    // Serve 256 bytes every second.
    req, err := http.NewRequest("GET", "http://httpbin.org/range/2048?duration=8&chunk_size=256", nil)
    if err != nil {
        log.Fatal(err)
    }
    req.Cancel = c

    //執行請求,請求的時間不該該超過5s
    log.Println("Sending request...")
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()

    log.Println("Reading body...")
    for {
        timer.Reset(2 * time.Second)
        // Try instead: timer.Reset(50 * time.Millisecond)
        _, err = io.CopyN(ioutil.Discard, resp.Body, 256)
        if err == io.EOF {
            break
        } else if err != nil {
            log.Fatal(err)
        }
    }
}

上述例子中,咱們給Do設置了5s的超時,經過後續8個循環來讀取response body的內容,這個操做至少花費了8s的時間。每次read的操做均設置了2s的超時。咱們能夠持續這樣讀,不須要考慮任何阻塞的風險。若是在2s內沒有接受到數據,io.CopyN將會返回net/http: request canceled

1.7的版本中context被引入到了標註庫,此處是一些介紹。接下來咱們用它來替換 Request.Cancel,實現相同的功能。

使用context來取消一個請求,咱們須要獲取一個Context類型,以及調用context.WithCancel返回的cancel()方法,並經過Request.WithContextcontext綁定到一個請求上。當咱們想取消這個請求時,只須要調用cancel()方法(代替上述關閉channel的作法)

//ctx是context.TODO()的子節點
ctx, cancel := context.WithCancel(context.TODO())
timer := time.AfterFunc(5*time.Second, func() {
    cancel()
})

req, err := http.NewRequest("GET", "http://httpbin.org/range/2048?duration=8&chunk_size=256", nil)
if err != nil {
    log.Fatal(err)
}
req = req.WithContext(ctx)

Contexts有不少優,好比一個parent(傳遞給context.WithCancel的對象)被取消,那麼命令會沿着傳遞的路徑一直向下傳遞,直到關閉全部子context

博客地址:neojos welcome you
閱讀原文:The complete guide to Go net/http timeouts

相關文章
相關標籤/搜索