写点什么

记录 response.Body.Close() 引发的 goroutine 泄漏

用户头像
王博
关注
发布于: 2021 年 06 月 09 日

某天下午三点,同事突然找我,因为新版本早上刚刚发布,线上所有机器 CPU 和 Memory 全部打满,ssh 连不上(因为打满的太快了,也没有 pprof 生成),紧急 rollback。rollback 之后,依然很快打满...

一边让 SRE 帮忙 terminal 机器,一边尝试生成 pprof,最终成功凭借 pprof 查出问题。

问题代码如下:

response, err = http.Do(req)if err != nil{  logging.Err()	return}defer response.Body.Close()
复制代码

乍一看这里代码是没有问题的,然后搜了一下 error log,果然这里一直在报错(下游服务内存不足了还没来得及扩容,一直报错也没通知我们。不要好奇为什么不是 gRPC,因为个别服务是发的 http...)

所以猜测是 http.Do 里面开了某个 goroutine,等着执行 response.Body.Close()才会结束 goroutine。

我们先看看 close 方法做了什么(找了半天,真的难找,项目太大了,实现这个接口的真多)

type bodyEOFSignal struct {	body         io.ReadCloser	mu           sync.Mutex        // guards following 4 fields	closed       bool              // whether Close has been called	rerr         error             // sticky Read error	fn           func(error) error // err will be nil on Read io.EOF	earlyCloseFn func() error      // optional alt Close func used if io.EOF not seen}// /usr/local/Cellar/go@1.13/1.13.11/libexec/src/net/http/transport.go:2594func (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 代码,看到只是在调用 bodyEOFSignal 对象里的一些方法,那么这些对象是怎么来的呢?显而易见是 http.Do(req)方法返回的 request body 的实现类(又要看最不想看的代码了),下面我们看看 http.Do 方法里面哪里出现了泄漏,我们直接给出代码

func (t *Transport) roundTrip(req *Request) (*Response, error) {	//省略一堆代码	for {		select {		case <-ctx.Done():			req.closeBody()			return nil, ctx.Err()		default:		}
// Get the cached or newly-created connection to either the // host (for http or https), the http proxy, or the http proxy // pre-CONNECTed to https server. In any case, we'll be ready // to send it requests. pconn, err := t.getConn(treq, cm) if err != nil { t.setReqCanceler(req, nil) req.closeBody() return nil, err }}// /usr/local/Cellar/go@1.13/1.13.11/libexec/src/net/http/transport.go:1580// t.getConn()func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (pconn *persistConn, err error) { pconn = &persistConn{ t: t, cacheKey: cm.key(), reqch: make(chan requestAndChan, 1), writech: make(chan writeRequest, 1), closech: make(chan struct{}), writeErrCh: make(chan error, 1), writeLoopDone: make(chan struct{}), } //省略一堆代码 go pconn.readLoop() go pconn.writeLoop() return pconn, nil}
复制代码

可以看到,http 会启动两个 goroutine 分别处理 readLoop 和 writeLoop。

下面我们看看 readLoop 做了哪些事

func (pc *persistConn) readLoop() {		waitForBodyRead := make(chan bool, 2)    //我们一开始要的构造body对象以及close调用的两个方法		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			},		}
resp.Body = body select { case rc.ch <- responseAndError{res: resp}: case <-rc.callerGone: return }
// 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 { //接收earlyCloseFn传过来的false,也就是这里没有close,goroutine卡在这里了,造成了泄漏 case bodyEOF := <-waitForBodyRead: pc.t.setReqCanceler(rc.req, nil) // before pc might return to idle pool alive = alive && bodyEOF && !pc.sawEOF && pc.wroteRequest() && tryPutIdleConn(trace) if bodyEOF { eofc <- struct{}{} } case <-rc.req.Cancel: alive = false pc.t.CancelRequest(rc.req) case <-rc.req.Context().Done(): alive = false pc.t.cancelRequest(rc.req, rc.req.Context().Err()) case <-pc.closech: alive = false } }}
复制代码

所以问题很清晰了,readLoop 和 writeLoop 两个 goroutine 在 写入请求并获取 response 返回后,并没有跳出 for 循环,而继续阻塞在下一次 for 循环的 select 语句里面,goroutine 一直无法被回收,cpu 和 memory 全部打满(整个 http 的代码太多了...有空慢慢分析吧)

发布于: 2021 年 06 月 09 日阅读数: 9
用户头像

王博

关注

我是一名后端,写代码的憨憨 2018.12.29 加入

还未添加个人简介

评论

发布
暂无评论
记录response.Body.Close()引发的goroutine泄漏