NOTE

4.4 Golang内存泄露

1. 内存泄露 内存泄露.md 2. Golang内存泄露 2.1. root对象引用 2.2. goroutine 泄漏 goroutine泄露:原理、场景、检测和防范 \- SegmentFault 思否 - 如果你启动了一个 goroutine,但并没有符合预期的退出,那么直到程序结束这个go

Go创建于 更新于 historical

这是历史学习笔记,可能存在过时或不完整的理解。

1. 内存泄露

内存泄露.md(原链接已失效)

2. Golang内存泄露

2.1. root对象引用

var cache = map[interface{}]interface{}{}

func keepalloc() {
  for i := 0; i < 10000; i++ {
    m := make([]byte, 1<<10)
    cache[i] = m
  }
}

2.2. goroutine 泄漏

goroutine泄露:原理、场景、检测和防范 - SegmentFault 思否

  • 如果你启动了一个 goroutine,但并没有符合预期的退出,那么直到程序结束这个goroutine才会退出。这就是goroutine泄漏
  • 当 goroutine 泄露发生时,
    • 该 goroutine 的栈(一般 2k 内存空间起)一直被占用不能释放
    • goroutine 里的函数在堆上申请的空间也不能被 垃圾回收器 回收。 一个程序持续不断地产生新的 goroutine

func keepalloc2() {
  for i := 0; i < 100000; i++ {
    go func() {
      select {}
    }()
  }
}

2.3. 非runtime管理的内存泄露

比如cgo分配的内存,不属于golang runtime管理

3. 如何分析内存泄露

  1. top命令查看是否golang程序占用内存高
  2. 使用pprof分析
    1. go tool pprof http://xxx/debug/pprof/heap?debug=1查看runtime.MemStat
      • 查看HeapSys+StackSys或者Sys ,如果远远小于Top的RSS,那么说明消耗的内存不是由go runtime管理的,问题可能出在cgo,
        • 对于cgo而言,为了不让goroutine阻塞,cgo都是单独开一个线程进行处理的,这种是runtime不能管理的;可以通过ps -mp PID查看线程数,数量很大那么说明就是cgo开辟的线程
        • valgrind或者bcc分析C程序内存泄露Go程序内存泄露问题快速定位 - MySpace
      • 如果HeapInuse远远小于Top的RSS,那么可能是Go1.12中引进MADV_FREE模式导致的,通过HeapIdle-HeapReleased可以知道没有归还给OS的内存有多少
    2. 查看goroutine数量,分析是否goroutine阻塞造成泄露
      1. go tool pprof http://xxx/debug/pprof/goroutine?debug=1查看goroutine数量
      2. go tool pprof http://xxx/debug/pprof/goroutine?debug=2查看goroutine详情,是否有锁资源或者chan send阻塞的情况
    3. go tool pprof http://xxx/debug/pprof/heap查看堆对象占用情况,找出占用最大的

4. 内存泄露实例

4.1. Top RSS > Golang heap Size

压测后发现Top的RSS值没有下降,Top查看了下是Golang进程的RSS占用很高,Golang pprof分析了下发现没有占用很高的内存对象,排查发现是Go1.12中引进MADV_FREE模式导致的,即为了提高内存分配效率Golang runtime回收对象后不把内存还给操作系统

4.2. 内存掉-1分析

监控发现Golang程序每隔2小时内存掉为-1,以为是panic,查了下panic日志发现没问题,Golang pprof分析了下发现没有占用很高的内存对象,不过有引用cgo库,所以以为是c程序core dump了导致panic没捕获,然后打开core dump开关发现也没有,想来想去怀疑是监控问题,然后在另一个属性监控页面发现内存是正常的,没有掉-1的情况,所以结论就是监控问题

4.3. cgo内存泄露

程序定时执行,HTTP取出主播的封面图片,然后调用imagick调整像素,HTTP重新上传 HTTP client只设置了TCP建立连接的超时时间,没有设置HTTP的请求超时

DefaultCli = &http.Client{
		Transport: &http.Transport{
			DialContext: (&net.Dialer{
				Timeout:   2 * time.Second,
				KeepAlive: 30 * time.Second,
			}).DialContext,
	}}

channel在make的时候没有设置缓冲值,所以当超时的时候函数返回,此时ch没有消费者了,就一直阻塞了

func RebuildImage() {
	var wg sync.WaitGroup
	wg.Add(3)

	// 耗时1
	go func() {
		// do sth
		defer wg.Done()
	} ()

	// 耗时2
	go func() {
		// do sth
		defer wg.Done()
	} ()

	// 耗时3
	go func() {
		// do sth
		defer wg.Done()
	} ()

	ch := make(chan struct{})

	go func () {
		wg.Wait()
		ch <- struct{}{}
	}()

	// 接收完成或者超时
	select {
	case <- ch:
		return
	case <- time.After(time.Second * 10):
		return
	}
}

服务里面有用到cgo的一个库进行图片处理,在处理的时候占用了很大的内存,由于某种原因阻塞或者没有释放线程,导致服务的线程数暴涨,最终导致了golang的内存泄漏

5. 参考