華文網

Golang 優化之路——HTTP長連接

寫在前面

壓測的是否發現服務端

TIME_WAIT 狀態的連接很多。

netstat -nat | grep :8080 | grep TIME_WAIT | wc -l 17731

TIME_WAIT 狀態多,簡單的說就是服務端主動關閉了TCP連接。

TCP頻繁的建立連接,會有一些問題:

三次握手建立連接、四次握手斷開連接都會對性能有損耗;

斷開的連接斷開不會立刻釋放,會等待2MSL的時間,據我觀察是1分鐘;

大量 TIME_WAIT會佔用記憶體,一個連接實測是3.155KB。而且佔用太多,有可能會占滿埠,一台伺服器最多只能有6萬多個埠;

TCP 相關

長連接的概念包括TCP長連接和HTTP長連接。首先得保證TCP是長連接。

我們就從它說起。

func (c *TCPConn) SetKeepAlive(keepalive bool) error

SetKeepAlive sets whether the operating system should send keepalive messages on the connection. 這個方法比較簡單,設置是否開啟長連接。

func (c *TCPConn) SetReadDeadline(t time.Time) error

SetReadDeadline sets the deadline for future Read calls and any currently-blocked Read call. A zero value for t means Read will not time out.這個函數就很講究了。我之前的理解是設置讀取超時時間,這個方法也有這個意思,但是還有別的內容。它設置的是讀取超時的絕對時間。

func (c *TCPConn) SetWriteDeadline(t time.Time) error

SetWriteDeadline sets the deadline for future Write calls and any currently-blocked Write call. Even if write times out, it may return n > 0, indicating that some of the data was successfully written. A zero value for t means Write will not time out. 這個方法是設置寫超時,同樣是絕對時間。

HTTP 包如何使用 TCP 長連接?

http 伺服器啟動之後,會迴圈接受新請求,為每一個請求(連接)創建一個協程。

下面是每個協程的執行的代碼,我只摘錄了一部分關鍵的邏輯。

可以發現, serve方法裡面還有一個 for 迴圈。

這個迴圈是用來做什麼的?其實也容易理解,如果是長連接,一個協程可以執行多次回應。如果只執行了一次,那就是短連接。長連接會在超時或者出錯後退出迴圈,也就是關閉長連接。 defer
函數可以讓協程結束之後關閉 TCP 連接。

readRequest 函數用來解析 HTTP 協定。

具體參與解析 HTTP 協定的部分是 ReadRequest 方法,而調用它之前,設置了讀寫超時時間。根據前面的描述,超時時間設置的是絕對時間。所以這裡都是通過 time.Now().Add(d) 來設置的。不同的是寫超時是 defer 執行,也就是函數返回後才執行。

我們的程式為啥長連接失效?

通過源碼我們能大概知道程式流程了,按道理是支援長連接的。為啥我們的程式不行呢?

我們的程式使用的是 beego 框架,它支持的超時是同時設置讀寫超時。而我們的設置是1秒。

beego.HttpServerTimeOut = 1

我對讀寫超時的理解,讀超時是收到資料到讀取完畢的時間;寫超時是從一開始寫到寫完的時間。我對這兩個超時的理解都不對。

實際上,從上面的源碼可以發現,

寫超時是讀取完畢之後設置的超時時間。也就是讀取完畢之後的時間,加上邏輯執行時間,加上內容返回時間的總和。按照我們的設置,超過1秒就算超時。

下面詳細說說讀超時。 ReadRequest是堵塞執行的,如果沒有用戶請求,它會一直等待著。而讀超時是 ReadRequest之前設置的,它除了讀取資料之外,還有一部分耗時,那就是等待時間。假如一直沒有用戶請求,此時讀超時已經被設置成1秒後了,超過1秒之後,這個連接還是會被斷開。

如何解決問題?

原因已經說明白了。大量 TIME_WAIT是超時引起的,有可能是等待時間過長引起的讀超時;也有可能是程式在壓測情況下出現一部分執行超時,這樣會導致寫超時。

我們目前使用的是 beego 框架,它並不支持單獨設置讀寫超時,所以我目前的解決方式是將讀寫超時調整得大一些。

從1.6版本開始,Golang 能夠支持空閒超時 IdleTimeout,可以認為讀超時就是讀取資料的時間,空閒超時來控制等待時間。但是它有一個問題,如果空閒超時沒有設置,而讀超時設置了,那麼讀超時還是會作為空閒超時時間來使用。我估計這麼做的原因是為了向前相容。再一個問題就是 beego 並不支持這個時間的設置,所以我目前也沒有別的太好的方法來控制超時時間。

後續

其實服務端最合理的超時控制需要這幾個方面:

讀超時。就是單純的讀超時,不要包括等待時間,否則無法區分超時是讀數據引起的還是等待引起的。

寫超時。最好也是單純的寫資料超時。如果網路良好,因為邏輯執行慢就把連接斷開,這樣也不是很合適。讀寫超時都應該和目前邏輯設置的一樣,設置得短一些。

空閒超時。這個可以根據實際情況配置,可以適當大一些。

邏輯超時。一般情況下是不會發生網路層面的讀寫超時的,壓測情況下超時大部分都是由於邏輯超時引起的。Golang 原生包支持了 TimeoutHandler 。它可以控制邏輯的超時。可惜 beego 目前不支援設置邏輯超時。而我也沒有想到太好的方法把 beego 中接入它。

func TimeoutHandler(h Handler, dt time.Duration, msg string) Handler

想瞭解更多IT知識,更多就業知識可關注:

優就業官網:http://www.ujiuye.com/?wt.bd=zy35844

優就業大學生就業扶持計畫:http://www.ujiuye.com/zt/jyfc/?wt.bd=zy35844

線上視頻學習:http://xue.ujiuye.com/?wt.bd=zy35844

超過1秒之後,這個連接還是會被斷開。

如何解決問題?

原因已經說明白了。大量 TIME_WAIT是超時引起的,有可能是等待時間過長引起的讀超時;也有可能是程式在壓測情況下出現一部分執行超時,這樣會導致寫超時。

我們目前使用的是 beego 框架,它並不支持單獨設置讀寫超時,所以我目前的解決方式是將讀寫超時調整得大一些。

從1.6版本開始,Golang 能夠支持空閒超時 IdleTimeout,可以認為讀超時就是讀取資料的時間,空閒超時來控制等待時間。但是它有一個問題,如果空閒超時沒有設置,而讀超時設置了,那麼讀超時還是會作為空閒超時時間來使用。我估計這麼做的原因是為了向前相容。再一個問題就是 beego 並不支持這個時間的設置,所以我目前也沒有別的太好的方法來控制超時時間。

後續

其實服務端最合理的超時控制需要這幾個方面:

讀超時。就是單純的讀超時,不要包括等待時間,否則無法區分超時是讀數據引起的還是等待引起的。

寫超時。最好也是單純的寫資料超時。如果網路良好,因為邏輯執行慢就把連接斷開,這樣也不是很合適。讀寫超時都應該和目前邏輯設置的一樣,設置得短一些。

空閒超時。這個可以根據實際情況配置,可以適當大一些。

邏輯超時。一般情況下是不會發生網路層面的讀寫超時的,壓測情況下超時大部分都是由於邏輯超時引起的。Golang 原生包支持了 TimeoutHandler 。它可以控制邏輯的超時。可惜 beego 目前不支援設置邏輯超時。而我也沒有想到太好的方法把 beego 中接入它。

func TimeoutHandler(h Handler, dt time.Duration, msg string) Handler

想瞭解更多IT知識,更多就業知識可關注:

優就業官網:http://www.ujiuye.com/?wt.bd=zy35844

優就業大學生就業扶持計畫:http://www.ujiuye.com/zt/jyfc/?wt.bd=zy35844

線上視頻學習:http://xue.ujiuye.com/?wt.bd=zy35844