濮阳杆衣贸易有限公司

主頁(yè) > 知識(shí)庫(kù) > Go http client 連接池不復(fù)用的問(wèn)題

Go http client 連接池不復(fù)用的問(wèn)題

熱門標(biāo)簽:上海極信防封電銷卡價(jià)格 重慶慶云企業(yè)400電話到哪申請(qǐng) 湛江crm外呼系統(tǒng)排名 不封卡外呼系統(tǒng) 宿遷便宜外呼系統(tǒng)代理商 仙桃400電話辦理 鄭州智能語(yǔ)音電銷機(jī)器人價(jià)格 地圖標(biāo)注免費(fèi)定制店 寧波語(yǔ)音外呼系統(tǒng)公司

當(dāng) http client 返回值為不為空,只讀取 response header,但不讀 body 內(nèi)容就執(zhí)行 response.Body.Close(),那么連接會(huì)被主動(dòng)關(guān)閉,得不到復(fù)用。

測(cè)試代碼如下:

// 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 外,很少會(huì)有只讀取 header 的需求吧。

話說(shuō),golang httpclient 需要注意的地方著實(shí)不少。

  • 如沒(méi)有 response.Body.Close(),有些小場(chǎng)景造成 persistConn 的 writeLoop 泄露。
  • 如 header 和 body 都不管,那么會(huì)造成泄露的連接干滿連接池,后面的請(qǐng)求只能是短連接。

上下文

由于某幾個(gè)業(yè)務(wù)系統(tǒng)會(huì)瘋狂調(diào)用各區(qū)域不同的 k8s 集群,為減少跨機(jī)房帶來(lái)的時(shí)延、兼容新老 k8s 集群 api、減少k8s api-server 的負(fù)載,故而開(kāi)發(fā)了 k8scache 服務(wù)。在部署運(yùn)行后開(kāi)始對(duì)該服務(wù)進(jìn)行監(jiān)控,發(fā)現(xiàn) metrics 呈現(xiàn)的 QPS 跟連接數(shù)不成正比,qps 為 1500,連接數(shù)為 10 個(gè)。開(kāi)始以為觸發(fā) idle timeout 被回收,但通過(guò)歷史監(jiān)控圖分析到連接依然很少。????

按照對(duì) k8scache 調(diào)用方的理解,他們經(jīng)常粗暴的開(kāi)啟不少協(xié)程來(lái)對(duì) k8scache 進(jìn)行訪問(wèn)。已知默認(rèn)的 golang httpclient transport 對(duì)連接數(shù)是有默認(rèn)限制的,連接池總大小為 100,每個(gè) host 連接數(shù)為 2。當(dāng)并發(fā)對(duì)某 url 進(jìn)行請(qǐng)求時(shí),無(wú)法歸還連接池,也就是超過(guò)連接池大小的連接會(huì)被主動(dòng)clsoe()。所以,我司的 golang 腳手架中會(huì)對(duì)默認(rèn)的 httpclient 創(chuàng)建高配的 transport,不太可能出現(xiàn)連接池爆滿被 close 的問(wèn)題。

如果真的是連接池爆了?  誰(shuí)主動(dòng)挑起關(guān)閉,誰(shuí)就有 tcp time-wait 狀態(tài),但通過(guò) netstat 命令只發(fā)現(xiàn)少量跟 k8scache 相關(guān)的 time-wait。

排查問(wèn)題

已知問(wèn)題,  為隱藏敏感信息,索性使用簡(jiǎn)單的場(chǎng)景設(shè)立問(wèn)題的 case

tcpdump抓包分析問(wèn)題?

包信息如下,通過(guò)最后一行可以確認(rèn)是由客戶端主動(dòng)觸發(fā) RST連接重置 。觸發(fā)RST的場(chǎng)景有很多,但常見(jiàn)的有 tw_bucket 滿了、tcp 連接隊(duì)列爆滿且開(kāi)啟 tcp_abort_on_overflow、配置 so_linger、讀緩沖區(qū)還有數(shù)據(jù)就給 close。

通過(guò) linux 監(jiān)控和內(nèi)核日志可以確認(rèn)不是內(nèi)核配置的問(wèn)題,配置 so_linger 更不可能。???? 大概率就一個(gè)可能,關(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

通過(guò) lsof 找到進(jìn)程關(guān)聯(lián)的 TCP 連接,然后使用 ss 或 netstat 查看讀寫(xiě)緩沖區(qū)。信息如下,recv-q 讀緩沖區(qū)確實(shí)是存在數(shù)據(jù)。這個(gè)緩沖區(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)用

通過(guò)系統(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)在正在進(jì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 ...>

邏輯代碼

那么到這里,可以大概猜測(cè)問(wèn)題所在,找到業(yè)務(wù)方涉及到 httpclient 的邏輯代碼。偽代碼如下,跟上面的結(jié)論一樣,只是讀取了header,但并未讀取完response body數(shù)據(jù)。

還以為是特殊的場(chǎng)景,結(jié)果是使用不當(dāng),把請(qǐng)求投遞過(guò)去后只判斷 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ì)說(shuō)了,把 header length 長(zhǎng)度的數(shù)據(jù)讀完就可以了。

分析問(wèn)題

先不管別人使用不當(dāng),再分析下為何出現(xiàn)短連接,連接不能復(fù)用的問(wèn)題。

為什么不讀取 body 就出問(wèn)題?其實(shí) http.Response 字段描述中已經(jīng)有說(shuō)明了。當(dāng) Body 未讀完時(shí),連接可能不能復(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 "h2c" mode.
 Body io.ReadCloser

眾所周知,golang httpclient 要注意 response Body 關(guān)閉問(wèn)題,但上面的 case 確實(shí)有關(guān)了 body,只是非常規(guī)地沒(méi)去讀取 reponse body 數(shù)據(jù)。這樣會(huì)造成連接異常關(guān)閉,繼而引起連接池不能復(fù)用。

一般 http 協(xié)議解釋器是要先解析 header,再解析 body,結(jié)合當(dāng)前的問(wèn)題開(kāi)始是這么推測(cè)的,連接的 readLoop 收到一個(gè)新請(qǐng)求,然后嘗試解析 header 后,返回給調(diào)用方等待讀取 body,但調(diào)用方?jīng)]去讀取,而選擇了直接關(guān)閉 body。那么后面當(dāng)一個(gè)新請(qǐng)求被 transport roundTrip 再調(diào)度請(qǐng)求時(shí),readLoop 的 header 讀取和解析會(huì)失敗,因?yàn)樗淖x緩沖區(qū)里有前面未讀的數(shù)據(jù),必然無(wú)法解析 header。按照常見(jiàn)的網(wǎng)絡(luò)編程原則,協(xié)議解析失敗,直接關(guān)閉連接。

想是這么想的,但還是看了 golang net/http 的代碼,結(jié)果不是這樣的。????

分析源碼

httpclient 每個(gè)連接會(huì)創(chuàng)建讀寫(xiě)協(xié)程兩個(gè)協(xié)程,分別使用 reqch 和 writech 來(lái)跟 roundTrip 通信。上層使用的response.Body 其實(shí)是經(jīng)過(guò)多次封裝的,一次封裝的 body 是直接跟 net.conn 進(jìn)行交互讀取,二次封裝的 body 則是加強(qiáng)了 close 和 eof 處理的 bodyEOFSignal。

當(dāng)未讀取 body 就進(jìn)行 close 時(shí),會(huì)觸發(fā) earlyCloseFn() 回調(diào),看 earlyCloseFn 的函數(shù)定義,在 close 未見(jiàn) io.EOF 時(shí)才調(diào)用。自定義的 earlyCloseFn 方法會(huì)給 readLoop 監(jiān)聽(tīng)的 waitForBodyRead 傳入 false,  這樣引發(fā) alive 為 false 不能繼續(xù)循環(huán)的接收新請(qǐng)求,只能是退出調(diào)用注冊(cè)過(guò)的 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 // 從管道中拿到請(qǐng)求,roundTrip 對(duì)該管道進(jìn)行輸入
 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)
}

最終會(huì)調(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ě)協(xié)程
 }
 }
}

總之

同事的 httpclient 使用方法有些奇怪,除了 head method 之外,還真想不到有不讀取 body 的請(qǐng)求。所以,大家知道 httpclient 有這么一回事就行了。

另外,一直覺(jué)得 net/http 的代碼太繞,沒(méi)看過(guò)一些介紹直接看代碼很容易陷進(jìn)去,有時(shí)間專門講講 http client 的實(shí)現(xiàn)。

到此這篇關(guān)于Go http client 連接池不復(fù)用的問(wèn)題的文章就介紹到這了,更多相關(guān)Go http client 連接池內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

您可能感興趣的文章:
  • go 原生http web 服務(wù)跨域restful api的寫(xiě)法介紹
  • golang http使用踩過(guò)的坑與填坑指南
  • Golang實(shí)現(xiàn)http server提供壓縮文件下載功能
  • golang語(yǔ)言http協(xié)議get拼接參數(shù)操作
  • 在go文件服務(wù)器加入http.StripPrefix的用途介紹
  • Golang 實(shí)現(xiàn)分片讀取http超大文件流和并發(fā)控制
  • Go 實(shí)現(xiàn)HTTP中間人代理的操作

標(biāo)簽:儋州 青海 海南 安康 遼寧 西雙版納 電子產(chǎn)品 物業(yè)服務(wù)

巨人網(wǎng)絡(luò)通訊聲明:本文標(biāo)題《Go http client 連接池不復(fù)用的問(wèn)題》,本文關(guān)鍵詞  http,client,連接,池,不復(fù),;如發(fā)現(xiàn)本文內(nèi)容存在版權(quán)問(wèn)題,煩請(qǐng)?zhí)峁┫嚓P(guān)信息告之我們,我們將及時(shí)溝通與處理。本站內(nèi)容系統(tǒng)采集于網(wǎng)絡(luò),涉及言論、版權(quán)與本站無(wú)關(guān)。
  • 相關(guān)文章
  • 下面列出與本文章《Go http client 連接池不復(fù)用的問(wèn)題》相關(guān)的同類信息!
  • 本頁(yè)收集關(guān)于Go http client 連接池不復(fù)用的問(wèn)題的相關(guān)信息資訊供網(wǎng)民參考!
  • 推薦文章
    廉江市| 周宁县| 泗阳县| 海阳市| 资溪县| 饶河县| 玉田县| 泸水县| 浮梁县| 西青区| 临朐县| 辛集市| 左贡县| 抚州市| 北碚区| 安图县| 鄂州市| 榕江县| 邵武市| 个旧市| 灯塔市| 逊克县| 肥西县| 鄱阳县| 濮阳县| 襄樊市| 邹平县| 固始县| 南京市| 池州市| 涞源县| 冀州市| 通城县| 商丘市| 马公市| 冷水江市| 富锦市| 丘北县| 蛟河市| 黄冈市| 石门县|