[譯]Go net/http 逾時機制完全手冊

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

目錄 [−]

  1. SetDeadline
  2. 伺服器端逾時設定
    1. http.ListenAndServe 的錯誤
    2. 關於流
  3. 用戶端逾時設定
  4. Cancel 和 Context

英文原始出處: The complete guide to Go net/http timeouts, 作者: Filippo Valsorda

當用Go寫HTTP的伺服器和用戶端的時候,逾時處理總是最易犯錯和最微妙的地方之一。錯誤可能來自很多地方,一個錯誤可能等待很長時間沒有結果,直到網路故障或者進程掛起。

HTTP是一個複雜的、多階段(multi-stage)協議,所以沒有一個放之四海而皆準的逾時解決方案,比如一個流服務、一個JSON API和一個Comet服務對逾時的需求都不相同, 往往預設值不是你想要的。

本文我將拆解需要逾時設定的各個階段,看看用什麼不同的方式去處理它, 包括伺服器端和用戶端。

SetDeadline

首先,你需要瞭解Go實現逾時的網路原語(primitive): Deadline (期限)。

net.Conn為Deadline提供了多個方法Set[Read|Write]Deadline(time.Time)。Deadline是一個絕對時間值,當到達這個時間的時候,所有的 I/O 操作都會失敗,返回逾時(timeout)錯誤。

Deadline不是逾時(timeout)。一旦設定它們永久生效(或者直到下一次調用SetDeadline), 不管此時串連是否被使用和怎麼用。所以如果想使用SetDeadline建立逾時機制,你不得不每次在Read/Write操作之前調用它。

你可能不想自己調用SetDeadline, 而是讓net/http代替你調用,所以你可以調用更進階的timeout方法。但是請記住,所有的逾時的實現都是基於Deadline, 所以它們不會每次接收或者發送重新設定這個值(so they do NOT reset every time data is sent or received)。

江南雨的指正:
應該是由於“Deadline是一個絕對時間值”,不是真的逾時機制,所以作者特別提醒,這個值不會自動重設的,需要每次手動設定。

伺服器端逾時設定

對於暴露在網上的伺服器來說,為用戶端串連設定逾時至關重要,否則巨慢的或者隱失的用戶端可能導致檔案控制代碼無法釋放,最終導致伺服器出現下面的錯誤:

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

http.Server有兩個設定逾時的方法: ReadTimeoutandWriteTimeout`。你可以顯示地設定它們:

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

ReadTimeout的時間計算是從串連被接受(accept)到request body完全被讀取(如果你不讀取body,那麼時間截止到讀完header為止)。它的內部實現是在Accept立即調用SetReadDeadline方法(程式碼)。

12345678
  ……  if d := c.server.ReadTimeout; d != 0 {c.rwc.SetReadDeadline(time.Now().Add(d))}if d := c.server.WriteTimeout; d != 0 {c.rwc.SetWriteDeadline(time.Now().Add(d))}  ……

WriteTimeout的時間計算正常是從request header的讀取結束開始,到 response write結束為止 (也就是 ServeHTTP 方法的聲明周期), 它是通過在readRequest方法結束的時候調用SetWriteDeadline實現的(程式碼)。

12345678910111213141516
func (c *conn) readRequest(ctx context.Context) (w *response, err error) {if c.hijacked() {return nil, ErrHijacked}if d := c.server.ReadTimeout; d != 0 {c.rwc.SetReadDeadline(time.Now().Add(d))}if d := c.server.WriteTimeout; d != 0 {defer func() {c.rwc.SetWriteDeadline(time.Now().Add(d))}()}  ……}

但是,當串連是HTTPS的時候,SetWriteDeadline會在Accept之後立即調用(代碼),所以它的時間計算也包括 TLS握手時的寫的時間。 討厭的是, 這就意味著(也只有這種情況) WriteTimeout設定的時間也包含讀取Headerd到讀取body第一個位元組這段時間。

12345678
if tlsConn, ok := c.rwc.(*tls.Conn); ok {if d := c.server.ReadTimeout; d != 0 {c.rwc.SetReadDeadline(time.Now().Add(d))}if d := c.server.WriteTimeout; d != 0 {c.rwc.SetWriteDeadline(time.Now().Add(d))}    ……

當你處理不可信的用戶端和網路的時候,你應該同時設定讀寫逾時,這樣用戶端就不會因為讀慢或者寫慢長久的持有這個串連了。

最後,還有一個http.TimeoutHandler方法。 它並不是Server參數,而是一個Handler封裝函數,可以限制 ServeHTTP調用。它緩衝response, 如果deadline超過了則發送 504 Gateway Timeout 錯誤。 注意這個功能在 1.6 中有問題,在1.6.2中改正了。

http.ListenAndServe 的錯誤

順便提一句,net/http包下的封裝的繞過http.Server的函數http.ListenAndServe, http.ListenAndServeTLShttp.Serve並不適合實現互連網的伺服器。這些函數讓逾時設定預設不啟用,並且你沒有辦法設定啟用逾時處理。所以如果你使用它們,你會很快發現串連泄漏,太多的檔案控制代碼。我犯過這種錯誤至少五六次。

取而代之,你應該建立一個http.Server樣本,設定ReadTimeoutWriteTimeout,像上面的例子中一樣使用相應的方法。

關於流

令人心塞的是, 沒有辦法從ServeHTTP中訪問底層的net.Conn,所以提供流服務強制不去設定WriteTimeout(這也可能是為什麼這些值的預設值總為0)。如果無法訪問net.Conn就不能在每次Write的時候調用SetWriteDeadline來實現一個正確的idle timeout。

而且,也沒有辦法取消一個阻塞的ResponseWriter.Write,因為ResponseWriter.Close沒有文檔指出它可以取消一個阻塞並發寫。也沒有辦法使用Timer建立以俄國手工的timeout 杯具就是流伺服器不能對於慢讀的用戶端進行防護。我提交的了一個[bug](https://github.com/golang/go/issues/16100),歡迎大家反饋。

編者按: 作者此處的說法是有問題的,可以通過Hijack擷取net.Conn,既然可以可以擷取net.Conn,我們就可以調用它的SetWriteDeadline方法。代碼例子如下:

123456789101112131415161718192021222324252627282930313233343536
package mainimport ("fmt""log""net/http")func main() {http.HandleFunc("/hijack", func(w http.ResponseWriter, r *http.Request) {hj, ok := w.(http.Hijacker)if !ok {http.Error(w, "webserver doesn't support hijacking", http.StatusInternalServerError)return}conn, bufrw, err := hj.Hijack()if err != nil {http.Error(w, err.Error(), http.StatusInternalServerError)return}// Don't forget to close the connection:defer conn.Close()conn.SetWriteDeadline(time.Now().Add(10 * time.Second))bufrw.WriteString("Now we're speaking raw TCP. Say hi: ")bufrw.Flush()s, err := bufrw.ReadString('\n')if err != nil {log.Printf("error reading string: %v", err)return}fmt.Fprintf(bufrw, "You said: %q\nBye.\n", s)bufrw.Flush()})}

用戶端逾時設定

Client端的逾時設定說複雜也複雜,說簡單也簡單,看你怎麼用了,最重要的就是不要有資源泄漏的情況或者程式被卡住。

最簡單的方式就是使用http.ClientTimeout欄位。 它的時間計算包括從串連(Dial)到讀完response body。

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

就像伺服器端一樣,http.GET使用Client的時候也沒有逾時設定,所以在互連網上使用也很危險。

有一些更細粒度的逾時控制:

  • net.Dialer.Timeout 限制建立TCP串連的時間
  • http.Transport.TLSHandshakeTimeout 限制 TLS握手的時間
  • http.Transport.ResponseHeaderTimeout 限制讀取response header的時間
  • http.Transport.ExpectContinueTimeout 限制client在發送包含 Expect: 100-continue的header到收到繼續發送body的response之間的時間等待。注意在1.6中設定這個值會禁用HTTP/2(DefaultTransport自1.6.2起是個特例)
1234567891011
c := &http.Client{      Transport: &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,    }}

如我所講,沒有辦法限制發送request的時間。讀取response body (原文是讀取request body,按照理解應該是讀取response可以手工控制)的時間花費可以手工的通過一個time.Timer來實現, 讀取發生在調用Client.Do之後(詳見下一節)。

最後將一點,在Go 1.7中,增加了一個http.Transport.IdleConnTimeout, 它不控制client request的阻塞階段,但是可以控制串連池中一個串連可以idle多長時間。

注意一個Client預設的可以執行 redirecthttp.Client.Timeout包含所有的redirect,而細粒度的逾時控制參數只針對單次請求有效, 因為http.Transport是一個底層的類型,沒有redirect的概念。

Cancel 和 Context

net/http提供了兩種方式取消一個client的請求: Request.Cancel以及Go 1.7新加的Context

Request.Cancel是一個可選的channel, 當設定這個值並且close它的時候,request就會終止,就好像逾時了一樣(實際它們的實現是一樣的,在寫本文的時候我還發現一個1.7 的 一個bug, 所有的cancel操作返回的錯誤還是timeout error )。

我們可以使用Request.Canceltime.Timer來構建一個細粒度的逾時控制,允許讀取流資料的時候延遲deadline:

123456789101112131415161718192021222324252627282930313233343536373839404142
package mainimport (      "io"    "io/ioutil"    "log"    "net/http"    "time")func main() {      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    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方法執行階段設定5秒的逾時,但是我們至少花費8秒執行8次才能讀完所欲的body,每一次設定2秒的逾時。我們可以為流 API這樣處理避免程式死在那裡。 如果超過兩秒我們沒有從伺服器讀取到資料, io.CopyN會返回net/http: request canceled錯誤。

在1.7中, context包升級了,進入到標準庫中。Context有很多值得學習的功能,但是對於本文介紹的內容來講,你只需直到它可以用來替換和扔掉Request.Cancel

用Context取消請求很簡單,我們只需得到一個新的Context和它的cancel()函數,這是通過context.WithCancel方法得到的,然後建立一個request並使用Request.WithContext綁定它。當我們想取消這個請求是,我們調用cancel()取消這個Context:

12345678910
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)

Context好處還在於如果parent context被取消的時候(在context.WithCancel調用的時候傳遞進來的),子context也會取消, 命令會進行傳遞。

好了,這就是本文要講的全部,希望我沒有超過你的閱讀deadline。

作者的公司cloudflare在英國、美國和新加坡招人。雲初創公司,挺知名。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.