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

news2025/1/12 14:37:14

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 29
      flat  flat%   sum%        cum   cum%
      5474 99.91% 99.91%       5474 99.91%  runtime.gopark
         0     0% 99.91%         77  1.41%  bufio.(*Reader).Peek
         0     0% 99.91%         77  1.41%  bufio.(*Reader).fill
         0     0% 99.91%         80  1.46%  internal/poll.(*FD).Read
         0     0% 99.91%         82  1.50%  internal/poll.(*pollDesc).wait
         0     0% 99.91%         82  1.50%  internal/poll.(*pollDesc).waitRead (inline)
         0     0% 99.91%         82  1.50%  internal/poll.runtime_pollWait
         0     0% 99.91%         79  1.44%  main.(*AlertingRule).Exec
         0     0% 99.91%        170  3.10%  main.(*Group).start
         0     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.gopark
             runtime.selectgo
             net/http.(*persistConn).writeLoop
-----------+-------------------------------------------------------
      2060   runtime.gopark
             runtime.selectgo
             net/http.(*persistConn).readLoop

(pprof) list writeLoop
Total: 4486
ROUTINE ======================== net/http.(*persistConn).writeLoop in /usr/local/go/src/net/http/transport.go
         0       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 function
        return nil

    },
    fn: func(err error) error {
        isEOF := err == io.EOF
        waitForBodyRead <- isEOF
        if 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 = true
	if 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 function
    return 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 = false
    	pc.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.EOF
			b.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.rerr
	es.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 = nil
	return err
}

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

fn: func(err error) error {
    isEOF := err == io.EOF
    waitForBodyRead <- isEOF
    if 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 = false
    	pc.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.coloradmin.cn/o/2275525.html

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

相关文章

TrustRAG:增强RAG系统鲁棒性与可信度的创新框架

在人工智能飞速发展的今天&#xff0c;大语言模型&#xff08;LLMs&#xff09;凭借其强大的语言处理能力在诸多领域大放异彩。检索增强生成&#xff08;RAG&#xff09;系统&#xff08;面向企业RAG&#xff08;Retrieval Augmented Generation&#xff09;系统的多维检索框架…

业务链指标,用户行为模式识别,埋点系统

个人博客&#xff1a;无奈何杨&#xff08;wnhyang&#xff09; 个人语雀&#xff1a;wnhyang 共享语雀&#xff1a;在线知识共享 Github&#xff1a;wnhyang - Overview 此前&#xff0c;我们已经成功搭建起最为基础的聚类指标体系&#xff0c;涵盖计数、求和、最大值、最小…

Git撤销指定commit并更新远端仓库

Git撤销指定commit并更新远端仓库 一、撤销指定commit 1.首先执行git log 命令&#xff0c;查看git历史提交以及commit信息&#xff1a; 由于需要脱敏&#xff0c;所以截图可能看得马赛克比较多&#xff0c;需要关注的就是上面的commit后跟的id&#xff0c;以及HEAD当前指定…

基于DFT与IIR-FIR滤波器的音频分析与噪声处理

基于DFT与IIR-FIR滤波器的音频分析与噪声处理 【完整源码文档报告】 【需要可随时联系博主&#xff0c;常在线能秒回!】 系统功能与实现介绍 功能与实现 音频处理系统界面搭建&#xff1a;利用MATLAB的GUI工具&#xff0c;构建了音频分析界面&#xff0c;包括文件导入、录…

基于单片机的无线气象仪系统设计(论文+源码)

1系统方案设计 如图2.1所示为无线气象仪系统设计框架。系统设计采用STM32单片机作为主控制器&#xff0c;结合DHT11温湿度传感器、光敏传感器、BMP180气压传感器、PR-3000-FS-N01风速传感器实现气象环境的温度、湿度、光照、气压、风速等环境数据的检测&#xff0c;并通过OLED1…

MySQL库表的操作

目录 一、库的操作 1.1库的创建 1.2字符集和校验规则 1.2.1 查看系统默认字符集以及校验规则 1.2.2 查看数据库支持的字符集 1.2.3 查看数据库支持的字符集校验规则 1.2.4 校验规则对数据库的影响 1.3操纵数据库 1.3.1显示库 1.3.2显示创建语句 1.3.3修改数据库 1.3…

硬件设计-齐纳管

目录 摘要 详情 齐纳管的工作电流、 摘要 齐纳管&#xff08;Zener Diode&#xff09;是一种特殊的二极管&#xff0c;它能够在特定的反向电压下保持电流稳定。正常情况下&#xff0c;二极管只允许正向电流通过&#xff0c;而阻止反向电流流过。而齐纳管在一定的反向电压下可…

Airflow:TimeSensor感知时间条件

在数据管道工作流中&#xff0c;任务可能需要在特定的时间执行&#xff0c;或者在继续之前等待一定的时间。为了满足这些需求&#xff0c;Apache Airflow提供了TimeSensor&#xff0c;这是一种内置Sensor&#xff0c;可以监控当前时间&#xff0c;并在达到指定时间时触发后续任…

libusb学习——简单介绍

文章目录 libusb 简介libusb 编译libusb 源码目录介绍核心代码文件平台支持例子 API使用libusb初始化和去初始化libusb设备处理和枚举libusb 杂项libusb USB描述符libusb 设备热插拔事件通知libusb 异步设备I/Olibusb 同步设备I/Olibusb 轮询与定时 libusb 涉及技术参考 libusb…

HTML5 网站模板

HTML5 网站模板 参考 HTML5 Website Templates

阿里云ios镜像源

阿里云镜像源&#xff1a;阿里巴巴开源镜像站-OPSX镜像站-阿里云开发者社区 下载centos7

【大数据】Apache Superset:可视化开源架构

Apache Superset是什么 Apache Superset 是一个开源的现代化数据可视化和数据探索平台&#xff0c;主要用于帮助用户以交互式的方式分析和展示数据。有不少丰富的可视化组件&#xff0c;可以将数据从多种数据源&#xff08;如 SQL 数据库、数据仓库、NoSQL 数据库等&#xff0…

【2024年华为OD机试】 (A卷,100分)- 端口合并(Java JS PythonC/C++)

一、问题描述 题目描述 有 M 个端口组 (1 < M < 10)&#xff0c; 每个端口组是长度为 N 的整数数组 (1 < N < 100)&#xff0c; 如果端口组间存在 2 个及以上不同端口相同&#xff0c;则认为这 2 个端口组互相关联&#xff0c;可以合并。 输入描述 第一行输入端…

【灵码助力安全3】——利用通义灵码辅助智能合约漏洞检测的尝试

前言 随着区块链技术的快速发展&#xff0c;智能合约作为去中心化应用&#xff08;DApps&#xff09;的核心组件&#xff0c;其重要性日益凸显。然而&#xff0c;智能合约的安全问题一直是制约区块链技术广泛应用的关键因素之一。由于智能合约代码一旦部署就难以更改&#xf…

AOP实现操作日志记录

文章目录 1.common-log4j2-starter1.目录2.pom.xml 引入依赖3.LogAspect.java4.Log4j2AutoConfiguration.java Log4j2自动配置类条件注入切面类 2.common-log4j2-starter-demo 测试1.目录2.application.yml 启用日志切面3.TraceController.java4.结果 1.common-log4j2-starter …

分布式ID—雪花算法

背景 现在的服务基本是分布式、微服务形式的&#xff0c;而且大数据量也导致分库分表的产生&#xff0c;对于水平分表就需要保证表中 id 的全局唯一性。 对于 MySQL 而言&#xff0c;一个表中的主键 id 一般使用自增的方式&#xff0c;但是如果进行水平分表之后&#xff0c;多…

JavaEE之定时器及自我实现

在生活当中&#xff0c;有很多事情&#xff0c;我们不是立马就去做&#xff0c;而是在规定了时间之后&#xff0c;在到该时间时&#xff0c;再去执行&#xff0c;比如&#xff1a;闹钟、定时关机等等&#xff0c;在程序的世界中&#xff0c;有些代码也不是立刻执行&#xff0c;…

深入Android架构(从线程到AIDL)_23 活用IBinder接口于近程通信01

1、 在同一进程里活用IBinder接口 议题 1. myActivity对象是谁创建的呢? 2. myService对象是谁创建的呢? 3. 当myService类里有个f1()函数&#xff0c;如何去调用它呢? 4. 必须先取得myService对象的指针&#xff0c;才能调用f1()函数去存取对象的属性(Attribute)值。 …

vue3后台系统动态路由实现

动态路由的流程&#xff1a;用户登录之后拿到用户信息和token&#xff0c;再去请求后端给的动态路由表&#xff0c;前端处理路由格式为vue路由格式。 1&#xff09;拿到用户信息里面的角色之后再去请求路由表&#xff0c;返回的路由为tree格式 后端返回路由如下&#xff1a; …

如何开启苹果手机(IOS)系统的开发者模式?

如何开启开发者模式&#xff1f; 一、打开设置二、隐私与安全性三、找到开发者模式四、开启开发者模式------------------------------------------------------------如果发现没有开发者模式的选项一、电脑下载爱思助手二、连接手机三、工具箱——虚拟定位——打开虚拟定位——…