Go http client接続池が多重化されていない問題


http clientの戻り値が空ではなく、reponse headerのみを読みますが、bodyの内容を読まないで、reponse.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の注意すべきところは本当に多いです.
  • 、例えば、レスリング・Body.Close()がない場合、いくつかのシーンでは、persistConのwriteLoopが漏れることがあります.
  • headerとbodyのいずれにも関わらず、漏れの原因となる接続が干満の接続池になり、後の要求は だけである.
  • コンテキスト
    いくつかの業務システムが狂ったように各エリアの異なるk 8 sクラスタを呼び出して、機械室を跨いでの遅延を減らすために、新しい古いk 8 sクラスタappiに対応して、k 8 s appi-serverの負荷を減らすために、k 8 scacheサービスを開発しました.配備運転後にこのサービスを監視し始めたところ、metricsが提示するQPSは接続数に比例しないことが分かりました.qpsは1500で、接続数は10個です.トリガのIDle timeoutは回収されたと思いますが、履歴監視図で接続はまだ少ないです.????
    k 8 scache呼び出し側の理解によって、彼らはいつも乱暴に多くの協力プログラムを開いてK 8 scacheを訪問します.デフォルトのgolang httpclient transportは接続数にデフォルト制限があり、接続池の総サイズは100で、各host接続数は2です.あるurlを同時に要求すると、接続池の返却ができません.つまり接続池の大きさを超えた接続は自動的にclsoe()されます.だから、我が社のゴンランドの足場ではデフォルトのhttpclientに対してハイスペックのトランスポートを作成します.接続池がクローズアップされているという問題はあまりありません.
    もし本当に接続池が爆発したら? 誰が自発的に仕掛けて閉鎖して、誰がtcp time-wait状態があって、しかしnetstat命令を通じて(通って)少量k 8 scacheと関係があるtime-waitだけを発見します.
    問題をつきとめる
    問題をすでに知っています 敏感な情報を隠すために、簡単なシーンで問題のcaseを設定します.
    tcpdumpは問題を分析しますか?
    パケット情報は、クライアントアクティブトリガRST が最後の行で確認される.RSTをトリガするシーンが多いですが、よくあるのはtw_です.bucketがいっぱいになりました.tcp接続列がいっぱいになりました.tcp_を開けます.abort_うむoverflow、配置so_linger、バッファを読んで、データがまだあります.closeにあげます.
    linux監視とカーネルログにより、カーネルの設定ではないことが確認できます.リンガーの方がもっと不可能です.???大体の確率は可能です.クリアされていない読み取りバッファの接続を閉じます.
    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を通じて、プロセス関連のTCP接続を見つけて、ssまたはnetstatを使って読み書きバッファを確認します.情報は以下の通りです.recv-q読み取りバッファは確かにデータが存在します.このバッファバイトはまだ読んでいません.接続が切れるまで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トレースシステム呼び出し
    システム呼び出しによって分析できますが、headerの部分だけが読み取られたようです.まだbodyのデータが読めていません.
    [pid  8311] connect(6, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("110.242.68.3")}, 16 
    [pid 195519] epoll_pwait(3,  
    [pid  8311] <...>)      = -1 EINPROGRESS (        )
    [pid  8311] epoll_ctl(3, EPOLL_CTL_ADD, 6, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=2350546712, u64=140370471714584}} 
    [pid 195519] getsockopt(6, SOL_SOCKET, SO_ERROR,  
    [pid 192592] nanosleep({tv_sec=0, tv_nsec=20000},  
    [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,  
    [pid 195519] <...>{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 
    [pid  8311] write(6, "GET / HTTP/1.1\r
    Host: www.baidu.com\r
    User-Agent: Go-http-client/1.1\r
    Accept-Encoding: gzip\r
    \r
    ", 94  [pid 192595] read(6,   [pid 192595] <...>"HTTP/1.1 200 OK\r
    Bdpagetype: 1\r
    Bdqid: 0xc43c9f460008101b\r
    Cache-Control: private\r
    Connection: keep-alive\r
    Content-Encoding: gzip\r
    Content-Type: text/html;charset=utf-8\r
    Date: Wed, 09 Dec 2020 13:46:30 GMT\r
    Expires: Wed, 09 Dec 2020 13:45:33 GMT\r
    P3p: CP=\" OTI DSP COR IVA OUR IND COM \"\r
    P3p: CP=\" OTI DSP COR IVA OUR IND COM \"\r
    Server: BWS/1.1\r
    Set-Cookie: BAIDUID=996EE645C83622DF7343923BF96EA1A1:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com\r
    Set-Cookie: BIDUPSID=99"..., 4096) = 4096 [pid 192595] close(6 
    論理コード
    ここでは、問題の所在を大体推測して、業務先がhttpclientに関わる論理コードを見つけます.ダミーコードは、上の結論と同じように、headerだけ読みましたが、reponse bodyデータは読み終わりませんでした.
    特殊なシーンだと思いましたが、結果的に使い方が間違っていました.依頼を投函してからhttp 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の長さのデータを読んでください.
    問題を分析する
    他の人が使うべきではないにもかかわらず、なぜ短い接続ができて、多重化できないのかを分析します.
    なぜbodyを読み込まないと問題が発生しますか?実はhttp.Resonseフィールドの記述には既に説明があります.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はreponse Bodyのクローズに注意しなければなりませんが、上のcaseは確かにbodyに関連しています.ただし、例外的にreponse bodyのデータを読み取っていません.このように接続が異常に閉鎖され、接続池が多重化できなくなることがあります.
    一般的にhttpプロトコルの解釈器はまずheaderを解析してからbodyを解析して、現在の問題と結びつけてこのように推測しています.接続のreadLoopは新しい要求を受け取って、headerを解析してみてから、呼び出し元に戻ってbodyを読むのを待っていますが、呼び出し側は読み取りに行かず、直接bodyを閉じるように選択しました.その後、新しい要求がtransport roundTripによって再スケジュールされた時、readLoopのheaderは読み取りと解析に失敗しました.彼の読み取りバッファには前の未読のデータがありますので、headerは解析できません.一般的なネットワークプログラミングの原則に従って、プロトコルの解析に失敗し、直接接続を閉じます.
    そう思いますが、golang net/httpのコードを見ましたが、結局はそうではありませんでした.???
    ソースの分析
    httpclientは接続ごとに読み書きの協働工程を作成します.それぞれreqchとwritechを使ってroundTripと通信します.上の段阶で使用されているレスポンス.Bodyは何回もカプセル化されています.一回のパッケージのbodyは直接にnet.co nnと交互に読み取っています.二次パッケージのbodyはcloseとeof処理を强化したbody EOFriginalです.
    bodyを読み取らずにcloseを行うと、earlyClose Fn()のコールバックがトリガされ、earlyClose Fnの関数定義を見て、closeがio.EOFに会えない時に呼び出します.カスタムearlyCloose Fn方法はreadLoopの傍受するwaitForBodyReadにfalseに伝えられます. このように、aliveがfalseのためにループを続けることができない新しい要求を受信するには、登録された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 := 
    bodyEOFriginalの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)
    }
    
    
    最終的には、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) //       
      }
     }
    }
    
    とにかく
    同僚のhttpclientの使い方がおかしいです.head method以外に、bodyを読み込まないという依頼があるとは思いませんでした.だから、みんなはhttpclientがこのようなことがあると知っています.
    また、ネット/httpのコードが回りくどいと感じています.紹介を見たことがないので、直接コードを見ても、はまりやすいです.http clientの実現を専門に説明する時間があります.
    Go http client 连接池不复用的问题_第1张图片