翻譯自:The complete guide to Go net/http timeouts
地址:https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/
使用Go編寫HTTP服務器或客戶端時,超時是最容易出錯的最容易發生的事情:一個錯誤可能在很長一段時間內沒有任何影響,直到網絡出現故障並掛起該進程爲止。
HTTP是一個複雜的多階段協議,因此沒有一個適合所有超時的解決方案。考慮一下流端點、JSON API和Comet端點。實際上,默認設置通常不是您想要的。
在本文中,我將介紹在服務器端和客戶端都可能會導致應用超時的各個階段,並探討解決超時的不同方法。
SetDeadline
首先,您需要了解Go用於實現超時的網絡原語:Deadlines。
net.Conn使用Set[Read|Write]Deadline(time.Time)方法公開,Deadlines是絕對時間,一旦到時,所有I/O操作都會因超時錯誤而失敗。
Deadlines不是超時。一旦設置,它們就會永久生效(直到下一次調用SetDeadline),不管在此期間是否使用連接以及如何使用連接。因此,要使用SetDeadline建立超時,您必須在每次讀/寫操作之前調用它。
實際開發中,你並不需要直接調用SetDeadline,而是在標準庫net/http中使用更高層次的超時設置。但是,請記住,所有超時都是根據Deadlines實現的,因此它們不會在每次發送或接收數據時重置。
Server Timeouts
“So you want to expose Go on the Internet”一文提供了有關服務器超時的更多信息,尤其是有關HTTP/2和Go 1.7的信息。
對於暴露於Internet的HTTP服務器來說,設置客戶端鏈接超時,是至關重要的。否則,非常緩慢或消失的客戶端可能會泄漏文件描述符,最終導致以下情況:
|
|
http.Server有兩個設置超時的方法:ReadTimeout和WriteTimeout。你可以顯式地設置它們:
|
|
ReadTimeout的時間計算是從連接被接受(accept)到request body完全被讀取(if you do read the body, otherwise to the end of the headers)。net/http的內部實現是在Accept之後立即調用SetReadDeadline。
WriteTimeout的時間計算正常是從request header的讀取結束開始,到response write結束爲止(也就是ServeHTTP的生命週期), 它是通過在readRequest方法結束的時候調用SetWriteDeadline實現的。
但是,當連接爲HTTPS時,會在Accept之後立即調用SetWriteDeadline,所以它的時間計算也包括 TLS握手時的寫的時間。令人討厭的是,這意味着(僅在這種情況下)WriteTimeout最終將包括Header和讀取body第一個字節這段時間。
當你處理不可信的客戶端和網絡的時候,你應該同時設置讀寫超時,這樣客戶端就不會因爲讀慢或者寫慢長久的持有這個連接了。
最後,還有http.TimeoutHandler。它不是一個Server參數,而是一個Handler包裝函數,限制了ServeHTTP調用的最大持續時間。它緩存response, 如果deadline超過了則發送504 Gateway Timeout錯誤。注意這個功能在1.6 中有問題,在1.6.2中改正了。
http.ListenAndServe is doing it wrong
順便說一句,這意味着繞過諸如http.ListenAndServe,http.ListenAndServeTLS和http.Serve之類的http.Server的程序包級便捷功能不適用於公共Internet服務器。
因爲這些函數默認關閉了超時設置,也無法手動設置。使用這些函數,將很快泄露連接,然後耗盡文件描述符。對於這點,我至少犯了6次以上這樣的錯誤。
對此,你應該使用http.server。在創建http.server實例的時候,調用相應的方法指定ReadTimeout(讀取超時時間)和WriteTimeout(寫超時時間),在以下會有一些案例。
About streaming
非常煩人的是,無法從ServeHTTP訪問底層net.Conn,因此打算流式傳輸響應的服務器被迫取消WriteTimeout的設置(這也可能是默認情況下它們爲0的原因)。這是因爲沒有net.Conn訪問,就無法在每次Write之前調用SetWriteDeadline來實現適當的空閒(不是絕對)超時。
同樣,也沒有辦法取消一個被阻塞的ResponseWriter。由於無法確認ResponseWriter.Close支持併發寫操作。因此,也沒有辦法使用計時器手動構建超時。
這意味着流媒體服務器面對一個低速客戶端時,將無法有效保障自身的效率、穩定
我提交了一個問題和一些建議,期待反饋。
譯者注:: 原文作者此處的說法有問題,其實通過Hijack是可以獲取到net.Conn的。
Client Timeouts
客戶端超時,可以很簡單,也可以很複雜,取決於你怎麼用。但同樣重要的是:要防止資源泄漏和阻塞。
最簡單的使用超時的方式是http.Client。它涵蓋整個交互過程,從發起連接(如果未重用連接)到接收響應報文結束。
|
|
與服務端情況類似,程序包級別的功能(例如http.Get)可以使用沒有超時的客戶端,因此在開放的Internet上使用非常危險。
還有其它一些方法,可以讓你進行更精細的超時控制:
net.Dialer.Timeout 限制創建一個TCP連接使用的時間(如果需要一個新的鏈接)
http.Transport.TLSHandshakeTimeout 限制TLS握手使用的時間
http.Transport.ResponseHeaderTimeout 限制讀取響應報文頭使用的時間
http.Transport.ExpectContinueTimeout 限制客戶端在發送一個包含:100-continue的http報文頭後,等待收到一個go-ahead響應報文所用的時間。在1.6中,此設置對HTTP/2無效。(在1.6.2中提供了一個特定的封裝DefaultTransport)
|
|
據我瞭解,尚沒有限制發送請求使用時間的機制。目前的解決方案是,在客戶端方法返回後,通過time.Timer來個手工控制讀取請求信息的時間(參見下面的“如何取消請求”)。
最後,在新的1.7版本中,提供了http.Transport.IdleConnTimeout。它用於控制一個閒置連接在連接池中的保留時間,而不考慮一個客戶端請求被阻塞在哪個階段。
請注意,客戶端將使用默認的重定向機制。由於http.Transport是一個底層的系統機制,沒有重定向概念,因此http.Client.Timeout涵蓋了用於重定向花費的時間,而更精細的超時控,可以根據請求的不同,進行定製。
Cancel and Context
net/http提供了兩種方式取消一個client的請求: Request.Cancel以及Go 1.7新加的Context。
Request.Cancel是一個可選channel。在Request.Timeout被觸發時,Request.Cancel將被設置並關閉,進而促使請求中斷(基本上“撤銷”都採用相同的機制,在寫此文時,我發現一個1.7中的bug,所有的撤銷操作,都會當作一個超時錯誤返回)。
我們可以使用Request.Cancel和time.Timer來構建一個細粒度的超時控制,以允許流傳輸,每次成功從Body讀取一些數據時,都將截止日期推遲:
|
|
在上面這個例子中,我們在請求階段,設置了一個5秒鐘的超時。但讀取響應報文階段,我們需要讀8次,至少8秒鐘的時間。每次讀操作,設置2秒鐘的超時。採用這樣的機制,我們可以無限制的獲取流媒體,而不用擔心阻塞的風險。如果我們沒有在2秒鐘內讀取到任何數據,io.CopyN將返回錯誤信息:net/http: request canceled.。
context包升級了,進入到標準庫中。關於Contexts,我們有大量需要學習的東西。基於本文的主旨,你首先應該知道的是:Contexts將替代Request.Cancel,不再建議(反對)使用Request.Cancel。
爲了使用Contexts來撤銷一個請求,我們需要創建一個新的Context以及它的基於context.WithCancel的cancel()函數,同時還有創建一個基於Request.WithContext的Request。當我們要撤銷一個請求時,我們其實際是通過cancel()函數撤銷相應的Context(取代原有的關閉Cancel channel的方式):
|
|
Context好處還在於如果parent context被取消的時候(在context.WithCancel調用的時候傳遞進來的),子context也會取消, 消息會進行傳遞。
< END >
喜歡就點個在看 or 轉發個朋友圈唄
衣舞晨風
推薦閱讀: