最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Go http client 連接池不復用的問題

 更新時間:2021年01月13日 09:24:02   作者:qcrao  
這篇文章主要介紹了Go http client 連接池不復用的問題,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧

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

測試代碼如下:

// 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è)務系統(tǒng)會瘋狂調(diào)用各區(qū)域不同的 k8s 集群,為減少跨機房帶來的時延、兼容新老 k8s 集群 api、減少k8s api-server 的負載,故而開發(fā)了 k8scache 服務。在部署運行后開始對該服務進行監(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 進行訪問。已知默認的 golang httpclient transport 對連接數(shù)是有默認限制的,連接池總大小為 100,每個 host 連接數(shù)為 2。當并發(fā)對某 url 進行請求時,無法歸還連接池,也就是超過連接池大小的連接會被主動clsoe()。所以,我司的 golang 腳手架中會對默認的 httpclient 創(chuàng)建高配的 transport,不太可能出現(xiàn)連接池爆滿被 close 的問題。

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

排查問題

已知問題,  為隱藏敏感信息,索性使用簡單的場景設立問題的 case

tcpdump抓包分析問題?

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

通過 linux 監(jiān)控和內(nèi)核日志可以確認不是內(nèi)核配置的問題,配置 so_linger 更不可能。???? 大概率就一個可能,關閉未清空讀緩沖區(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 找到進程關聯(lián)的 TCP 連接,然后使用 ss 或 netstat 查看讀寫緩沖區(qū)。信息如下,recv-q 讀緩沖區(qū)確實是存在數(shù)據(jù)。這個緩沖區(qū)字節(jié)一直未讀,直到連接關閉引發(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è)務方涉及到 httpclient 的邏輯代碼。偽代碼如下,跟上面的結論一樣,只是讀取了header,但并未讀取完response body數(shù)據(jù)。

還以為是特殊的場景,結果是使用不當,把請求投遞過去后只判斷 http code?真正的業(yè)務 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()
}

如何解決

不細說了,把 header length 長度的數(shù)據(jù)讀完就可以了。

分析問題

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

為什么不讀取 body 就出問題?其實 http.Response 字段描述中已經(jīng)有說明了。當 Body 未讀完時,連接可能不能復用。

 // 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 關閉問題,但上面的 case 確實有關了 body,只是非常規(guī)地沒去讀取 reponse body 數(shù)據(jù)。這樣會造成連接異常關閉,繼而引起連接池不能復用。

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

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

分析源碼

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

當未讀取 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 方法,關閉連接和清理連接池。

// xiaorui.cc
 
func (pc *persistConn) readLoop() {
 closeErr := errReadLoopExiting // default value, if not changed below
 defer func() {
 pc.close(closeErr)   // 關閉連接
 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,
  // 提前關閉 !!! 輸出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(), 連接關閉并關閉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() // 關閉連接
  }
  close(pc.closech) // 通知讀寫協(xié)程
 }
 }
}

總之

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

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

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

相關文章

  • Golang實現(xiàn)密碼加密的示例詳解

    Golang實現(xiàn)密碼加密的示例詳解

    數(shù)據(jù)庫在存儲密碼時,不能明文存儲,需要加密后存儲,而Golang中的加密算法有很多種,下面小編就來通過簡單的示例和大家簡單聊聊吧
    2023-07-07
  • Golang學習筆記(六):struct

    Golang學習筆記(六):struct

    這篇文章主要介紹了Golang學習筆記(六):struct,本文講解了struct的聲明及初始化、struct的匿名字段(繼承)、method、method繼承和重寫等內(nèi)容,需要的朋友可以參考下
    2015-05-05
  • 自己動手用Golang實現(xiàn)約瑟夫環(huán)算法的示例

    自己動手用Golang實現(xiàn)約瑟夫環(huán)算法的示例

    這篇文章主要介紹了自己動手用Golang實現(xiàn)約瑟夫環(huán)算法的示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2019-12-12
  • Windows下使用go語言寫程序安裝配置實例

    Windows下使用go語言寫程序安裝配置實例

    這篇文章主要介紹了Windows下使用go語言寫程序安裝配置實例,本文講解了安裝go語言、寫go代碼、生成可執(zhí)行文件、批量生成可執(zhí)行文件等內(nèi)容,需要的朋友可以參考下
    2015-03-03
  • Go底層channel實現(xiàn)原理及示例詳解

    Go底層channel實現(xiàn)原理及示例詳解

    這篇文章主要介紹了Go底層channel實現(xiàn)原理及示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-08-08
  • Go語言使用Zap輕松搞定結構化日志

    Go語言使用Zap輕松搞定結構化日志

    在?Go?語言中,有許多日志庫可供選擇,但在性能和靈活性方面,Zap?是其中的佼佼者,下面我們就來看看Go?項目中如何使用?Zap?進行結構化日志記錄吧
    2024-11-11
  • Golang實現(xiàn)斷點續(xù)傳功能

    Golang實現(xiàn)斷點續(xù)傳功能

    這篇文章主要為大家詳細介紹了Golang實現(xiàn)斷點續(xù)傳、復制文件功能,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-07-07
  • 使用golang如何優(yōu)雅的關機或重啟操作示例

    使用golang如何優(yōu)雅的關機或重啟操作示例

    這篇文章主要為大家介紹了使用golang如何優(yōu)雅的關機或重啟操作示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步早日升職加薪
    2022-04-04
  • Go語言中for和range的性能比較

    Go語言中for和range的性能比較

    這篇文章主要為大家詳細介紹了Go語言中for和range語句的使用以及性能比較,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起了解一下
    2023-07-07
  • Golang map如何生成有序的json數(shù)據(jù)詳解

    Golang map如何生成有序的json數(shù)據(jù)詳解

    最近在學習Golang,發(fā)現(xiàn)了一個問題,覺著有必要給大家總結下,下面這篇文章主要給大家介紹了關于Golang map如何生成有序json數(shù)據(jù)的相關資料,文中通過示例代碼介紹的非常詳細,需要的朋友們下面來一起看看吧。
    2017-07-07

最新評論

灵丘县| 长武县| 治县。| 衡阳市| 南华县| 曲阳县| 阿拉善左旗| 万山特区| 景东| 荣成市| 雷山县| 晋中市| 宽城| 资兴市| 镇坪县| 大荔县| 黄梅县| 和田市| 确山县| 施秉县| 舞钢市| 平昌县| 温宿县| 鱼台县| 长垣县| 台中县| 佛教| 湘西| 富平县| 金华市| 涪陵区| 健康| 禹城市| 镇江市| 开江县| 鄂尔多斯市| 涟水县| 汕尾市| 抚顺县| 泸溪县| 彭州市|