net-http-transport 引发的句柄数(协程)泄漏问题

Reference

  • 关于 Golang 中 http.Response.Body 未读取导致连接复用问题的一点研究
  • https://manishrjain.com/must-close-golang-http-response
  • https://www.reddit.com/r/golang/comments/13fphyz/til_go_response_body_must_be_closed_even_if_you/?rdt=35002
  • https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779
  • https://stackoverflow.com/questions/17948827/reusing-http-connections-in-go
  • 内存泄漏排查:https://lessisbetter.site/2019/05/18/go-goroutine-leak/

最近在工作中遇到一个句柄数泄漏的问题,在排查的过程中学习了很多关于 net/http 源码库的一些使用和原理

问题描述

背景:一个负责告警规则判断的服务,vmAlert,主要流程是根据用户配置的规则,查询对应的指标是否满足告警的阈值条件,从而进行告警
现象:vmAlert,在执行一段时间后,提示panic掉了,提示” too many open file “ 相关的错误

image.png

进程数:
分析来看,vmalert 在频繁地与 某个服务 建立 TCP 连接,并且频繁地打开和关闭 socket 套接字,由于建立连接的操作与关闭连接的操作频率不 一致,从而使得连接数持续增加,最后超过 ulimit -Sn 和 ulimit -Hn 65535 的限制 [[Linux 的最大TCP连接数]];大胆猜测可能是任意一个HTTP请求的问题

image.png

image.png

排查过程

从现象看本质,HTTP 请求本质上是建立一个TCP连接,每建立一个TCP连接,则需要打开对应的文件,而 fd 则是控制这些文件的一个数据结构,fd 泄漏了,则说明几个可能性:

  • 某个地方连接未被释放
  • 打开fd的速度远远快于释放的速度

从这个思路排查,找一下服务里面调用外部的HTTP请求

  1. 检查 vm 的相关请求的连接和代码 使用的自定义的连接池,并且连接都正常建立连接,看起来没啥问题
  2. 检查心跳接口相关的连接和代码

在 vmalert 运行了一段时间后,通过 lsof -p [进程号] 查看其打开的文件连接,发现 vmselect 的TCP连接数较为稳定,连接数与 vmalert 的在执行的 组数基本是相同的 检查发现存在大量的 sock 连接,HTTP请求会建立TCP连接,从而打开socket进行读写,从这个角度去排查;

image.png

检视 心跳相关的代码,每30s执行一次,使用timer.ticker 进行定时调用,错误也处理了,一个简单的post心跳上报请求; resp 也不读取,直接 _ 进行忽略即可,也不用close调,看起来没啥问题
image.png

在 lsof -p [进程号] 中偶然发现与一个后端服务Backend存在 CLOSE_WAIT 的连接,并且 src port 也不一样,也就是意味着心跳接口的 TCP 连接在重建?
image.png

大胆猜测,http.Post 这个调用没有使用到长连接? 减少interval时间为1s,执行10m,再次 lsof 看下,发现存在大量的 sock 连接,整体打开的fd数上涨到了 711

image.png

并且出现了大量的 close_wait 连接

image.png

基本就能定位到是这个段代码的问题了,线上增长缓慢是因为interval为30s,fd会慢增加; 查看容器配置的最大可打开文件数为 65535,超过就会进行报错;

回看心跳的代码,只是使用了http.Post 发送一个简单的上报心跳请求,为什么会导致句柄泄漏的问题呢?难道每个HTTP请求建立了新的TCP连接吗?看下 net/http 的代码 [[net-http-transport]]

// DefaultClient is the default [Client] and is used by [Get], [Head], and [Post].
var DefaultClient = &Client{}func Post(url, contentType string, body io.Reader) (resp *Response, err error) { return DefaultClient.Post(url, contentType, body)  
}func (c *Client) Post(url, contentType string, body io.Reader) (resp *Response, err error) {  req, err := NewRequest("POST", url, body)  if err != nil {  return nil, err  }  req.Header.Set("Content-Type", contentType)  return c.Do(req)  
}func (c *Client) do(req *Request) (retres *Response, reterr error) {// ... 省略不关键的部分if resp, didTimeout, err = c.send(req, deadline); err != nil {  // c.send() always closes req.Body  reqBodyClosed = true  if !deadline.IsZero() && didTimeout() {  err = &timeoutError{err.Error() + " (Client.Timeout exceeded while awaiting headers)"}  }  return nil, uerr(err)  }
}// didTimeout is non-nil only if err != nil.  
func (c *Client) send(req *Request, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {  if c.Jar != nil {  for _, cookie := range c.Jar.Cookies(req.URL) {  req.AddCookie(cookie)  }  }  resp, didTimeout, err = send(req, c.transport(), deadline)  if err != nil {  return nil, didTimeout, err  }  if c.Jar != nil {  if rc := resp.Cookies(); len(rc) > 0 {  c.Jar.SetCookies(req.URL, rc)  }  }  return resp, nil, nil  
}func (c *Client) transport() RoundTripper {  if c.Transport != nil {  return c.Transport  }  return DefaultTransport  
}// DefaultTransport is the default implementation of [Transport] and is// used by [DefaultClient]. It establishes network connections as needed  
// and caches them for reuse by subsequent calls. It uses HTTP proxies  
// as directed by the environment variables HTTP_PROXY, HTTPS_PROXY  
// and NO_PROXY (or the lowercase versions thereof).  
var DefaultTransport RoundTripper = &Transport{  Proxy: ProxyFromEnvironment,  DialContext: defaultTransportDialContext(&net.Dialer{  Timeout:   30 * time.Second,  KeepAlive: 30 * time.Second,  }),  ForceAttemptHTTP2:     true,  MaxIdleConns:          100,  IdleConnTimeout:       90 * time.Second,  TLSHandshakeTimeout:   10 * time.Second,  ExpectContinueTimeout: 1 * time.Second,  
}

可以看到,http.Post的方法,使用了默认的连接池,也就是会有TCP连接被 reuse 的,但从实际的情况(大量的close_wait)来看,又没复用连接,为什 么呢? 现在能大概猜测是没复用到长连接的问题,所以修改一下代码,使用短连接 connection为close看一下效果;
改完跑一段时间, lsof -p [进程号] 发现很正常,都是与 vmselect 建立的 ESTABLISH 连接,并且连接数固定在 50 左右;

实在是没什么思路了,范围就能确定是长连接没复用,导致不断建立新的TCP连接,导致socket被不断打开的问题,TCP 连接建立的速度比关闭的速度要快导致的;把心跳的代码丢到gpt问一下,一下子发现了新世界,重点: ”未关闭响应体可能导致fd泄漏“
image.png

?!没有使用到 resp 进行 io.Read 也需要关闭吗?这个时候就得 google 一下了,果然,找到了大佬们的一些解答

TIL: Go Response Body be MUST closed, even if you don’t read it

https://manishrjain.com/must-close-golang-http-response
文章中提到了几个点:

  • net/http 源码包中的 response.body 的描述,如果上一个body没有被读取完毕并且close掉,则这个 TCP 连接,不会被 reuse
  • 永远不要使用 HTTP.GET 以及 HTTP.DefaultClient,而应该使用自己创建的 httpclient
  • 无论是否需要(处理Response),都必须始终读Body并将其关闭

http.Response.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.

上述三点的一些个人理解:

  • 第一点官方已经给出说明了,如果你不读取完body并且关闭,则下一次请求过来,tcp 会认为上一个请求还未结束,则不会resue 这个连接,更详细的可以继续往下看根因分析
  • 第二点则是因为,使用这些方法时,defaultHttpClient 初始化未 var DefaultClient = &Client{},默认的 timeout 为 zero,即没有过期时间,这样客户端就不会主动去关闭这个连接,完全交给服务端决定是否断开,这样只要故障服务器决定等待,它们就会继续挂起。由于 API 调用是为了满足用户请求,这会导致满足用户请求的 goroutine 也挂起。一旦有足够多的调用,则应用程序就会崩溃;
  • 第三点,则是一个建议,但实际上有一定的隐患,可以参考这篇文章;关于 Golang 中 http.Response.Body 未读取导致连接复用问题的一点研究,主要提到了当网络出现阻塞时,读取body也会被阻塞,需要看实际的使用场景,如果你不需要resp的内容,不建议读,
  • 额外提一嘴
    • 如果你只进行 defer close,而不读取 resp,则tcp连接也不会复用
    • 如果你只进行读取 resp,而不 close 连接,则 tcp 连接可以复用(具体原因,可以看后续根因分析)

找到原因后,修改代码进行测试,发现 src port一直不变,说明复用的是同一个tcp连接

按照实际场景测试,30s触发一次心跳,发现tcp连接又不复用了,调整为5s又复用了,再次调整为 10s,发现有时候复用有时候不复用? 猜测是跟客户端的连接时间有关,检查一下客户端的初始化代码。检查了 transport 的idleconnTime 为 90s,也没啥问题,只能抓包看是哪边把连接关了,发现服务端 Backend 给 vmalert 发送了一个 FIN 关闭连接的报文,证明:是服务端主动关闭的连接

image.png

检查服务端的代码,发现设置了http的read和write的timeout为 10s, 破案

深挖根因

大量 fd 的根因

到底是net-http中哪一段代码导致的fd泄漏呢?这个问题还是不知道,我们继续深挖一下

在复现大量 close_wait 请求后,我们通过 pprof 看下服务的一些信息,主要看 top,trace,list

[root@vmalert-84cf77d57c-dwqzv nocalhost-dev]# go tool pprof http://0.0.0.0:xxxx/debug/pprof/goroutine
File: vmalert
Type: goroutine
Time: Jan 6, 2025 at 12:01pm (UTC)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 5474, 99.91% of 5479 total
Dropped 107 nodes (cum <= 27)
Showing top 10 nodes out of 29flat  flat%   sum%        cum   cum%5474 99.91% 99.91%       5474 99.91%  runtime.gopark0     0% 99.91%         77  1.41%  bufio.(*Reader).Peek0     0% 99.91%         77  1.41%  bufio.(*Reader).fill0     0% 99.91%         80  1.46%  internal/poll.(*FD).Read0     0% 99.91%         82  1.50%  internal/poll.(*pollDesc).wait0     0% 99.91%         82  1.50%  internal/poll.(*pollDesc).waitRead (inline)0     0% 99.91%         82  1.50%  internal/poll.runtime_pollWait0     0% 99.91%         79  1.44%  main.(*AlertingRule).Exec0     0% 99.91%        170  3.10%  main.(*Group).start0     0% 99.91%         79  1.44%  main.(*Group).start.func2(pprof) traces
File: vmalert
Type: goroutine
Time: Jan 6, 2025 at 11:52am (UTC)
-----------+-------------------------------------------------------2144   runtime.goparkruntime.selectgonet/http.(*persistConn).writeLoop
-----------+-------------------------------------------------------2060   runtime.goparkruntime.selectgonet/http.(*persistConn).readLoop(pprof) list writeLoop
Total: 4486
ROUTINE ======================== net/http.(*persistConn).writeLoop in /usr/local/go/src/net/http/transport.go0       2144 (flat, cum) 47.79% of Total.          .   2516:func (pc *persistConn) writeLoop() {.          .   2517:   defer close(pc.writeLoopDone).          .   2518:   for {.       2144   2519:           select {.          .   2520:           case wr := <-pc.writech:.          .   2521:                   startBytesWritten := pc.nwrite.          .   2522:                   err := wr.req.Request.write(pc.bw, pc.isProxy, wr.req.extra, pc.waitForContinue(wr.continueCh)).          .   2523:                   if bre, ok := err.(requestBodyReadError); ok {.          .   2524:                           err = bre.error(pprof) list readLoop
Total: 5479.          .   2322:           // Before looping back to the top of this function and peeking on.          .   2323:           // the bufio.Reader, wait for the caller goroutine to finish.          .   2324:           // reading the response body. (or for cancellation or death).       2562   2325:           select {.          .   2326:           case bodyEOF := <-waitForBodyRead:.          .   2327:                   alive = alive &&.          .   2328:                           bodyEOF &&.          .   2329:                           !pc.sawEOF &&.          .   2330:                           pc.wroteRequest() &&.          .   2331:                           tryPutIdleConn(rc.treq).          .   2332:                   if bodyEOF {.          .   2333:                           eofc <- struct{}{}.          .   2334:                   }.          1   2335:           case <-rc.treq.ctx.Done():.          .   2336:                   alive = false.          .   2337:                   pc.cancelRequest(context.Cause(rc.treq.ctx)).          .   2338:           case <-pc.closech:.          .   2339:                   alive = false.          .   2340:           }
... # 后面省略了

可以得到以下信息:

top命令

  • 总计:1450 个 goroutine,占总 goroutine 数量的 99.86%
  • 主要函数:
    • runtime.gopark:1450 次,几乎占据了所有的 goroutine
    • 其他函数如 bufio.(*Reader).Peek、internal/poll.(*FD).Read 等也有一定的调用次数,但相对较少

分析:

  • runtime.gopark:这个函数用于将当前 goroutine 暂停,直到某个条件满足。它通常在等待某些事件(如 I/O 操作、锁等)时被调用
  • bufio.(*Reader).Peek 和 bufio.(*Reader).fill:这些函数与缓冲 I/O 操作有关,表明有 goroutine 正在等待读取数据
  • internal/poll:这些函数与网络 I/O 操作有关,表明有 goroutine 正在等待网络连接的读操作

trace 命令

  • net/http.(*persistConn).writeLoop 和 net/http.(*persistConn).readLoop
  • 在协程暂停之前,最多调用就是这两个函数

list 命令

  • 可以看到具体是在哪个地方阻塞住了

那么 writeLoop 和 readLoop 到底是干嘛的呢?有兴趣详细了解可以参考这篇文章 [[net-http-transport]],下面就简单说一下,附带一个流程图

  • http 请求本质还是去发起一个 tcp 连接,net-http库通过transport来实现http请求的过程,通过一个请求池来控制空闲连接数量,transport 会尝试去申请一个 persistConn 持久连接,persistConn 负责控制读写过程
  • 如果存在空闲TCP连接则直接复用,否则通过 net-dial 重新请求一个连接;每个 persistConn 都会启动两个协程,也就是writeLoop 和 readLoop
    • persistConn 发送 request 信号,writeLoop 监听 writech 读取 request 并通过 bufio.Writer 写入 bw,也就是真正发送请求,服务端处理完,会把结果写回到 br
    • persistConn 与 readLoop 是通过 reqch 传递 request,readLoop 不断从br.Peek 检查是否有数据,有则读取然后写入到 respAndErr.ch ,persistConn 则进行最后的返回 resp

net-http.drawio.png

按照我们上面的 list 分析,我们可以知道具体是哪行代码阻塞了

readLoop 阻塞根因

先看下readLoop,可以发现其阻塞在了 waitForBodyRead;

go的select用法,在没有default的case情况下,会一直阻塞,直到满足某个case;

通过其注释我们能知道,readLoop退出的几个条件:

  • body 读取完毕
  • request 主动 cancel
  • request context Done 状态 true
  • 当前的 persistConn 关闭
// Before looping back to the top of this function and peeking on
// the bufio.Reader, wait for the caller goroutine to finish
// reading the response body. (or for cancellation or death) 
select {.          .   2326:           case bodyEOF := <-waitForBodyRead:}

继续看下 waitForBodyRead 是哪里传输的,发现是body中的一个回调函数 earlyCloseFn,这个函数又是bodyEOFSignal 这个结构体的变量,从注释可知, bodyEOFSignal作用是确保在读取到响应体的末尾EOF之前,不会继续在连接上进行其他读取操作 我们继续溯源

waitForBodyRead := make(chan bool, 2)// bodyEOFSignal is used by the HTTP/1 transport when reading response
// bodies to make sure we see the end of a response body before
// proceeding and reading on the connection again.
body := &bodyEOFSignal{body: resp.Body,earlyCloseFn: func() error {waitForBodyRead <- false<-eofc // will be closed by deferred call at the end of the functionreturn nil},fn: func(err error) error {isEOF := err == io.EOFwaitForBodyRead <- isEOFif isEOF {<-eofc // see comment above eofc declaration} else if err != nil {if cerr := pc.canceled(); cerr != nil {return cerr}}return err},
}
...func (es *bodyEOFSignal) Close() error {es.mu.Lock()defer es.mu.Unlock()if es.closed {return nil}es.closed = trueif es.earlyCloseFn != nil && es.rerr != io.EOF {return es.earlyCloseFn()}err := es.body.Close()return es.condfn(err)
}

Close 方法存在太多引用了,无法通过idea直接查询,但是我们发现 resp 会被 bodyEOFSingal 包裹一层,这个Close是 resp.body 的,也就是调用 resp.body.Close() 肯定能触发这段逻辑;详细看下 earlyCloseFn 这个函数用来干嘛的,从注释知道,这个函数就是在未读到EOF之前,也可通过这个channel信号,提早关闭conn,结合 readLoop 的阻塞逻辑,可以分析出,这里就是 resp.body.Close 关闭操作前的处理,通知 readLoop ,我读取完了,你继续执行吧

earlyCloseFn // optional alt Close func used if io.EOF not seen// readLoop 
2326:           case bodyEOF := <-waitForBodyRead:

并且我们还看到另外一个channel变量,eofc,注释中介绍,这个变量用于阻塞caller协程(也就是close函数),读取到EOF结束符时,进行阻塞,知道连接放回到空闲连接池中,这也是httpClient维护连接池的一些处理逻辑;

// eofc is used to block caller goroutines reading from Response.Body
// at EOF until this goroutines has (potentially) added the connection
// back to the idle pool.
eofc := make(chan struct{})
defer close(eofc) // unblock reader on errors// close 回调函数
earlyCloseFn: func() error {waitForBodyRead <- false<-eofc // will be closed by deferred call at the end of the functionreturn nil
}// readLopp 循环逻辑
select {case bodyEOF := <-waitForBodyRead:alive = alive &&bodyEOF &&!pc.sawEOF &&pc.wroteRequest() &&tryPutIdleConn(rc.treq)if bodyEOF {eofc <- struct{}{}}case <-rc.treq.ctx.Done():alive = falsepc.cancelRequest(context.Cause(rc.treq.ctx))case <-pc.closech:alive = false
}rc.treq.cancel(errRequestDone)

所以,resp.body.close 会给watiForBodyRead 发送 false 值,readLoop 接收到后alive 的判断就为false,从而跳出了 readLoop 的 for alive {} 循环,这个函数退出前会进行defer ,pc.Close 则最终会调用 net.conn.close 将 fd 打开的 fd 给关掉

defer func() {pc.close(closeErr)pc.t.removeIdleConn(pc)
}()// Close closes the connection.
func (c *conn) Close() error {if !c.ok() {return syscall.EINVAL}err := c.fd.Close()if err != nil {err = &OpError{Op: "close", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err}}return err
}

所以,当我们没有调用 resp.body.Close 时,readLoop 就会一直阻塞住,那为什么不会走到 rc.treq.ctx.Done或者closech呢,因为 rc.treq.cancel(errRequestDone) 在select 之后,另外一个调用的地方则是其他情况,不做讨论;而 closech 则是 conn关闭时才会走到;因此,我们得出结论:

如果不主动调用 resp.body.Close() 则 readLoop 会阻塞在 waitForBodyRead

总结:

  • 我们通过分析 readLoop 的各个退出条件,分析出 resp.body.Close 会回调 earlyCloseFn 函数,从而发送 false 给 waitForBodyRead channel,使得 alive 为 false 退出循环,结束协程

OK,这样我们解决一个协程泄漏的问题了,接下来看第二个 writeLoop

writeLoop 阻塞根因

其实很简单,看下代码,会发现writeLoop的退出条件基本都满足,要么写错误退出,要么等待pconn连接关闭,所以大量的pconn就存在大量的协程泄漏

func (pc *persistConn) writeLoop() {  defer close(pc.writeLoopDone)  for {  select {  case wr := <-pc.writech:  startBytesWritten := pc.nwrite  err := wr.req.Request.write(pc.bw, pc.isProxy, wr.req.extra, pc.waitForContinue(wr.continueCh))  if bre, ok := err.(requestBodyReadError); ok {  err = bre.error  // Errors reading from the user's  // Request.Body are high priority.            // Set it here before sending on the            // channels below or calling            // pc.close() which tears down            // connections and causes other            // errors.            wr.req.setError(err)  }  if err == nil {  err = pc.bw.Flush()  }  if err != nil {  if pc.nwrite == startBytesWritten {  err = nothingWrittenError{err}  }  }  pc.writeErrCh <- err // to the body reader, which might recycle us  wr.ch <- err         // to the roundTrip function  if err != nil {  pc.close(err)  return  }  case <-pc.closech:  return  }  }  
}

连接为什么无法复用?

前面我们有提到过,为什么我们不读取 body,连接就无法复用?我们看下 io.copy 干了啥,通过debug 调用下去,看下调用链路;

// Copy copies from src to dst until either EOF is reached
// on src or an error occurs. It returns the number of bytes
// copied and the first error encountered while copying, if any.
_, _ = io.Copy(ioutil.Discard, resp.Body)

发现就是把 resp.body 读取到某个地方;过程汇总调用到了 net.http.body 这个结构体的 readLocked

image.png

// Must hold b.mu.
func (b *body) readLocked(p []byte) (n int, err error) {if b.sawEOF {return 0, io.EOF}n, err = b.src.Read(p)if err == io.EOF {// ... 省略,这个是正常读取到结束符的情况}// If we can return an EOF here along with the read data, do// so. This is optional per the io.Reader contract, but doing// so helps the HTTP transport code recycle its connection// earlier (since it will see this EOF itself), even if the// client doesn't do future reads or Close.if err == nil && n > 0 {if lr, ok := b.src.(*io.LimitedReader); ok && lr.N == 0 {// 最终会执行到这一步,提前结束err = io.EOFb.sawEOF = true}}if b.sawEOF && b.onHitEOF != nil {b.onHitEOF()}return n, err
}

根据这个注释,我们可以知道这个 sawEOF 就是提早返回 EOF 以及读取的数据的标志,并且返回了err为io.EOF;按照这个错误,回溯到 bodyEOSignal 的 Read函数,可以发现如果错误了,会调用 condfn,这个fn就是之前出现过的,readLoop中的resp.body的初始化的fn

func (es *bodyEOFSignal) Read(p []byte) (n int, err error) {es.mu.Lock()closed, rerr := es.closed, es.rerres.mu.Unlock()if closed {return 0, errReadOnClosedResBody}if rerr != nil {return 0, rerr}n, err = es.body.Read(p)if err != nil {es.mu.Lock()defer es.mu.Unlock()if es.rerr == nil {es.rerr = err}err = es.condfn(err)}return
}// caller must hold es.mu.
func (es *bodyEOFSignal) condfn(err error) error {if es.fn == nil {return err}err = es.fn(err)es.fn = nilreturn err
}

这个回调函数,还是给 waitForBodyRead 赋值,但这次是 true,因为 err 等于 io.EOF

fn: func(err error) error {isEOF := err == io.EOFwaitForBodyRead <- isEOFif isEOF {<-eofc // see comment above eofc declaration} else if err != nil {if cerr := pc.canceled(); cerr != nil {return cerr}}return err
},

ok 又回到 readLoop 的select,可以发现通过 io.Copy 读取完,bodyEOF 的值为 true,这样alive 就会继续往下执行,直到调用 tryPutIdleConn

// readLopp 循环逻辑
select {case bodyEOF := <-waitForBodyRead:alive = alive &&bodyEOF &&!pc.sawEOF &&pc.wroteRequest() &&tryPutIdleConn(rc.treq)if bodyEOF {eofc <- struct{}{}}case <-rc.treq.ctx.Done():alive = falsepc.cancelRequest(context.Cause(rc.treq.ctx))case <-pc.closech:alive = false
}

tryPutIdleConn 就是尝试把连接 persistConn 放回 Idle 队列中,让下一个 http 请求过来能直接拿到现有的persistConn,具体是怎么做的,这里不细讲了;

总结:

  • io.Copy 将 resp.body 读取出来,然后它发现能提前返回结果,则进行返回,或者读取完发现确实是EOF,结束了,也返回 err 为 io.EOF
  • 对于返回的io.EOF,bodyEOFSignal 这个结构体识别到了则开始回调fn(正常结束的回调函数),而前面介绍过 earlyClosefn,则是提前结束的回调函数(通过resp.body.close调用触发的)
  • io.EOF 的回调函数,则是通过 waitForBodyRead 这个channel 与 readLoop 交互,让其触发 tryPutIdleConn 操作,最终将 persistConn 放回 空闲连接队列中,达到下一个http请求能直接复用的效果

这就是为什么我们不调用 io.Copy 或者 io.ReadAll 将 resp.body 读取完时,连接无法被复用的根因,因为根因就没触发 tryPutIdleConn 呀~

随带提一下,resp.body.Close 也会调用 io.Copy 消费 body后,才去关闭连接

_, err = io.Copy(io.Discard, bodyLocked{b})

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/68011.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

关于husky8.0 与 4.0的配置

husky的场景使用很多&#xff0c;一般大多场景是在配置git commit 命令拦截hook, 校验 commit-msg 格式规范。以下环境默认&#xff1a;git > 2.27.0, node >14 1、安装huskey8.0.1 npm install --save-dev husky8.0.1 2、初始化配置文件 在package.json scripts 属性…

CV(9)--迁移学习

前言 仅记录学习过程&#xff0c;有问题欢迎讨论 fine-tuning 在实践中&#xff0c;由于数据集不够大&#xff0c;很少有人从头开始训练网络。常见的做法是使用预训练的网络 &#xff08;例如在ImageNet上训练的分类1000类的网络&#xff09;来重新fine-tuning&#xff08;也…

LS1046+XILINX XDMA PCIE调通

欢迎点赞收藏&#xff0c;欢迎私下讨论技术&#xff0c;分享技术 硬件平台 &#xff1a;NXP LS1046 XILINX FPGA 软件平台&#xff1a;LINUX 4.19.68 buildroot LS1046 PEX3 接 XILINX FPGA&#xff0c;linux使用designware的PCI主控制器。下载XILINX DMA驱动&#xff0c;解…

C语言gdb调试

目录 1.gdb介绍 2.设置断点 2.1.测试代码 2.2.设置函数断点 2.3.设置文件行号断点 2.4.设置条件断点 2.5.多线程调试 3.删除断点 3.1.删除指定断点 3.2.删除全部断点 4.查看变量信息 4.1.p命令 4.2.display命令 4.3.watch命令 5.coredump日志 6.总结 1.gdb介绍…

[程序设计]—代理模式

[程序设计]—代理模式&#x1f473; 本文章记录学习于——52.面向切面&#xff1a;AOP-场景模拟_哔哩哔哩_bilibili 最近闲来无事&#xff0c;在学习Spring的源码&#xff1a; 后面慢慢更新源码系列blog&#xff0c;希望多多关注&#x1f64f;&#x1f64f; 目前已经总结的b…

我的128天创作之路:回顾与展望

大家好呀&#xff01;今天来和你们分享一下我的创作历程&#x1f601;。 一、机缘 最开始创作呢&#xff0c;是因为在学习 C 的 STL 时&#xff0c;像 string、list、vector 这些模板可把我折腾得够呛&#xff0c;但也让我学到了超多东西&#xff01;我就想&#xff0c;要是把我…

RAID储存技术

RAID独立磁盘冗余技术是一种把2个或者多个HDD或SSD合并为一个协调的存储单元或列阵&#xff0c;从而预防数据丢失的技术&#xff0c;其最早由加州大学伯克利分校的计算机科学家David Patterson、Garth Gibson和Randy Katz在1987年提出。他们的研究论文“关于RAID的论证”提出了…

Openstack持久存储-Swift,Cinder,Manila三者之间的区别

总结不易&#xff0c;给个三连吧&#xff01;&#xff01;&#xff01; 补充&#xff1a; 文件共享存储服务Manila 在OpenStack生态系统中&#xff0c;Cinder和Manila分别提供了两种不同类型的存储服务&#xff0c;类似于传统的SAN&#xff08;存储区域网络&#xff09;和NAS&…

软件测试预备知识④—NTFS权限管理、磁盘配额与文件共享

在软件测试的实际环境搭建与管理过程中&#xff0c;了解和掌握NTFS权限管理、磁盘配额以及文件共享等知识至关重要。这些功能不仅影响系统的安全性和稳定性&#xff0c;还对测试数据的存储、访问以及多用户协作测试有着深远的影响。 一、NTFS权限管理 1.1 NTFS简介 NTFS&am…

PyTorch深度学习CNN神经网络ResNet、DenseNet在CIFAR图像数据集分类应用与分析

全文链接&#xff1a;https://tecdat.cn/?p38782 在当今深度学习领域&#xff0c;卷积神经网络&#xff08;CNN&#xff09;架构不断发展与创新&#xff0c;诸多先进的架构被提出并广泛应用。像GoogleNet&#xff08;ILSVRC 2014获胜者&#xff09;、ResNet&#xff08;ILSVRC…

word论文排版常见问题汇总

word论文排版常见问题汇总 常用快捷键&#xff1a; Alt F9 正常模式与域代码模式切换 Ctrl F9 插入域代码 F9 刷新域代码显示&#xff0c;要注意选定后刷新才会有效果 word中在当前列表的基础上修改列表 在使用word时&#xff0c;我们会定义一个列表&#xff0c;并将其链接…

【Rust】数据类型

目录 思维导图 1. 数据类型概述 1.1 标量类型 1.1.1 整数类型 1.1.2 浮点数类型 1.1.3 布尔类型 1.1.4 字符类型 1.2 复合类型 1.2.1 元组类型 1.2.2 数组类型 2. 类型注解与类型推断 3. 整数溢出处理 4. 数字运算 5. 示例 思维导图 1. 数据类型概述 Rust是一种静…

Proteus-8086调试汇编格式的一点心得

这阵子开始做汇编的微机实验&#xff08;微机原理与接口技术题解及实验指导&#xff0c;吴宁版本13章&#xff09;&#xff0c;中间出了挺多问题&#xff0c;解决后记录下。 先上电路图 用子电路来仿真发现仿真的时候子电路这块根本没有高低电平输出&#xff0c;只好把子电路拿…

跨界融合:人工智能与区块链如何重新定义数据安全?

引言&#xff1a;数据安全的挑战与现状 在信息化驱动的数字化时代&#xff0c;数据已成为企业和个人最重要的资产之一。然而&#xff0c;随着网络技术的逐步优化和数据量的爆发式增长&#xff0c;数据安全问题也愈变突出。 数据安全现状&#xff1a;– 数据泄露驱动相关事件驱…

机器人碳钢去毛刺,用大扭去毛刺主轴可轻松去除

在碳钢精密加工的最后阶段&#xff0c;去除毛刺是确保产品质量的关键步骤。面对碳钢这种硬度较高的材料&#xff0c;采用大扭矩的SycoTec去毛刺主轴&#xff0c;成为了行业内的高效解决方案。SycoTec作为精密加工领域的领军品牌&#xff0c;其生产的高速电主轴以其卓越的性能&a…

大疆上云API连接遥控器和无人机

文章目录 1、部署大疆上云API关于如何连接我们自己部署的上云API2、开启无人机和遥控器并连接自己部署的上云API如果遥控器和无人机没有对频的情况下即只有遥控器没有无人机的情况下如果遥控器和无人机已经对频好了的情况下 4、订阅无人机或遥控器的主题信息4.1、订阅无人机实时…

[OPEN SQL] 限定选择行数

本次操作使用的数据库表为SCUSTOM&#xff0c;其字段内容如下所示 航班用户(SCUSTOM) 该数据库表中的部分值如下所示 指定查询多少行数据&#xff0c;我们可以使用语法UP TO n ROWS来实现对数据前n项的查询 语法格式 SELECT * FROM <dbtab> UP TO n ROWS 参数说明 db…

机器视觉3-线性分类器

机器视觉3-线性分类器 前言一、整体流程二、其他相关内容 图像的表示图像类型黑白图像灰度图像彩色图像 图像表示为向量一、基本概念二、表示方法三、优点四、局限性五、应用场景 线性分类器一、神经网络的层级结构形成非线性模型二、支撑向量机的高维映射形成非线性模型 线性分…

解决“无法定位程序输入点 av_buffer_create 于动态链接库 XXX\Obsidian.exe 上”问题

解决“无法定位程序输入点 av_buffer_create 于动态链接库 XXX\Obsidian.exe 上”问题 问题描述 本人在使用zotero中的zotero one&#xff08;青柠学术插件&#xff09;的时候&#xff0c;使用插件跳转obsidian中的对应笔记&#xff0c;出现上图情况。&#xff08;错误中提到的…

【数据结构高阶】B-树

目录 一、常见的搜索结构 二、B树 2.1 B树的概念 2.2 B树插入数据的分析 2.3 B树的性能分析 2.4 模拟实现B树 2.4.1 B树节点的定义 2.4.2 B树数据的查找 2.4.3 B树节点的数据插入 2.4.4 B树的遍历 2.4.5 模拟实现B树实现的完整代码 三、B树 3.1 B树的概念 3.2 B树…