http包的内存泄漏

问题

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "runtime"
)

func main() {
    num := 6
    for index := 0; index < num; index++ {
        resp, _ := http.Get("https://www.baidu.com")
        _, _ = ioutil.ReadAll(resp.Body)
    }
    fmt.Printf("此时goroutine个数= %d\n", runtime.NumGoroutine())


}

上面这道题在不执行resp.Body.Close()的情况下,泄漏了吗?如果泄漏,泄漏了多少个goroutine?

怎么答

不进行resp.Body.Close(),泄漏是一定的。但是泄漏的goroutine个数就让我迷糊了。由于执行了6遍,每次泄漏一个读和写goroutine,就是12个goroutine,加上main函数本身也是一个goroutine,所以答案是13. 然而执行程序,发现答案是3,出入有点大,为什么呢?

解释

我们直接看源码。golang 的 http 包。

  • 说明 http.Get 默认使用 DefaultTransport 管理连接。

DefaultTransport 是干嘛的呢?

  • DefaultTransport 的作用是根据需要建立网络连接并缓存它们以供后续调用重用。

那么 DefaultTransport 什么时候会建立连接呢?

接着上面的代码堆栈往下翻

  • 一次建立连接,就会启动一个读goroutine和写goroutine。这就是为什么一次http.Get()会泄漏两个goroutine的来源。

  • 泄漏的来源知道了,也知道是因为没有执行close

那为什么不执行 close 会泄漏呢?

回到刚刚启动的读goroutine 的 readLoop() 代码里

其中第一个 body 被读取完或关闭这个 case:

bodyEOF 来源于到一个通道 waitForBodyRead,这个字段的 true 和 false 直接决定了 alive 变量的值(alive=true那读goroutine继续活着,循环,否则退出goroutine)。

那么这个通道的值是从哪里过来的呢?

  • 如果执行 earlyCloseFn ,waitForBodyRead 通道输入的是 false,alive 也会是 false,那 readLoop() 这个 goroutine 就会退出。

  • 如果执行 fn ,其中包括正常情况下 body 读完数据抛出 io.EOF 时的 case,waitForBodyRead 通道输入的是 true,那 alive 会是 true,那么 readLoop() 这个 goroutine 就不会退出,同时还顺便执行了 tryPutIdleConn(trace) 。

  • tryPutIdleConn 将 pconn 添加到等待新请求的空闲持久连接列表中,也就是之前说的连接会复用。

那么问题又来了,什么时候会执行这个 fnearlyCloseFn 呢?

  • 上面这个其实就是我们比较收悉的 resp.Body.Close() ,在里面会执行 earlyCloseFn,也就是此时 readLoop() 里的 waitForBodyRead 通道输入的是 false,alive 也会是 false,那 readLoop() 这个 goroutine 就会退出,goroutine 不会泄露。

  • 这个read,其实就是 bodyEOFSignal 里的

  • 上面这个其实就是我们比较收悉的读取 body 里的内容。 ioutil.ReadAll() ,在读完 body 的内容时会执行 fn,也就是此时 readLoop() 里的 waitForBodyRead 通道输入的是 true,alive 也会是 true,那 readLoop() 这个 goroutine 就不会退出,goroutine 会泄露,然后执行 tryPutIdleConn(trace) 把连接放回池子里复用。

总结

  • 所以结论呼之欲出了,虽然执行了 6 次循环,而且每次都没有执行 Body.Close() ,就是因为执行了ioutil.ReadAll()把内容都读出来了,连接得以复用,因此只泄漏了一个读goroutine和一个写goroutine,最后加上main goroutine,所以答案就是3个goroutine。

  • 从另外一个角度说,正常情况下我们的代码都会执行 ioutil.ReadAll(),但如果此时忘了 resp.Body.Close(),确实会导致泄漏。但如果你调用的域名一直是同一个的话,那么只会泄漏一个 读goroutine 和一个写goroutine,这就是为什么代码明明不规范但却看不到明显内存泄漏的原因。

  • 那么问题又来了,为什么上面要特意强调是同一个域名呢?改天,回头,以后有空再说吧。

作者:9號同学 链接:https://juejin.cn/post/6896993332019822605 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

最后更新于