10

Go程序内存泄漏的分析以及避免

 3 years ago
source link: https://www.zenlife.tk/go-leak.md
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Go程序内存泄漏的分析以及避免

2016-10-18

给系统打压力,内存占用上去了,停止打压后,仍然降不下来,就可能是有泄漏。

对于无状态的服务,连接上有请求过来,内存上去了。停了请求,但是内存仍然居高不下,等到连接断开内存才降,则session可能存在不合理的引用,造成GC无法回收。

看内存占用的时候不要光盯着top上面的数字,因为Go向系统申请的内存不使用后,也不会立刻归还给系统。也就意味着停止打压后内存占用不立刻下降属于正常行为,仅看这一项需要多花点时间观察一会儿。最好是几个数据一起关注:一个是程序占用的系统内存,另一个是Go的堆内存,最后一个是实际使用到的内存。从系统申请的内存会在Go的内存池管理,整块的内存页,长时间不被访问并满足一定条件后,才归还给操作系统。又因为有GC,堆内存也不能代表内存占用,清理过之后剩下的,才是实际使用的内存。调用runtime.ReadMemStats可以看到Go的内存使用信息; 或者启用net/pprof后访问 http://127.0.0.1:6060/debug/pprof/heap ,也可以看,其中HeapInuse是实际的内存使用量,具有参考意义; 还可以带上参数debug/pprof/heap?debug=2之类得到更细的信息,

发现问题后,首先不要怀疑Go语言的GC有问题,一定要相信是自己的代码写的有问题。内存释放不掉,不是泄漏的,而是代码肯定有什么地方还引用着那块内存,导致GC无法释放。怎么查问题呢?Go语言有pprof这个神器。

经过在上一步看HeapInuse,确认过内存没释放之后,可以用 go tool pprof -inuse_space http://127.0.0.1:6060/debug/pprof/heap 查看是什么地方占用了内存,这里可以大致看到是什么函数占内存,根据代码可以推测出一些信息。一般来讲,很有可能的就是goroutine leak,然后里面引用到的内存都释放不掉。举一个例子:

ch := make(chan T)
go produce(ch) {
  // 生产者往ch里写数据
  ch <- T{}
}
go consume(ch) {
  // 消费者从ch里读出数据
  <-ch
  err := doSomeThing()
}
消费者发生err并退出了,不再读ch,导致生产者阻塞在ch <- T{}上面,然后生产者的goroutine就泄漏了,里面引用的内存永远无法释放。这是一个十分常见的场景,比如说开多个worker,worker处理好数据后写channel,主线程读channel,但是主线程处理过程中出错退出,如果处理不当worker就可能泄漏的。

开启net/pprof之后,通过 http://127.0.0.1:6060/debug/pprof/goroutine?debug=1 可以看到当前的所有goroutine栈,可以找到有哪些goroutine,当前执行到什么位置,可以找到在哪里goroutine泄漏了。

上面说了如何分析和定位Go程序的内存泄漏问题,接下来讲一下如果避免写会泄漏的代码。

第一条原则是,绝对不能由消费者关channel,因为向关闭的channel写数据会panic。正确的姿势是生产者写完所有数据后,关闭channel,消费者负责消费完channel里面的全部数据:

func produce(ch chan<- T) {
    defer close(ch) // 生产者写完数据关闭channel
    ch <- T{}
}
func consume(ch <-chan T) {
    for _ = range ch { // 消费者用for-range读完里面所有数据
    }
}
ch := make(chan T)
go produce(ch)
consume(ch)

为什么consume要读完channel里面所有数据?因为go produce()可能有多个,这样写的代码,在读完ch可以确定所有produce的goroutine都退出了,不会泄漏。

第二条原则是,利用关闭channel来广播取消动作。向关闭的channel读数据永远不会阻塞,这是进阶的技巧。假设消费者拿到数据处理后有error发生,整个动作失败,那么需要有某种机制通知生产者停止并退出。

func produce(ch chan<- T, cancel chan struct{}) {
    select {
      case ch <- T{}:
      case <- cancel: // 用select同时监听cancel动作
    }
}
func consume(ch <-chan T, cancel chan struct{}) {
    v := <-ch
    err := doSomeThing(v)
    if err != nil {
        close(cancel) // 能够通知所有produce退出
        return
    }
}
for i:=0; i<10; i++ {
    go produce()
}
consume()

WaitGroup之类的可以配合着用,看自己喜欢的风格。基本上能处理好error场景下的资源释放,问题就不大。哦,第零条原则是,对于并发的代码心存敬畏之心,哪怕用Go,哪怕有channel这么好用的东西!

context里面的cancel比较值得参考和学习 ,其实没什么技巧,就是多看代码多写代码,标准库的代码是极好的学习材料。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK