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


    我們的一個服務是用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
}

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM