Go net 超時處理
序
這篇文章詳細介紹了,net/http
包中對應HTTP
的各個階段,如何使用timeout
來進行讀/寫超時控制以及服務端和客戶端支援設定的timeout
型別。本質上,這些timeout
都是程式碼層面對各個函式設定的處理時間。比如,讀取客戶端讀取請求頭、讀取響應體的時間,本質上都是響應函式的超時時間。
作者強烈不建議,在工作中使用net/http
包上層封裝的預設方法(沒有明確設定timeout
),很容易出現系統檔案套接字被耗盡等令人悲傷的情況。比如:
// 相信工作中也不會出現這樣的程式碼 func main() { http.ListenAndServe("127.0.0.1:3900", nil) }
正文
在使用Go
開發HTTP Server
或client
的過程中,指定timeout
很常見,但也很容易犯錯。timeout
錯誤一般還不容易被發現,可能只有當系統出現請求超時、服務掛起時,錯誤才被嚴肅暴露出來。
HTTP
是一個複雜的多階段協議,所以也不存在一個timeout
值適用於所有場景。想一下ofollow,noindex" target="_blank">
StreamingEndpoint
、JSON API
、
Comet
, 很多情況下,預設值根本不是我們所需要的值。
這篇部落格中,我會對HTTP
請求的各個階段進行拆分,列舉可能需要設定的timeout
值。然後從客戶端和服務端的角度,分析它們設定timeout
的不同方式。
SetDeadline
首先,你需要知道Go
所暴露出來的,用於實現timeout
的方法:Deadline
。
timeout
本身通過
net.Conn
包中的Set[Read|Write]Deadline(time.Time)
方法來控制。Deadline
是一個絕對的時間點,當連線的I/O
操作超過這個時間點而沒有完成時,便會因為超時失敗。
Deadlines
不同於timeouts
. 對一個連線而言,設定Deadline
之後,除非你重新呼叫SetDeadline
,否則這個Deadline
不會變化。前面也提了,Deadline
是一個絕對的時間點。因此,如果要通過SetDeadline
來設定timeout
,就不得不在每次執行Read
/Write
前重新呼叫它。
你可能並不想直接呼叫SetDeadline
方法,而是選擇net/http
提供的更上層的方法。但你要時刻記住:所有timeout
操作都是通過設定Deadline
實現的。每次呼叫,它們並不會去重置的deadline
。
Server Timeouts
關於服務端超時,這篇帖子
So you want to expose Go on the Internet
也介紹了很多資訊,特別是關於HTTP/2
和Go 1.7 bugs
的部分.
對於服務端而言,指定timeout
至關重要。否則,一些請求很慢或消失的客戶端很可能導致系統檔案描述符
洩漏,最終服務端報錯:
http: Accept error: accept tcp [::]:80: accept4: too many open files; retrying in 5ms
在建立http.Server
的時候,可以通過ReadTimeout
和WriteTimeout
來設定超時。你需要明確的宣告它們:
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
的一部分。同時,這也意味著(僅僅HTTPS
)WriteTimeout
包括了讀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
應該像文章開頭例子中那樣,明確設定ReadTimeout
和WriteTimeout
,並使用相應的方法來使server
更完善。
About streaming
Very annoyingly, there is no way of accessing the underlyingnet.Conn
fromServeHTTP
so a server that intends to stream a response is forced to unset theWriteTimeout
(which is also possibly why they are 0 by default). This is because withoutnet.Conn
access, there is no way of callingSetWriteDeadline
before eachWrite
to implement a proper idle (not absolute) timeout.
Also, there's no way to cancel a blockedResponseWriter.Write
sinceResponseWriter.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 submittedan issue with some proposals , and I welcome feedback there.
Client Timeouts
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.Cancel
和time.Timer
對timeout
進行更細的控制。比如,在我們每次從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.WithContext
將context
繫結到一個請求上。當我們想取消這個請求時,只需要呼叫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 have the advantage that if the parent context (the one we passed tocontext.WithCancel
) is canceled, ours will be, too, propagating the command down the entire pipeline.
Contexts
有很多優,比如一個parent
(傳遞給context.WithCancel
的物件)被取消,那麼命令會沿著傳遞的路徑一直向下傳遞,直到關閉所有子context
。
部落格地址:
noejos welcome you