您好,登錄后才能下訂單哦!
這篇文章將為大家詳細(xì)講解有關(guān)Go http client 連接池不復(fù)用如何解決,文章內(nèi)容質(zhì)量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關(guān)知識有一定的了解。
當(dāng) http client 返回值為不為空,只讀取 response header,但不讀 body 內(nèi)容就執(zhí)行 response.Body.Close(),那么連接會被主動關(guān)閉,得不到復(fù)用。
測試代碼如下:
// xiaorui.cc func HttpGet() { for { fmt.Println("new") resp, err := http.Get("http://www.baidu.com") if err != nil { fmt.Println(err) continue } if resp.StatusCode == http.StatusOK { continue } resp.Body.Close() fmt.Println("go num", runtime.NumGoroutine()) } }
正如大家所想,除了 HEAD Method 外,很少會有只讀取 header 的需求吧。
話說,golang httpclient 需要注意的地方著實不少。
如沒有 response.Body.Close(),有些小場景造成 persistConn 的 writeLoop 泄露。
如 header 和 body 都不管,那么會造成泄露的連接干滿連接池,后面的請求只能是短連接。
由于某幾個業(yè)務(wù)系統(tǒng)會瘋狂調(diào)用各區(qū)域不同的 k8s 集群,為減少跨機房帶來的時延、兼容新老 k8s 集群 api、減少k8s api-server 的負(fù)載,故而開發(fā)了 k8scache 服務(wù)。在部署運行后開始對該服務(wù)進行監(jiān)控,發(fā)現(xiàn) metrics 呈現(xiàn)的 QPS 跟連接數(shù)不成正比,qps 為 1500,連接數(shù)為 10 個。開始以為觸發(fā) idle timeout 被回收,但通過歷史監(jiān)控圖分析到連接依然很少。????
按照對 k8scache 調(diào)用方的理解,他們經(jīng)常粗暴的開啟不少協(xié)程來對 k8scache 進行訪問。已知默認(rèn)的 golang httpclient transport 對連接數(shù)是有默認(rèn)限制的,連接池總大小為 100,每個 host 連接數(shù)為 2。當(dāng)并發(fā)對某 url 進行請求時,無法歸還連接池,也就是超過連接池大小的連接會被主動clsoe()。所以,我司的 golang 腳手架中會對默認(rèn)的 httpclient 創(chuàng)建高配的 transport,不太可能出現(xiàn)連接池爆滿被 close 的問題。
如果真的是連接池爆了? 誰主動挑起關(guān)閉,誰就有 tcp time-wait 狀態(tài),但通過 netstat 命令只發(fā)現(xiàn)少量跟 k8scache 相關(guān)的 time-wait。
已知問題, 為隱藏敏感信息,索性使用簡單的場景設(shè)立問題的 case
tcpdump抓包分析問題?
包信息如下,通過最后一行可以確認(rèn)是由客戶端主動觸發(fā) RST連接重置 。觸發(fā)RST的場景有很多,但常見的有 tw_bucket 滿了、tcp 連接隊列爆滿且開啟 tcp_abort_on_overflow、配置 so_linger、讀緩沖區(qū)還有數(shù)據(jù)就給 close。
通過 linux 監(jiān)控和內(nèi)核日志可以確認(rèn)不是內(nèi)核配置的問題,配置 so_linger 更不可能。???? 大概率就一個可能,關(guān)閉未清空讀緩沖區(qū)的連接。
22:11:01.790573 IP (tos 0x0, ttl 64, id 29826, offset 0, flags [DF], proto TCP (6), length 60) host-46.54550 > 110.242.68.3.http: Flags [S], cksum 0x5f62 (incorrect -> 0xb894), seq 1633933317, win 29200, options [mss 1460,sackOK,TS val 47230087 ecr 0,nop,wscale 7], length 0 22:11:01.801715 IP (tos 0x0, ttl 43, id 0, offset 0, flags [DF], proto TCP (6), length 52) 110.242.68.3.http > host-46.54550: Flags [S.], cksum 0x00a0 (correct), seq 1871454056, ack 1633933318, win 29040, options [mss 1452,nop,nop,sackOK,nop,wscale 7], length 0 22:11:01.801757 IP (tos 0x0, ttl 64, id 29827, offset 0, flags [DF], proto TCP (6), length 40) host-46.54550 > 110.242.68.3.http: Flags [.], cksum 0x5f4e (incorrect -> 0xb1f5), seq 1, ack 1, win 229, length 0 22:11:01.801937 IP (tos 0x0, ttl 64, id 29828, offset 0, flags [DF], proto TCP (6), length 134) host-46.54550 > 110.242.68.3.http: Flags [P.], cksum 0x5fac (incorrect -> 0xb4d6), seq 1:95, ack 1, win 229, length 94: HTTP, length: 94 GET / HTTP/1.1 Host: www.baidu.com User-Agent: Go-http-client/1.1 22:11:01.814122 IP (tos 0x0, ttl 43, id 657, offset 0, flags [DF], proto TCP (6), length 40) 110.242.68.3.http > host-46.54550: Flags [.], cksum 0xb199 (correct), seq 1, ack 95, win 227, length 0 22:11:01.815179 IP (tos 0x0, ttl 43, id 658, offset 0, flags [DF], proto TCP (6), length 4136) 110.242.68.3.http > host-46.54550: Flags [P.], cksum 0x6f4e (incorrect -> 0x0e70), seq 1:4097, ack 95, win 227, length 4096: HTTP, length: 4096 HTTP/1.1 200 OK Bdpagetype: 1 Bdqid: 0x8b3b62c400142f77 Cache-Control: private Connection: keep-alive Content-Encoding: gzip Content-Type: text/html;charset=utf-8 Date: Wed, 09 Dec 2020 14:11:01 GMT ... 22:11:01.815214 IP (tos 0x0, ttl 64, id 29829, offset 0, flags [DF], proto TCP (6), length 40) host-46.54550 > 110.242.68.3.http: Flags [.], cksum 0x5f4e (incorrect -> 0xa157), seq 95, ack 4097, win 293, length 0 22:11:01.815222 IP (tos 0x0, ttl 43, id 661, offset 0, flags [DF], proto TCP (6), length 4136) 110.242.68.3.http > host-46.54550: Flags [P.], cksum 0x6f4e (incorrect -> 0x07fa), seq 4097:8193, ack 95, win 227, length 4096: HTTP 22:11:01.815236 IP (tos 0x0, ttl 64, id 29830, offset 0, flags [DF], proto TCP (6), length 40) host-46.54550 > 110.242.68.3.http: Flags [.], cksum 0x5f4e (incorrect -> 0x9117), seq 95, ack 8193, win 357, length 0 22:11:01.815243 IP (tos 0x0, ttl 43, id 664, offset 0, flags [DF], proto TCP (6), length 5848) ... host-46.54550 > 110.242.68.3.http: Flags [.], cksum 0x5f4e (incorrect -> 0x51ba), seq 95, ack 24165, win 606, length 0 22:11:01.815369 IP (tos 0x0, ttl 64, id 29834, offset 0, flags [DF], proto TCP (6), length 40) host-46.54550 > 110.242.68.3.http: Flags [R.], cksum 0x5f4e (incorrect -> 0x51b6), seq 95, ack 24165, win 606, length 0
通過 lsof 找到進程關(guān)聯(lián)的 TCP 連接,然后使用 ss 或 netstat 查看讀寫緩沖區(qū)。信息如下,recv-q 讀緩沖區(qū)確實是存在數(shù)據(jù)。這個緩沖區(qū)字節(jié)一直未讀,直到連接關(guān)閉引發(fā)了 rst。
$ lsof -p 54330 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME ... aaa 54330 root 1u CHR 136,0 0t0 3 /dev/pts/0 aaa 54330 root 2u CHR 136,0 0t0 3 /dev/pts/0 aaa 54330 root 3u a_inode 0,10 0 8838 [eventpoll] aaa 54330 root 4r FIFO 0,9 0t0 223586913 pipe aaa 54330 root 5w FIFO 0,9 0t0 223586913 pipe aaa 54330 root 6u IPv4 223596521 0t0 TCP host-46:60626->110.242.68.3:http (ESTABLISHED) $ ss -an|egrep "68.3:80" State Recv-Q Send-Q Local Address:Port Peer Address:Port ESTAB 72480 0 172.16.0.46:60626 110.242.68.3:80
strace 跟蹤系統(tǒng)調(diào)用
通過系統(tǒng)調(diào)用可分析出,貌似只是讀取了 header 部分了,還未讀到 body 的數(shù)據(jù)。
[pid 8311] connect(6, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("110.242.68.3")}, 16 <unfinished ...> [pid 195519] epoll_pwait(3, <unfinished ...> [pid 8311] <... connect resumed>) = -1 EINPROGRESS (操作現(xiàn)在正在進行) [pid 8311] epoll_ctl(3, EPOLL_CTL_ADD, 6, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=2350546712, u64=140370471714584}} <unfinished ...> [pid 195519] getsockopt(6, SOL_SOCKET, SO_ERROR, <unfinished ...> [pid 192592] nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...> [pid 195519] getpeername(6, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("110.242.68.3")}, [112->16]) = 0 [pid 195519] getsockname(6, <unfinished ...> [pid 195519] <... getsockname resumed>{sa_family=AF_INET, sin_port=htons(47746), sin_addr=inet_addr("172.16.0.46")}, [112->16]) = 0 [pid 195519] setsockopt(6, SOL_TCP, TCP_KEEPINTVL, [15], 4) = 0 [pid 195519] setsockopt(6, SOL_TCP, TCP_KEEPIDLE, [15], 4 <unfinished ...> [pid 8311] write(6, "GET / HTTP/1.1\r\nHost: www.baidu.com\r\nUser-Agent: Go-http-client/1.1\r\nAccept-Encoding: gzip\r\n\r\n", 94 <unfinished ...> [pid 192595] read(6, <unfinished ...> [pid 192595] <... read resumed>"HTTP/1.1 200 OK\r\nBdpagetype: 1\r\nBdqid: 0xc43c9f460008101b\r\nCache-Control: private\r\nConnection: keep-alive\r\nContent-Encoding: gzip\r\nContent-Type: text/html;charset=utf-8\r\nDate: Wed, 09 Dec 2020 13:46:30 GMT\r\nExpires: Wed, 09 Dec 2020 13:45:33 GMT\r\nP3p: CP=\" OTI DSP COR IVA OUR IND COM \"\r\nP3p: CP=\" OTI DSP COR IVA OUR IND COM \"\r\nServer: BWS/1.1\r\nSet-Cookie: BAIDUID=996EE645C83622DF7343923BF96EA1A1:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com\r\nSet-Cookie: BIDUPSID=99"..., 4096) = 4096 [pid 192595] close(6 <unfinished ...>
邏輯代碼
那么到這里,可以大概猜測問題所在,找到業(yè)務(wù)方涉及到 httpclient 的邏輯代碼。偽代碼如下,跟上面的結(jié)論一樣,只是讀取了header,但并未讀取完response body數(shù)據(jù)。
還以為是特殊的場景,結(jié)果是使用不當(dāng),把請求投遞過去后只判斷 http code?真正的業(yè)務(wù) code 是在 body 里的。????
urls := []string{...} for _, url := range urls { resp, err := http.Post(url, ...) if err != nil { // ... } if resp.StatusCode == http.StatusOK { continue } // handle redis cache // handle mongodb // handle rocketmq // ... resp.Body.Close() }
如何解決
不細(xì)說了,把 header length 長度的數(shù)據(jù)讀完就可以了。
先不管別人使用不當(dāng),再分析下為何出現(xiàn)短連接,連接不能復(fù)用的問題。
為什么不讀取 body 就出問題?其實 http.Response 字段描述中已經(jīng)有說明了。當(dāng) Body 未讀完時,連接可能不能復(fù)用。
// The http Client and Transport guarantee that Body is always // non-nil, even on responses without a body or responses with // a zero-length body. It is the caller's responsibility to // close Body. The default HTTP client's Transport may not // reuse HTTP/1.x "keep-alive" TCP connections if the Body is // not read to completion and closed. // // The Body is automatically dechunked if the server replied // with a "chunked" Transfer-Encoding. // // As of Go 1.12, the Body will also implement io.Writer // on a successful "101 Switching Protocols" response, // as used by WebSockets and HTTP/2's "h3c" mode. Body io.ReadCloser
眾所周知,golang httpclient 要注意 response Body 關(guān)閉問題,但上面的 case 確實有關(guān)了 body,只是非常規(guī)地沒去讀取 reponse body 數(shù)據(jù)。這樣會造成連接異常關(guān)閉,繼而引起連接池不能復(fù)用。
一般 http 協(xié)議解釋器是要先解析 header,再解析 body,結(jié)合當(dāng)前的問題開始是這么推測的,連接的 readLoop 收到一個新請求,然后嘗試解析 header 后,返回給調(diào)用方等待讀取 body,但調(diào)用方?jīng)]去讀取,而選擇了直接關(guān)閉 body。那么后面當(dāng)一個新請求被 transport roundTrip 再調(diào)度請求時,readLoop 的 header 讀取和解析會失敗,因為他的讀緩沖區(qū)里有前面未讀的數(shù)據(jù),必然無法解析 header。按照常見的網(wǎng)絡(luò)編程原則,協(xié)議解析失敗,直接關(guān)閉連接。
想是這么想的,但還是看了 golang net/http 的代碼,結(jié)果不是這樣的。????
httpclient 每個連接會創(chuàng)建讀寫協(xié)程兩個協(xié)程,分別使用 reqch 和 writech 來跟 roundTrip 通信。上層使用的response.Body 其實是經(jīng)過多次封裝的,一次封裝的 body 是直接跟 net.conn 進行交互讀取,二次封裝的 body 則是加強了 close 和 eof 處理的 bodyEOFSignal。
當(dāng)未讀取 body 就進行 close 時,會觸發(fā) earlyCloseFn() 回調(diào),看 earlyCloseFn 的函數(shù)定義,在 close 未見 io.EOF 時才調(diào)用。自定義的 earlyCloseFn 方法會給 readLoop 監(jiān)聽的 waitForBodyRead 傳入 false, 這樣引發(fā) alive 為 false 不能繼續(xù)循環(huán)的接收新請求,只能是退出調(diào)用注冊過的 defer 方法,關(guān)閉連接和清理連接池。
// xiaorui.cc func (pc *persistConn) readLoop() { closeErr := errReadLoopExiting // default value, if not changed below defer func() { pc.close(closeErr) // 關(guān)閉連接 pc.t.removeIdleConn(pc) // 從連接池中刪除 }() ... alive := true for alive { ... rc := <-pc.reqch // 從管道中拿到請求,roundTrip 對該管道進行輸入 trace := httptrace.ContextClientTrace(rc.req.Context()) var resp *Response if err == nil { resp, err = pc.readResponse(rc, trace) // 更多的是解析 header } else { err = transportReadFromServerError{err} closeErr = err } ... waitForBodyRead := make(chan bool, 2) body := &bodyEOFSignal{ body: resp.Body, // 提前關(guān)閉 !!! 輸出false earlyCloseFn: func() error { waitForBodyRead <- false ... }, // 正常收尾 !!! fn: func(err error) error { isEOF := err == io.EOF waitForBodyRead <- isEOF ... }, } resp.Body = body select { case rc.ch <- responseAndError{res: resp}: case <-rc.callerGone: return } select { case bodyEOF := <-waitForBodyRead: replaced := pc.t.replaceReqCanceler(rc.cancelKey, nil) // before pc might return to idle pool // alive 為 false, 不能繼續(xù) continue alive = alive && bodyEOF && !pc.sawEOF && pc.wroteRequest() && replaced && tryPutIdleConn(trace) ... case <-rc.req.Cancel: alive = false pc.t.CancelRequest(rc.req) case <-rc.req.Context().Done(): alive = false pc.t.cancelRequest(rc.cancelKey, rc.req.Context().Err()) case <-pc.closech: alive = false } } }
bodyEOFSignal 的 Close():
// xiaorui.cc func (es *bodyEOFSignal) Close() error { es.mu.Lock() defer es.mu.Unlock() if es.closed { return nil } es.closed = true if es.earlyCloseFn != nil && es.rerr != io.EOF { return es.earlyCloseFn() } err := es.body.Close() return es.condfn(err) }
最終會調(diào)用 persistConn 的 close(), 連接關(guān)閉并關(guān)閉closech:
// xiaorui.cc func (pc *persistConn) close(err error) { pc.mu.Lock() defer pc.mu.Unlock() pc.closeLocked(err) } func (pc *persistConn) closeLocked(err error) { if err == nil { panic("nil error") } pc.broken = true if pc.closed == nil { pc.closed = err pc.t.decConnsPerHost(pc.cacheKey) if pc.alt == nil { if err != errCallerOwnsConn { pc.conn.Close() // 關(guān)閉連接 } close(pc.closech) // 通知讀寫協(xié)程 } } }
關(guān)于Go http client 連接池不復(fù)用如何解決就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,可以學(xué)到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。