Go 超時引發大量 Fin-Wait2
本文轉載自微信公眾號「碼農桃花源」,作者峰云就她了。轉載本文請聯系碼農桃花源公眾號。
通過grafana監控面板,發現了幾個高頻的業務緩存節點出現了大量的fin-wait2,而且fin-wait2狀態持續了不短的時間。通過連接的ip地址和抓包數據判斷出對端的業務。除此之外,頻繁地去創建新連接,我們對golang net/http transport的連接池已優化過,但established已建連的連接沒有得到復用。
另外,隨之帶來的問題是大量time-wait的出現,畢竟fin-wait2在拿到對端fin后會轉變為time-wait狀態。但該狀態是正常的。
分析問題
通過分析業務日志發現了大量的接口超時問題,連接的地址跟netstat中fin-wait2目的地址是一致的。那么問題已經明確了,當http的請求觸發超時,定時器對連接對象進行了關閉。這邊都close了,那么連接自然無法復用,所以就需要創建新連接,但由于對端的API接口出現邏輯阻塞,自然就又觸發了超時,continue。
- // xiaorui.cc
- Get "http://xxxx": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
- Get "http://xxxx": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
- Get "http://xxxx": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
通過strace追蹤socket的系統調用,發現golang的socket讀寫超時沒有使用setsockopt so_sndtimeo so_revtimeo參數。
- [pid 34262] epoll_ctl(3, EPOLL_CTL_ADD, 6, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1310076696, u64=140244877192984}}) = 0
- [pid 34265] epoll_pwait(3, <unfinished ...>
- [pid 34262] <... getsockname resumed>{sa_family=AF_INET, sin_port=htons(45242), sin_addr=inet_addr("127.0.0.1")}, [112->16]) = 0
- [pid 34264] epoll_pwait(3, <unfinished ...>
- [pid 34262] setsockopt(6, SOL_TCP, TCP_NODELAY, [1], 4 <unfinished ...>
- [pid 34262] setsockopt(6, SOL_SOCKET, SO_KEEPALIVE, [1], 4 <unfinished ...>
- [pid 34264] read(4, <unfinished ...>
- [pid 34262] setsockopt(6, SOL_TCP, TCP_KEEPINTVL, [30], 4 <unfinished ...>
代碼分析
通過net/http源碼可以看到socket的超時控制是通過定時器來實現的,在連接的roundTrip方法里有超時引發關閉連接的邏輯。由于http的語義不支持多路復用,所以為了規避超時后再回來的數據造成混亂,索性直接關閉連接。
當觸發超時會主動關閉連接,這里涉及到了四次揮手,作為關閉方會發送fin,對端內核會回應ack,這時候客戶端從fin-wait1到fin-wait2,而服務端在close-wait狀態,等待觸發close syscall系統調用。服務端什么時候觸發close動作?需要等待net/http handler業務邏輯執行完畢。
- // xiaorui.cc
- var errTimeout error = &httpError{err: "net/http: timeout awaiting response headers", timeout: true}
- func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {
- for {
- testHookWaitResLoop()
- select {
- case err := <-writeErrCh:
- if debugRoundTrip {
- req.logf("writeErrCh resv: %T/%#v", err, err)
- }
- if err != nil {
- pc.close(fmt.Errorf("write error: %v", err))
- return nil, pc.mapRoundTripError(req, startBytesWritten, err)
- }
- if d := pc.t.ResponseHeaderTimeout; d > 0 {
- if debugRoundTrip {
- req.logf("starting timer for %v", d)
- }
- timer := time.NewTimer(d)
- defer timer.Stop() // prevent leaks
- respHeaderTimer = timer.C
- }
- case <-pc.closech:
- ...
- case <-respHeaderTimer:
- if debugRoundTrip {
- req.logf("timeout waiting for response headers.")
- }
- pc.close(errTimeout)
- return nil, errTimeout
如何解決
要么加大客戶端的超時時間,要么優化對端的獲取數據的邏輯,總之減少超時的觸發。這個問題其實跟 Go 沒有關系,換成openresyt和python同樣有這個問題。