Golang 作为一个提供了GC的语言,还能有内存泄漏一说?其实不然,Go 服务宕机80%应该是因为内存泄漏的缘故了。





导言


内存泄漏 (Memory Leak) 是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。(维基百科)

所以,内存泄漏是一个共性的问题。虽然在Golang中提供了聪明的GC操作,但是如果操作不慎,也可能掉入内存泄漏的坑。


什么情况下会内存泄漏


总结了一些经常碰到的内存泄漏的例子,以飨读者:

  1. 数据泄漏

    比如,在全局变量(或者单例模式)中 (例如:map,slice 等 构成的数据池),不断添加新的数据,而不释放。

  2. goroutine泄漏

    goroutine 泄漏,应该是Golang 中经常遇到的一个问题了。由于goroutine 存在栈空间(至少会有2K), 所以goroutine 的泄漏常常导致了golang的内存泄漏。
    在官方提供的方法中,如果使用不当,很容易出现goroutine泄漏。比如说:

    在 Time 包中:

1
2
3
4
5
6
7
8
9
10
func After(d Duration) <-chan Time {
return NewTimer(d).C
}

func Tick(d Duration) <-chan Time {
if d <= 0 {
return nil
}
return NewTicker(d).C
}
        由于 NewTicker 和 NewTimer 会创建倒计时发送chan 的协程(创建方法在startTimer 中实现,/src/runtime/time.go),所以这种方法不能多次使用。使用时建议新建 NewTimer 和NewTicker,并控制 Timer 和Ticker 的终结。

        再比如说,在 Http 请求时,会返回 *http.Response 对象,Http 响应中的Body是http的响应数据,Body 需要每次读取后关闭。那为什么需要关闭呢,我们从 Body 的赋值代码查找结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// /src/net/http/transport.go

// http 的持久化链接池,不断取需要做的请求,并做响应
func (pc *persistConn) readResponse(rc requestAndChan, trace *httptrace.ClientTrace) (resp *Response, err error) {
//...
resp.Body = newReadWriteCloserBody(pc.br, pc.conn) // pc.conn net.Conn
// ...
}

func newReadWriteCloserBody(br *bufio.Reader, rwc io.ReadWriteCloser) io.ReadWriteCloser {
body := &readWriteCloserBody{ReadWriteCloser: rwc}
if br.Buffered() != 0 {
body.br = br
}
return body
}
        从代码中可以看出 resp.Body 实际上仅仅是个代理,我们实际上读取和关闭的是net.Conn 对象。因此也不难看出关闭Body 的意义何在了。(如果不关闭,这个conn 应该是不能关闭的)

       除了官方的一些func使用不当会导致goroutine泄漏,日常开发也会碰到各种内存泄漏的例子, 比如说:redis 从连接池取的链接没有做释放,DB 的 stmt 没有关闭等。


       总体来说,golang 内存泄漏仅此而已,全局变量导致的内存不断增大,或者goroutine泄漏。但是在使用时,goroutine 泄漏占到了绝大多数。所以,有内存泄漏时,多看看是不是哪里又忘了关链接了



end


下篇文章我们将具体分析怎么从我们的服务中找出内存泄漏的点,以及如何监控和避免内存泄漏。