当前位置: 首页 > news >正文

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})

相关文章:

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

Reference 关于 Golang 中 http.Response.Body 未读取导致连接复用问题的一点研究https://manishrjain.com/must-close-golang-http-responsehttps://www.reddit.com/r/golang/comments/13fphyz/til_go_response_body_must_be_closed_even_if_you/?rdt35002https://medium.co…...

Elasticsearch:在 HNSW 中提前终止以实现更快的近似 KNN 搜索

作者&#xff1a;来自 Elastic Tommaso Teofili 了解如何使用智能提前终止策略让 HNSW 加快 KNN 搜索速度。 在高维空间中高效地找到最近邻的挑战是向量搜索中最重要的挑战之一&#xff0c;特别是当数据集规模增长时。正如我们之前的博客文章中所讨论的&#xff0c;当数据集规模…...

嵌入式c语言的内存管理

目录 一、内存布局概述 二、栈&#xff08;Stack&#xff09; 2.1. 定义与用途 2.2. 内存分配与释放 2.3. 增长方向与大小限制 三、堆&#xff08;Heap&#xff09; 3.1. 定义与用途 3.2. 内存分配与释放 3.3. 增长方向与潜在问题 四、全局/静态存储区 4.1. 定义与用…...

uniapp-vue3 实现, 一款带有丝滑动画效果的单选框组件,支持微信小程序、H5等多端

采用 uniapp-vue3 实现, 是一款带有丝滑动画效果的单选框组件&#xff0c;提供点状、条状的动画过渡效果&#xff0c;支持多项自定义配置&#xff0c;适配 web、H5、微信小程序&#xff08;其他平台小程序未测试过&#xff0c;可自行尝试&#xff09; 可到插件市场下载尝试&…...

【Linux】shell脚本编程

目录 概念&#xff1a; shell脚本的本质&#xff1a; shell脚本编程&#xff1a; shell变量&#xff1a; 变量的定义格式&#xff1a; 变量的分类 自定义变量&#xff1a; 环境变量&#xff1a; 命令变量与命令行参数&#xff1a; 预定义变量&#xff1a; shell中的…...

ingress-nginx-controller安装

ingress-nginx-controller安装 ingress-nginx-controller是配置ingress发布的基础。以下主要采用Helm安装。地址&#xff1a; GitHub - kubernetes/ingress-nginx: Ingress NGINX Controller for Kubernetes 1 Helm安装 安装不难&#xff0c;需要找到合适的压缩包就行。我自…...

机器学习基础-机器学习的常用学习方法

目录 半监督学习的概念 规则学习的概念 基本概念 机器学习里的规则 逻辑规则 规则集 充分性与必要性 冲突消解 命题逻辑 → 命题规则 序贯覆盖 单条规则学习 剪枝优化 强化学习的概念 1. 强化学习对应了四元组 2. 强化学习的目标 强化学习常用马尔可夫决策过程…...

如何在Windows 11 WSL2 Ubuntu 环境下安装和配置perf性能分析工具?

在Windows 11 WSL2 Ubuntu 环境下完整安装和配置perf性能分析工具 一、背景二、准备工作三、获取并编译Linux内核源码四、安装和配置perf五、测试perf六、总结 一、背景 由于WSL2使用的是微软定制的内核&#xff0c;并非标准的Ubuntu内核&#xff0c;因此直接使用apt安装linux…...

人工智能知识分享第十天-机器学习_聚类算法

聚类算法 1 聚类算法简介 1.1 聚类算法介绍 一种典型的无监督学习算法&#xff0c;主要用于将相似的样本自动归到一个类别中。 目的是将数据集中的对象分成多个簇&#xff08;Cluster&#xff09;&#xff0c;使得同一簇内的对象相似度较高&#xff0c;而不同簇之间的对象相…...

使用wav2vec 2.0进行音位分类任务的研究总结

使用wav2vec 2.0进行音位分类任务的研究总结 原文名称&#xff1a; Using wav2vec 2.0 for phonetic classification tasks: methodological aspects 研究背景 自监督学习在语音中的应用 自监督学习在自动语音识别任务中表现出色&#xff0c;例如说话人识别和验证。变换器模型…...

【Leetcode 热题 100】33. 搜索旋转排序数组

问题背景 整数数组 n u m s nums nums 按升序排列&#xff0c;数组中的值 互不相同 。 在传递给函数之前&#xff0c; n u m s nums nums 在预先未知的某个下标 k ( 0 ≤ k < n u m s . l e n g t h ) k(0 \le k \lt nums.length) k(0≤k<nums.length) 上进行了 旋转&…...

【VScode】设置代理,通过代理连接服务器

文章目录 VScode编辑器设置代理1.图形化界面1.1 进入proxy设置界面1.2 配置代理服务器 2.配置文件&#xff08;推荐&#xff09;2.1 打开setting.json 文件2.2 配置代理 VScode编辑器设置代理 根据情况安装nmap 1.图形化界面 1.1 进入proxy设置界面 或者使用快捷键ctrl , 。…...

每日一题-两个链表的第一个公共结点

文章目录 两个链表的第一个公共结点问题描述示例说明示例 1示例 2 方法及实现方法描述代码实现 复杂度分析示例运行过程示例 1示例 2 总结备注 两个链表的第一个公共结点 问题描述 给定两个无环的单向链表&#xff0c;找到它们的第一个公共节点。如果没有公共节点&#xff0c…...

Linux存储管理之核心秘密(The Core Secret of Linux Storage Management)

Linux存储管理之核心秘密 如果你来自Windows环境&#xff0c;那么Linux处理和管理存储设备的方式对你而言可能显得格外不同。我们知道&#xff0c;Linux的文件系统并不采用Windows那样的物理驱动器表示方式&#xff08;如C:、D:或E:&#xff09;&#xff0c;而是构建了一个以&…...

js单例模式

保证一个类只有一个实例,并提供一个访问它的全局访问点 实现 静态方法实现 class SingleTon{//全局的访问点static getInstance(){// 保证一个类只有一个实例if(!this.instance){this.instancenew SingleTon()}return this.instance}}let aSingleTon.getInstance()let bSing…...

搭建一个本地轻量级且好用的学习TypeScript语言的环境

需求说明 虽然 TypeScript 的在线 Playground 很方便 https://www.tslang.com.cn/play/&#xff0c;但毕竟是在浏览器中使用&#xff0c;没有本地的 IDE 那么顺手。所以我想搭建一个本地类似 Playground 的环境&#xff0c;这样在学习 TypeScript 的过程中&#xff0c;可以更方…...

大模型(LLM)面试全解:主流架构、训练目标、涌现能力全面解析

系列文章目录 大模型&#xff08;LLMs&#xff09;基础面 01-大模型&#xff08;LLM&#xff09;面试全解&#xff1a;主流架构、训练目标、涌现能力全面解析 大模型&#xff08;LLMs&#xff09;进阶面 文章目录 系列文章目录大模型&#xff08;LLMs&#xff09;基础面一、目…...

入门嵌入式(七)——PWM

PWM 脉冲宽度调制PWM&#xff08;Pulse-Width Modulation&#xff09; 一组方波 周期 1ms 频率 1s / 周期 占空比 高电平/周期 数字信号&#xff1a;0/1高低电平 串口 IIC PWM都属于数字信号 模拟信号&#xff1a;电压值&#xff0c; 会受到电磁影响 ADC 模数转换 用于读取…...

Flutter 实现 列表滑动过程控件停靠效果 学习

实现一个 Flutter 应用程序&#xff0c;使用 Sliver 系列组件来创建具有滚动效果的复杂布局。使用 NestedScrollView 和 SliverPersistentHeader 来实现固定和动态的头部效果&#xff0c;以及一个可滚动的列表。 前置知识点学习 SingleTickerProviderStateMixin SingleTicker…...

STM32+WIFI获取网络时间+8位数码管显示+0.96OLED显

资料下载地址&#xff1a;STM32WIFI获取网络时间8位数码管显示0.96OLED 1、项目介绍 主控芯片STM32C8T6 接线&#xff1a;串口1&#xff1a;PA9 PA10 OELD &#xff1a;PB6 PB7 数码管使用&#xff1a;MAX7219 8位数码管 Max7219_pinCLK PAout(5) Max7219_pinC…...

Kivy App开发之UX控件Slider滑块

在app中可能会调节如音量,亮度等,可以使用Slider来实现,该控件调用方便,兼容性好,滑动平稳。在一些参数设置中,也可以用来调整数值。 支持水平和垂直方向,可以设置默认值,最小及最大值。 使用方法,需用引入Slider类,通过Slider类生成一个滑块并设置相关的样式后,再…...

STM32学习(十)

I2C模块内部结构 I2C&#xff08;Inter-Integrated Circuit&#xff09;模块是一种由Philips公司开发的二线式串行总线协议&#xff0c;用于短距离通信&#xff0c;允许多个设备共享相同的总线‌。 ‌硬件连接简单‌&#xff1a;I2C通信仅需要两条总线&#xff0c;即SCL&…...

何为“正则表达式”!

详细解释&#xff1a; ^&#xff1a;在JSON的正则表达式中&#xff0c;^表示匹配输入字符串的开始位置。这意味着正则表达式将从字符串的开头开始进行匹配&#xff0c;确保整个字符串符合后续的模式要求。例如&#xff0c;对于字符串"3.14"&#xff0c;正则表达式会…...

创建Java项目,并添加MyBatis包和驱动包

一 : Mybatis和jsp使用上,只有Dao层有区别 Mybatis 使用方法: 测试类的7步骤 1.读取核心配置文件 2.构建sql会话工厂 3.开启sql会话 4.获取mapper接口 5.调用相对应的增删改查方法 6.打印 7.关闭回话 /*** 用户列表* throws IOException*/Testpublic void roleList() throws IO…...

目标检测文献阅读-DETR:使用Transformer进行端到端目标检测

目录 摘要 Abstract 1 引言 2 DETR结构 2.1 Backbone 2.2 Encoder 2.3 Decoder 2.4 FFN 3 目标检测集合预测损失 3.1 二分图匹配损失 3.2 损失函数 总结 摘要 本周阅读的论文题目是《End-to-End Object Detection with Transformers》(使用Transformer进行端到端目…...

C#里对已经存在的文件进行压缩生成ZIP文件

先要对目录下所有文件获取到: private List<string> GetXDFiles(string dirPath){//获取目录中的所有文件string suffix = "*.txt"; // 要查找的文件后缀var files = Directory.GetFiles(dirPath, suffix, SearchOption.TopDirectoryOnly).ToList();return fi…...

单片机软件定时器V4.0

单片机软件定时器V4.0 用于单片机定时执行任务等&#xff0c;比如LED GPIO等定时控制&#xff0c;内置前后台工作模式 头文件有使用例子 #ifndef __SORFTIME_APP_H #define __SORFTIME_APP_H#ifdef __cplusplus extern "C" { #endif#include <stdint.h>// #…...

【嵌入式硬件】嵌入式显示屏接口

数字显示串行接口&#xff08;Digital Display Serial Interface&#xff09; SPI 不过多赘述。 I2C-bus interface 不过多赘述 MIPI DSI MIPI (Mobile Industry Processor Interface) Alliance, DSI (Display Serial Interface) 一般用于移动设备&#xff0c;下面是接口…...

vuedraggable 选项介绍

vuedraggable 是基于 SortableJS 的 Vue 组件&#xff0c;提供了丰富的选项来定制拖拽行为。以下是 vuedraggable 常用的选项和它们的详细说明&#xff1a; 常用选项介绍 group 配置拖拽分组。多个列表可以共享同一个分组&#xff0c;允许它们之间的项目互相拖拽。 group: { na…...

OpenAI CEO 奥特曼发长文《反思》

OpenAI CEO 奥特曼发长文《反思》 --- 引言&#xff1a;从 ChatGPT 到 AGI 的探索 ChatGPT 诞生仅一个多月&#xff0c;如今我们已经过渡到可以进行复杂推理的下一代模型。新年让人们陷入反思&#xff0c;我想分享一些个人想法&#xff0c;谈谈它迄今为止的发展&#xff0c;…...

Appium(一)--- 环境搭建

一、Android自动化环境搭建 1、JDK 必须1.8及以上(1) 安装&#xff1a;默认安装(2) 环境变量配置新建JAVA_HOME:安装路径新建CLASSPath%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar在path中增加&#xff1a;%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin&#xff1b;(3) 验证…...

如何查看服务器上的MySQL/Redis等系统服务状态和列表

如果呢你知道系统服务名称&#xff0c;要看状态很简单&#xff1a; systemctl status server-name 比如 systemctl status nginxsystemctl status redis # 等 这是一个nginx的示例&#xff1a; 那问题是 当你不知道服务名称时该怎么办。举个例子&#xff0c;比如mysql在启动…...

多模态论文笔记——U-ViT

大家好&#xff0c;这里是好评笔记&#xff0c;公主号&#xff1a;Goodnote&#xff0c;专栏文章私信限时Free。本文详细介绍U-ViT的模型架构和实验细节&#xff0c;虽然没有后续的DiT在AIGC领域火爆&#xff0c;但为后来的研究奠定了基础&#xff0c;但其开创性的探索值得学习…...

08-1_队列的理论讲解

08-1_队列的理论讲解 队列概念理解 队列就是一个传送带,就是一个流水线 工人放入的数据, 放在传送带上,送给消费者, 消费者第一次拿到的数据,就是工人第一次放上的数据(类比队列,先进先出) freertos队列应用思考 队列对于消费者(取数据): (1)当队列没有数据, 消费者需要进入…...

HTML基础入门——简单网页页面

目录 一&#xff0c;网上转账电子账单 ​编辑 1&#xff0c;所利用到的标签 2&#xff0c;代码编写 3&#xff0c;运行结果 二&#xff0c;李白诗词 1&#xff0c;所用到的标签 2&#xff0c;照片的编辑 3&#xff0c;代码编写 4&#xff0c;运行结果 一&#xff0c;网…...

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

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

Python入门教程 —— 多任务

1.线程 1.1.线程安全问题 线程访问全局变量 import threading g_num = 0 def test(n):global g_numfor x in range(n):g_num += xg_num -= xprint(g_num)if __name__ == __main__:t1 = threading.Thread(target=test, args=(10,))t2 = threading.Thread(target=test, args=(…...

【笔记】算法记录

1、求一个数的素因子&#xff08;试除法&#xff09; // 获取一个数的所有素因子 set<int> getPrimeFactors(int num) {set<int> primeFactors;for (int i 2; i * i < num; i) {while (num % i 0) {primeFactors.insert(i);num / i;}}if (num > 1) {prime…...

2024年1月4日蜻蜓hr人才招聘系统v1.1.7更新-正式版发布-客户端源代码开源发布供学习-本产品完成上线正式版-修复多个bug-优雅草果果|小无

2024年1月4日蜻蜓hr人才招聘系统v1.1.7更新-正式版发布-客户端源代码开源发布供学习-本产品完成上线正式版-修复多个bug-优雅草果果|小无 前端代码开源库 关于开源说明&#xff1a;企业服务-招聘信息管理系统-前端uniapp-系统前端开放源代码仅供学习-优雅草科技-目前优雅草科…...

【网络】计算机网络的分类 局域网 (LAN) 广域网 (WAN) 城域网 (MAN)个域网(PAN)

局域网是通过路由器接入广域网的 分布范围 局域网Local Area Network&#xff1a;小范围覆盖&#xff0c;速度高&#xff0c;延迟低(办公室&#xff0c;家庭&#xff0c;校园&#xff0c;网络) 广域网Wide Area Network 大范围覆盖&#xff0c;速度相对低&#xff0c;延迟高…...

DeepSeek:性能强劲的开源模型

deepseek 全新系列模型 DeepSeek-V3 首个版本上线并同步开源。登录官网 chat.deepseek.com 即可与最新版 V3 模型对话。 性能对齐海外领军闭源模型​ DeepSeek-V3 为自研 MoE 模型&#xff0c;671B 参数&#xff0c;激活 37B&#xff0c;在 14.8T token 上进行了预训练。 论…...

MySql 通过 LOAD DATA INFILE 导入大量数据

背景&#xff1a;要在本地Mysql导入几十万-百万条数据非常耗时&#xff0c;从网上找到通过load data infile 方式可以快速导入大量数据&#xff0c;就动手尝试了。结果就是不太适合复杂的格式数据导入&#xff0c;比如字段多&#xff0c;数据格式多等&#xff0c;但对于简单的表…...

Linux系统中解决端口占用问题

在日常的 Linux 系统管理和开发过程中&#xff0c;端口占用是一个常见且令人头疼的问题。无论是部署新服务、调试应用程序&#xff0c;还是进行系统维护&#xff0c;遇到端口被占用都可能导致服务无法正常启动或运行。本文将详细介绍在 Linux 系统中如何识别和解决端口占用问题…...

两种方式实现Kepware与PLC之间的心跳检测

两种方式实现Kepware与PLC之间的心跳检测 实现Kepware与PLC之间的心跳检测1.OPCUA 外挂程序2.Kepware Advanced Tag 实现Kepware与PLC之间的心跳检测 1.OPCUA 外挂程序 这是通过上位程序来触发心跳的一种机制&#xff0c;在C#中&#xff0c;可以利用OPC UAOPCAutodll的方式…...

使用DockerCompose部署服务

由于格式或图片解析问题&#xff0c;为了更好的阅读体验&#xff0c;可前往 阅读原文 以前我们总是用命令管理每个容器的启动、停止等等&#xff0c;若有多个容器时可能还存在启动优先级的问题&#xff0c;那就要等到指定的容器启动后再去启动另一个容器&#xff0c;对于整体的…...

第P5周-Pytorch实现运动鞋品牌识别

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 具体实现 &#xff08;一&#xff09;环境 语言环境&#xff1a;Python 3.10 编 译 器: PyCharm 框 架: Pytorch &#xff08;二&#xff09;具体步骤 时间…...

react-quill 富文本组件编写和应用

index.tsx文件 import React, { useRef, useState } from react; import { Modal, Button } from antd; import RichEditor from ./RichEditor;const AnchorTouchHistory: React.FC () > {const editorRef useRef<any>(null);const [isModalVisible, setIsModalVis…...

因泰立科技激光车检器,高速公路的精准流量统计专家

高速公路自由流门架激光车检器&#xff0c;专为解决高速公路交通管理中的各种挑战而设计。该产品采用先进的激光测量技术&#xff0c;能够对动态通过的车辆进行高速动态扫描&#xff0c;通过测量物体表面点的反射距离&#xff0c;迅速换算成三维空间坐标&#xff0c;实现对车辆…...

windows中,git bash 使用conda命令

1、首先在Anaconda的安装路径如/Anaconda3/Scripts下&#xff0c;打开git bash窗口&#xff0c;然后输入下面的命令。 ./conda init bash 运行之后&#xff0c;会在用户目录下面生成.bash_profile文件&#xff0c;文件内容如下&#xff1a; # >>> conda initialize…...

《繁星路》V1.8.3(Build16632266)官方中文学习版

《繁星路》官方中文版https://pan.xunlei.com/s/VODae2_2Z3QyMF02I5y321uHA1?pwdqgsh# 作为一款星际模拟游戏&#xff0c;完美融合了硬科幻元素与基地建设玩法&#xff0c;体验改造行星的恢弘与壮阔。化身人工意识AMI&#xff0c;遵照基本指示推进火星改造的各项工作&#xf…...