这几天发现服务的内存一直往上涨,这是监控看到的图标,可以一眼看出
最后一段线变平了是因为业务方的调用停掉了。
遇到这种情况,首先想到的是查看内存分布图,于是调用pprof,拿到内存分布图
我们的机器是16G的,从监控图表看到内存占用达到了百分之50多,将近10个G,但是pprof那边显示内存占用只有2.58G,而且可以清楚的看到,其中两个G是copy函数生成的,0.5个G是加载字典文件的内存,从图上面看,内存分布完全符合我们的预期。但是机器上显示占用了10个G,那多余的7个多G去哪了?
为了搞明白这个,于是打印了GC的信息
{"level":"debug","msg":"GC-0:sys=10128MB HeapAlloc=5028MB HeapSys=9661MB","time":"2020-07-24 06:45:30.549"} {"level":"debug","msg":"GC-1:sys=10128MB HeapAlloc=2659MB HeapSys=9661MB","time":"2020-07-24 06:45:31.135"} {"level":"debug","msg":"GC-0:sys=10128MB HeapAlloc=4702MB HeapSys=9661MB","time":"2020-07-24 07:15:07.134"} {"level":"debug","msg":"GC-1:sys=10128MB HeapAlloc=2639MB HeapSys=9661MB","time":"2020-07-24 07:15:07.733"} {"level":"debug","msg":"GC-0:sys=10128MB HeapAlloc=5048MB HeapSys=9661MB","time":"2020-07-24 07:30:45.724"} {"level":"debug","msg":"GC-1:sys=10128MB HeapAlloc=2650MB HeapSys=9661MB","time":"2020-07-24 07:30:46.327"} {"level":"debug","msg":"GC-0:sys=10128MB HeapAlloc=4985MB HeapSys=9661MB","time":"2020-07-24 07:45:28.112"} {"level":"debug","msg":"GC-1:sys=10128MB HeapAlloc=2663MB HeapSys=9661MB","time":"2020-07-24 07:45:28.672"} {"level":"debug","msg":"GC-0:sys=10128MB HeapAlloc=4725MB HeapSys=9661MB","time":"2020-07-24 08:15:07.868"} {"level":"debug","msg":"GC-1:sys=10128MB HeapAlloc=2650MB HeapSys=9661MB","time":"2020-07-24 08:15:08.444"}
这是正式服的数据,GC-0是触发垃圾回收前的数据,GC-1是完成垃圾回收后的数据。可以看到 HeapSys用了9个多G,sys10个G,HeapAlloc则是2.5个G和5个G
5个G是垃圾回收之前打印的信息,而2.5个G是垃圾回收之后打印的信息,可以看到,垃圾回收之后HeapAlloc内存明显减小了。
但是为什么另外两个指标没减下去呢?
我在测试服加了些指标打印,如下:
{"level":"info","msg":"GC-0:sys=6109MB HeapAlloc=4856MB HeapSys=5822MB HeapIdle=495MB HeapInuse=5327MB HeapReleased=0MB HeapObjects=24977750","time":"2020-07-22 16:36:52.159"}
{"level":"info","msg":"GC-1:sys=6112MB HeapAlloc=2673MB HeapSys=5822MB HeapIdle=2680MB HeapInuse=3142MB HeapReleased=0MB HeapObjects=16438129","time":"2020-07-22 16:36:52.883"}
首先搞清楚每个指标的含义:
sys是程序所占用的堆空间+栈空间(包含虚拟空间)
HeapSys是程序占用 的堆空间(包含虚拟空间)
HeapAlloc是程序对象实际使用的堆空间,即span里面的object被分配出去的空间
HeapIdle是空闲的堆空间(包含虚拟空间)
HeapReleased是被释放的堆空间
HeapInuse是在使用的堆空间(只要span里面的一个object被使用了,整个span都被计入其中。所以这个比HeapAlloc大)
结合前面的pprof,可以看到HeapAlloc基本与pprof图标看到的内存消耗吻合,而且一直维持在2.5g左右,说明我们程序内存还是挺正常的,并没有产生内存泄漏。
结合go的内存分配策略,go在申请内存的时候,会预先申请一块大内存以备用。比如我程序需要用1个G的内存,他有可能会申请1.5个G,而且当垃圾回收后,程序内存释放,也只是归还到mspan,mcentral和mheap。并不会归还给操作系统。当正真归还给操作系统,大概在5分钟之后,前提是内存过多。
显然10个G的内存太多了,最终也没归还给系统。具体问题就变成了HeapSys的空闲内存为啥没返回给操作系统。而HeapSys = HeapInuse + HeapIdle,所以就是HeapIdle(空闲的堆空间)为啥没把内存返还给操作系统。
这边于是做了一个实验,就写了个程序去抢占内存。最终发现当内存不够用的时候,go会释放多余的空间。HeapReleased=2657MB,显示释放了2657MB空间(测试环境)。
但是在打印的堆信息上面,HeapIdle还是没有减小。这是因为那边显示的是虚拟空间,实际占用的空间已经还回去了。
进一步发现,HeapReleased是HeapIdle的子集,HeapIdle-HeapReleased 就是程序实际可用的空闲空间(未分配的span)。
最终通过百度发现,go在1.12版本的时候修改了他的内存回收策略,变成了惰性回收。
当系统内存不够的时候,go才会把这部分内存归还系统。这样做的好处是go向系统申请空间的时候,可以复用之前未被回收的堆内存,而不会触发缺页异常,从而导致内存重新分配。
我们可以通过参数GODEBUG=madvdontneed=1 回退会1.11版本的回收策略。
即GODEBUG=madvdontneed=1 ./server