accept 阻塞怎么断开_Golang 内存泄漏了怎么办?

如果很悲剧,代码上线后,服务发生了内存泄漏,亡羊补牢为时不晚。我们从以下几点去分析。

b9a4683c3887434e7ff491cf7c76e35c.png d8098f8ad4089fcc48c3e97dc3358d2e.png 26a328933ccb140b3a954d65cc99ab58.png

分析goroutine 是否泄漏

5d66d66edcf671ef4ff6c4f0063aab14.png

      从 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 泄漏了。

5f7121759a8fcffdaadd47a150233306.png

2. 之后访问  http://127.0.0.1:8080/debug/pprof/goroutine?debug=1查看各goroutine数量,查看持续增加的goroutine ,如果存在持续增长的goroutine,那从goroutine的堆栈代码短分析即可。下图中很明显可以看出1K的协程量。(当然是持续增长到达1K的) 136ee3d7a7c6843383a1a74ee797aa86.png
26a328933ccb140b3a954d65cc99ab58.png

数据泄漏怎么看

5d66d66edcf671ef4ff6c4f0063aab14.png数据泄漏出现的问题就比较多了,比如长的 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 中,我们能轻易的发现:

b75767524c512dc644f6b868ed6968a2.png

内存分配中,mem_leak文件的26行(append) 操作 申请的内存排在了top 1,仔细看代码,发现我们slice中的数据从来没有释放,所以造成了上面的问题。如何解决这个问题呢?其实比较简单。只需要将slice,修改成带cache的chan(作为一个队列来使用),当数据使用过后即可销毁。不仅不会再出现内存泄漏,也保证了功能上的一致性。(当然需要重新起一个协程, 由于上面的for 是阻塞的,不会断开,所以也导致了下面的slice 不工作)

26a328933ccb140b3a954d65cc99ab58.png

小结

5d66d66edcf671ef4ff6c4f0063aab14.png当然,上面的例子都是精简到不能再精简的小例子,实际中遇到的问题可能会要比这个复杂的多。但是万变不离其宗,找到正确的方法解决也不是什么难事。

除了上面的一些问题,还应该注意点什么,做了下面的总结:

  • 做一个服务进程内存监控的报警,这个很有必要,也是正常服务应该做的。

  • pprof 提供的是堆上的监控,栈内存很少会泄漏,也不容易被监控。

  • 尽量在方法返回时不要让使用者去操作Close,减少goroutine泄漏的可能。

  • 在用全局的Map,Slice 时要反复考虑导致内存泄漏。

  • slice 引用大切片时,考虑会不会有不释放的可能性。

end

才疏学浅,有问题请留言。谢谢!

推荐阅读

  • 获得了“官方自己都会踩的”坑认证:slice 类型内存泄露的逻辑


喜欢本文的朋友,欢迎关注“Go语言中文网”:

fe61ad7c83b2c50d6b661ea6b01cbbdb.png

Go语言中文网启用微信学习交流群,欢迎加微信:274768166,投稿亦欢迎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值