<bdo id="4g88a"><xmp id="4g88a">
  • <legend id="4g88a"><code id="4g88a"></code></legend>

    Loading

    開啟 Keep-Alive 可能會導致http 請求偶發失敗

    大家好,我是藍胖子,說起提高http的傳輸效率,很多人會開啟http的Keep-Alive選項,這會http請求能夠復用tcp連接,節省了握手的開銷。但開啟Keep-Alive真的沒有問題嗎?我們來細細分析下。

    最大空閑時間造成請求失敗

    通常我們開啟Keep-Alive后 ,服務端還會設置連接的最大空閑時間,這樣能保證在沒有請求發生時,及時釋放連接,不會讓過多的tcp連接白白占用機器資源。

    問題就出現在服務端主動關閉空閑連接這個地方,試想一下這個場景,客戶端復用了一個空閑連接發送http請求,但此時服務端正好檢測到這個連接超過了配置的連接最大空閑時間,在請求到達前,提前關閉了空閑連接,這樣就會導致客戶端此次的請求失敗。

    過程如下圖所示,

    image.png

    如何避免此類問題

    上述問題在理論上的確是一直存在的,但是我們可以針對發送http請求的代碼做一些加強,來盡量避免此類問題。來看看在Golang中,http client客戶端是如何盡量做到安全的http重試的。

    go http client 是如何做到安全重試請求的?

    在golang中,在發送一次http請求后,如果發現請求失敗,會通過shouldRetryRequest 函數判斷此次請求是否應該被重試,代碼如下,

    func (pc *persistConn) shouldRetryRequest(req *Request, err error) bool {  
        if http2isNoCachedConnError(err) {  
           // Issue 16582: if the user started a bunch of  
           // requests at once, they can all pick the same conn       // and violate the server's max concurrent streams.       // Instead, match the HTTP/1 behavior for now and dial       // again to get a new TCP connection, rather than failing       // this request.      
            return true  
        }  
        if err == errMissingHost {  
           // User error.  
           return false  
        }  
        if !pc.isReused() {  
           // This was a fresh connection. There's no reason the server  
           // should've hung up on us.       //       // Also, if we retried now, we could loop forever       // creating new connections and retrying if the server       // is just hanging up on us because it doesn't like       // our request (as opposed to sending an error).       
           return false  
        }  
        if _, ok := err.(nothingWrittenError); ok {  
           // We never wrote anything, so it's safe to retry, if there's no body or we  
           // can "rewind" the body with GetBody.      
            return req.outgoingLength() == 0 || req.GetBody != nil  
        }  
        if !req.isReplayable() {  
           // Don't retry non-idempotent requests.  
           return false  
        }  
        if _, ok := err.(transportReadFromServerError); ok {  
           // We got some non-EOF net.Conn.Read failure reading  
           // the 1st response byte from the server.       
           return true  
        }  
        if err == errServerClosedIdle {  
           // The server replied with io.EOF while we were trying to  
           // read the response. Probably an unfortunately keep-alive       // timeout, just as the client was writing a request.       
           return true  
        }  
        return false // conservatively  
    }
    

    我們來挨個看看每個判斷邏輯,

    http2isNoCachedConnError 是關于http2的判斷邏輯,這部分邏輯我們先不管。

    err == errMissingHost 這是由于請求路徑中缺少請求的域名或ip信息,這種情況不需要重試。

    pc.isReused() 這個是在判斷此次請求的連接是不是屬于連接復用情況,因為如果是新創建的連接,服務器正常情況下是沒有理由拒絕我們的請求,此時如果請求失敗了,則新建連接就好,不需要重試。

    if _, ok := err.(nothingWrittenError); ok 這是在判斷此次的請求失敗的時候是不是還沒有向對端服務器寫入任何字節,如果沒有寫入任何字節,并且請求的body是空的,或者有body但是能通過req.GetBody 恢復body就能進行重試。

    ????注意,因為在真正向連接寫入請求頭和body時,golang其實是構建了一個bufio.Writer 去封裝了連接對象,數據是先寫到了bufio.Writer 緩沖區中,所以有可能出現請求體Request已經讀取了部分body,寫入到緩沖區中,但實際真正向連接寫入數據時失敗的場景,這種情況重試就需要恢復原先的body,重試請求時,從頭讀取body數據。

    req.isReplayable() 則是從請求體中判斷這個請求是否能夠被重試,如果不滿足重試要求,則直接不重試,滿足重試要求則會繼續進行下面的重試判斷。 其代碼如下,如果http的請求body為空,或者有GetBody 方法能為其恢復body,并且是"GET", "HEAD", "OPTIONS", "TRACE" 方法之一則認為該請求重試是安全的。

    還有種情況是如果http請求頭中有Idempotency-Key 或者X-Idempotency-Key 也認為重試是安全的。

    X-Idempotency-KeyIdempotency-Key 其實是為了給post請求的重試給了一個后門,對應的key是由業務方自己定義的具有冪等性質的key,服務端可以拿到它做冪等性校驗,所以重試是安全的。

    func (r *Request) isReplayable() bool {  
        if r.Body == nil || r.Body == NoBody || r.GetBody != nil {  
           switch valueOrDefault(r.Method, "GET") {  
           case "GET", "HEAD", "OPTIONS", "TRACE":  
              return true  
           }  
           // The Idempotency-Key, while non-standard, is widely used to  
           // mean a POST or other request is idempotent. See       // https://golang.org/issue/19943#issuecomment-421092421       
           if r.Header.has("Idempotency-Key") || r.Header.has("X-Idempotency-Key") {  
              return true  
           }  
        }  
        return false  
    }
    

    只有認為請求重試是安全后,才會進一步判斷請求失敗 是不是由于服務端關閉空閑連接造成的 _, ok := err.(transportReadFromServerError)errServerClosedIdle都是由于服務端關閉空閑連接造成的錯誤碼,如果產生的錯誤碼是其中之一,則都是允許被重試的。

    ??????所以,綜上你可以看出,如果你發的請求是一個不帶有Idempotency-Key或者X-Idempotency-Keypost請求頭的post請求,那么即使是由于服務器關閉空閑連接造成請求失敗,該post請求是不會被重試的。不過在其他請求方法比如GET方法下,由服務器關閉空閑連接造成的請求錯誤,Golang 能自動重試。

    最佳實踐

    針對上述場景,我們應該如何設計我們的請求發送來保證安全可靠的發送http請求呢?針對于Golang開發環境,我總結幾點經驗,

    1,GET請求可以自動重試,如果你的接口沒有完全準尋restful 風格,GET請求的處理方法仍然有修改數據的操作,那么你應該保證你的接口是冪等的。

    2,POST請求不會自動重試,但是如果你需要讓你的操作百分百的成功,請添加失敗重試邏輯,同樣,服務端最好做好冪等操作。

    3,如果對性能要求不是那么高,那么直接關閉掉http的長鏈接,將請求頭的Connection 字段設置為close 這樣每次發送發送http請求時都是用的新的連接,不會存在潛在的服務端關閉空閑連接造成請求失敗的問題。

    4,第四點,其實你可以發現,網絡請求,不管你的網絡情況是否好壞,都是存在失敗的可能,即使將http長連接關掉,在網絡壞的情況下,請求還是會失敗,失敗了要想保證成功,就得重試,重試就一定得保證服務端接口冪等了,所以,你的接口如果是冪等的,你的請求如果具有重試邏輯,那么恭喜你,你的系統十分可靠。

    5,最后一點,千萬不要抱著僥幸心理去看待網絡請求,正如第四點說的那樣,不管你的網絡情況是否好壞,都是存在失敗的可能。嗯,面對異常編程。

    posted @ 2024-04-03 15:47  藍胖子的編程夢  閱讀(406)  評論(0編輯  收藏  舉報
    免费视频精品一区二区_日韩一区二区三区精品_aaa在线观看免费完整版_世界一级真人片
    <bdo id="4g88a"><xmp id="4g88a">
  • <legend id="4g88a"><code id="4g88a"></code></legend>