如果很悲剧,代码上线后,服务发生了内存泄漏,亡羊补牢为时不晚。我们从以下几点去分析。
分析goroutine 是否泄漏
从 pprof 的goroutine 分析,是否 goroutine 在持续增长。如果持续增长,那 goroutine 泄漏没跑了。我们用下面的例子来举例。
12345678910111213141516171819202122232425262728293031323334353637
| package mainimport ( "net/http" _ "net/http/pprof" "time")type none struct{}func main() { go func() { ch := make(chan none) consumer(ch) producer(ch) }() _ = http.ListenAndServe("0.0.0.0:8080", nil)}func consumer(ch chan none) { for i := 0; i < 1000; i++ { // 此处类似协程泄漏 go func() { }() time.Sleep(3 * time.Microsecond) }}func producer(ch chan none) { time.Sleep(100 * time.Second) for i := 0; i < 1000; i++ { ch }}
|
上述代码中,逐步创建了1k个goroutine(假定是泄漏的),我们可以通过
http://127.0.0.1:8080/debug/pprof/
访问查看goroutine的变化情况。1. 在debug 中观察goroutine的数量变化,如果持续增长,那可以确定是goroutine 泄漏了。
2. 之后访问
http://127.0.0.1:8080/debug/pprof/goroutine?debug=1
查看各goroutine数量,查看持续增加的goroutine ,如果存在持续增长的goroutine,那从goroutine的堆栈代码短分析即可。下图中很明显可以看出1K的协程量。(当然是持续增长到达1K的)
数据泄漏怎么看
数据泄漏出现的问题就比较多了,比如长的 string,slice 数据用切片的方式被引用,如果切片后的数据不释放,长的string,slice 是不会被释放的, 当然这种泄漏比较小。下面举一个前两天网友提供的一个案例。
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
| package mainimport ( "fmt" "io/ioutil" "net" "net/http" _ "net/http/pprof" "time")type None int64func main() { go func() { singals := []int64{} netListen, _ := net.Listen("tcp", ":30000") defer netListen.Close() for { conn, err := netListen.Accept() if err != nil { fmt.Println("Accept Error") } singals = append(singals, 1) go doSomething(conn) } for _ = range singals { fmt.Println("Received") } }() _ = http.ListenAndServe("0.0.0.0:8080", nil)}func doSomething(conn net.Conn) { defer conn.Close() time.Sleep(100 * time.Microsecond) buf, err := ioutil.ReadAll(conn) if err == nil { fmt.Println(string(buf)) }}
|
例子比较简单,从net Accept 数据,并开启一个goroutine 做数据处理。singals 呢,用于做事件处理,每接收一个链接,给singal 推一条数据。
为了从中查找内存泄漏,我们也增加了pprof。为了能尽快发现问题,我这边用了一个简单的shell对服务施压(请求2w http 服务,不关心请求返回结果)。命令如下:
1
| for i in `seq 0 20000`; do curl -m 1 "http://127.0.0.1:30000?abc=def" & done
|
从pprof 的 heap 中,我们能轻易的发现:
内存分配中,mem_leak文件的26行(append) 操作 申请的内存排在了top 1,仔细看代码,发现我们slice中的数据从来没有释放,所以造成了上面的问题。如何解决这个问题呢?其实比较简单。只需要将slice,修改成带cache的chan(作为一个队列来使用),当数据使用过后即可销毁。不仅不会再出现内存泄漏,也保证了功能上的一致性。(当然需要重新起一个协程, 由于上面的for 是阻塞的,不会断开,所以也导致了下面的slice 不工作)
小结
当然,上面的例子都是精简到不能再精简的小例子,实际中遇到的问题可能会要比这个复杂的多。但是万变不离其宗,找到正确的方法解决也不是什么难事。
除了上面的一些问题,还应该注意点什么,做了下面的总结:
做一个服务进程内存监控的报警,这个很有必要,也是正常服务应该做的。
pprof 提供的是堆上的监控,栈内存很少会泄漏,也不容易被监控。
尽量在方法返回时不要让使用者去操作Close,减少goroutine泄漏的可能。
在用全局的Map,Slice 时要反复考虑导致内存泄漏。
slice 引用大切片时,考虑会不会有不释放的可能性。
end
才疏学浅,有问题请留言。谢谢!
推荐阅读
喜欢本文的朋友,欢迎关注“Go语言中文网”:
Go语言中文网启用微信学习交流群,欢迎加微信:274768166,投稿亦欢迎