解决golang内存溢出

11次阅读

共计 1822 个字符,预计需要花费 5 分钟才能阅读完成。

最近在项目中出现 golang 内存溢出的问题,master 刚开始运行时只有10 多 M ,运行几天后,竟然达到了10 多个 G 。而且到凌晨流量变少内存也没有明显降低,内存状态呈现一种很不健康的曲线。

像这种情况肯定是 golang 内存溢出 了,为此我持续排查了两天,终于找到问题所在,特此记录下。

准备工作

  • 一台较好的环境测试机, 单台运行无污染。
  • 压测工具,无论服务是 http 还是 websocket 服务,都必须准备好压测工具模拟最真实的用户场景。
  • 将 master 引入 net/http/pprof 包,通过 http 访问获得 goroutine、heap 信息。

    // 引入 pprof
    import _"net/http/pprof"
    // 在 main 中加入
    go func() {log.Println(http.ListenAndServe("localhost:9999", nil))
    }()

    浏览器访问: http://127.0.0.1:9999/debug/pprof/

    获取 goroutine 信息 http://10.13.132.91:9999/debug/pprof/goroutine?debug=2
    获取 heap 信息 http://10.13.132.91:9999/debug/pprof/heap?debug=2
    使用 golang tool 进行统计分析,go tool pprof -inuse_space http://127.0.0.1:9999/debug/pprof/heap。输入 top10 可以看出前十占用内存情况,这里我是直接输入 png 导出图片来查看,以便以后比较。还有两个参数可以选择,-inuse_space顾名思义是正在使用的内存,-alloc_space是已经分配的内存,本次我是一直用 -inuse_space 进行分析。

开始进行分析

go 是一门自己 gc 的语言,大概两分钟会 gc 一次。如果有内存泄漏,无非就是两种情况。

  • 有 goroutine 泄漏,goroutine“飞”了,zombie goroutine 没有结束,这个时候在这个 goroutine 上分配的内存对象将一直被这个僵尸 goroutine 引用着,进而导致 gc 无法回收这类对象,内存泄漏。
  • 有一些全局(或者生命周期和程序本身运行周期一样长的)的数据结构意外的挂住了本该释放的对象,虽然 goroutine 已经退出了,但是这些对象并没有从这类数据结构中删除,导致对象一直被引用,无法被回收。

排除掉 goroutine 泄漏

首先,我利用压测工具对 server 进行 100 个 websocket 连接,模拟用户浏览行为,然后关闭连接。打开浏览器查看 goroutine 数量,发现新起的 goroutine 全部已经销毁,没有观察到有泄漏的 goroutine,因此排除此情况。

确定是全局变量无回收

排除 goroutine 泄漏,只能是由全局状态变量引起的。再次用压测工具进行压测然后关闭,使用观察内存情况。使用 go tool pprof -inuse_space http://127.0.0.1:9999/debug/pprof/heap 输入 png 导出(在这种情况下,需要等程序 gc 完再导出,建议等 10 分钟左右。)

发现问题所在
每次都会遗留这么大概 0.5M 的内存空间出来,就奇怪,明明整个 goroutine 退出为什么还有会内存占用? 相应的全局变量也会删除该地方的引用。等一下,全局变量,难道是删除的时候没做好配对导致没有真正删除该引用吗?去查了下代码,果然是没有 删除引用导致 的,至此问题解决。

后记

为什么会花了两天时间,看起来上述流程并不复杂。

  • 实际上你要完全排除掉 goroutine 泄漏需要花较长的时间去对比的,查看哪些 goroutine 是新起来没有关闭。
  • 在使用 -inuse_space 或者 -alloc_space 分析,也是很纠结,这些看起来也并不完全与表现对应上。实际上用 -inuse_space 是较为直观的,可以展现出程序真正在使用的(RSS)。Go 管理内存的方式可能与你以前使用的方式不太一样。它会在一开始就保留一大块 VIRT,而 RSS 与实际内存用量接近。RSS 和 VIRT 之间有什么区别呢?VIRT 或者虚拟地址空间大小是程序映射并可以访问的内存数量。RSS 或者常驻大小是实际使用的内存数量。因此用 -inuse_space 导出在 png 图上的统计中,与 top 上的 res 值是大致相同。
  • 还有就是每次做压测或者等待 golang 完全 gc 都要耗费不少时间,这样也会排查增加难度。
正文完
 0