如何關閉Golang中的HTTP串連 How to Close Golang's HTTP connection

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

    我們的一個服務是用Go寫的,在測試的時候發現幾個小時之後它就會core掉,而且core的時候沒有打出任何堆棧資訊,簡單分析後發現該服務中的幾個HTTP服務的串連數不斷增長,而我們的開發機的fd limit只有1024,當該服務所屬進程的串連數增長到系統的fd limit的時候,它被作業系統殺掉了。。。

    HTTP Connection中串連未被釋放的問題在https://groups.google.com/forum/#!topic/golang-nuts/wliZf2_LUag和https://groups.google.com/forum/#!topic/golang-nuts/tACF6RxZ4GQ都有提到。

    這個服務中,我們會定期向一個HTTP伺服器發起POST請求,因為請求非常不頻繁,所以想採用短串連的方式去做。請求代碼大概長這樣:

  

func dialTimeout(network, addr string) (net.Conn, error) {return net.DialTimeout(network, addr, time.Second*POST_REMOTE_TIMEOUT)}func DoRequest(URL string) xx, error {       transport := http.Transport{                Dial:              dialTimeout,        }        client := http.Client{                Transport: &transport,        }        content := RequestContent{}        // fill content here         postStr, err := json.Marshal(content)        if err != nil {                return nil, err        }        resp, err := client.Post(URL, "application/json", bytes.NewBuffer(postStr))        if err != nil {                return nil, err        }        defer resp.Body.Close()        body, err := ioutil.ReadAll(resp.Body)        if err != nil {                return nil, err        }        // receive body, handle it}

  運行這段代碼一段時間後會發現,該進程下面有一堆ESTABLISHED狀態的串連(用lsof -p pid查看某進程下的所有fd),因為每次DoRequest函數被調用後,都會建立一個TCP串連,如果對端不先關閉該串連(對端發FIN包)的話,我們這邊即便是調用了resp.Body.Close()函數仍然不會改變這些處於ESTABLISHED狀態的串連。為什麼會這樣呢?只有去原始碼一探究竟了。

      

      Golang的net包中client.go, transport.go, response.go和request.go這幾個檔案中實現了HTTP Client。當應用程式層調用client.Do()函數後,transport層會首先找與該請求相關的已經緩衝的串連(這個緩衝是一個map,map的key是要求方法、請求地址和proxy地址,value是一個叫persistConn的串連描述結構),如果已經有可以複用的舊串連,就會在這箇舊串連上發送和接受該HTTP請求,否則會建立一個TCP串連,然後在這個串連上讀寫資料。當client接受到整個響應後,如果應用程式層沒有
調用response.Body.Close()函數,剛剛傳輸資料的persistConn就不會被加入到串連緩衝中,這樣如果您在下次發起HTTP請求的時候,就會重建立立TCP串連,重新分配persistConn結構,這是不調用response.Body.Close()的一個副作用。
      如果不調用response.Body.Close()還存在一個問題。如果請求完成後,對端關閉了串連(對端的HTTP伺服器向我發送了FIN),如果這邊不調用response.Body.Close(),那麼可以看到與這個請求相關的TCP串連的狀態一直處於CLOSE_WAIT狀態(還記得嗎?CLOSE_WAIT是串連的半開半閉狀態,它是收到對方的FIN並且我們也發送了ACK,但是本端還沒有發送FIN到對端,如果本段不調用close關閉串連,那麼串連將一直處於
CLOSE_WAIT狀態,不會被系統回收)。

      調用了response.Body.Close()就萬無一失了嗎?上面代碼中也調用了body.Close()為什麼還會有很多ESTABLISHED狀態的串連呢?因為在函數DoRequest()的每次調用中,我們都會新建立transport和client結構,當HTTP請求完成並且接收到響應後,如果對端的HTTP伺服器沒有關閉串連,那麼這個串連會一直處於ESTABLISHED狀態。如何解呢?
有兩個方法:
      第一個方法是用一個全域的client,函數DoRequest()中每次都只在這個全域client上發送資料。但是如果我就想用短串連呢?用方法二。
      第二個方法是在transport分配時將它的DisableKeepAlives參數置為false,像下面這樣:

        // ...        transport := http.Transport{                Dial:              dialTimeout,                DisableKeepAlives: true,        }        client := http.Client{                Transport: &transport,        }        // ...

  從transport.go:L908可以看到,當應用程式層調用resp.Body.Close()時,如果DisableKeepAlives被開啟,那麼transport自動關閉本端串連。而不將它加入到串連緩衝中。

 

    補充一下,在dialTimeout函數中disable tcp串連的keepalive選項是不可行的,它只是設定TCP串連的選項,不會影響到transport中對串連的控制。

func dialTimeout(network, addr string) (net.Conn, error) {        conn, err := net.DialTimeout(network, addr, time.Second*POST_REMOTE_TIMEOUT)if err != nil {return conn, err}tcp_conn := conn.(*net.TCPConn)                                                                                                  tcp_conn.SetKeepAlive(false)                                                                                                     return tcp_conn, err}

  

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.