golang-memory-leak-failure

一次不成功的Golang内存溢出排查

我们的监控服务是以Prometheus加K8S为基础搭建的,运行一段时间后出现了内存溢出的现象,内存RSS占用高达9G,CPU占用60%。以下是整理的排查思路

PProf排查:

我们先查看了应用日志,这些日志内容不断重复,有一定规律,但是当时暂时没有发现特别之处。

memleak-log

接下来,针对Go应用的内存泄露问题,想到了通过官方PProf工具收集应用信息。幸好,Prometheus开启了PProf接口,因此我们可以很方便的得到协程与内存信息。具体过程不表,结果如下:

部分协程信息如下:

memleak-routines

部分内存信息如下:

memleak-memories

分析协程

我们发现协程数量达到了恐怖的30万个,而且其中有10万处在调用K8S的POD API上,另外还有20万分别是SERVICE API和ENDPOINT API没有在图中列出。接着,再查看了容器环境(Prometheus运行在容器网络中)中与K8S API通信的连接个数,很少,只有一个。

我们知道,K8S API基于gRPC协议通信,GRPC又是基于HTTP2协议,支持在一个TCP连接上多路复用多个请求流。因此,只用少数连接就可以同时处理多个请求,这是没问题的。但是现在只有一个连接,这就有点诡异了,一般HTTP2对于最大并发请求数是有限制的,需要客户端与服务端协商,服务端不会允许这么大的单连接并发。

于是,我们顺着协程栈,逐个翻看相关代码,具体细节不表。从代码中了解到Go HTTP2库对最大并发数的默认限制为1000,超出部分将会等待,也就是说,在某种特殊情况下TCP连接上的等待处理请求数量超过了最大限制,导致后续的类似协程都阻塞在条件变量上,并且呈不断增长的趋势。

因此,协程问题变成了两个子问题:

  1. 什么情况下,会导致待处理请求数超限
  2. 不断增长的协程,是如何触发的

关于第一个问题,很遗憾没有找到原因,后来也没有复现。

关于第二个问题,从前面的日志里面找到了一丝端倪。通过在代码里搜索日志内容的方式,我们定位到,不断重复的日志的源头来自于:

memleak-code

由于这段代码逻辑被反复执行,因此日志内容在不断重复输出。而更巧的是,这段代码所在函数还有另一部分功能,恰好就是启动新协程,通过K8S Client获取POD/SERVICE/ENDPOINT信息!

通过反向追述这段代码的调用源点,发现它会在Prometheus配置文件发生变化的时候被调用。

最后得出部分结论,由于配置文件的频繁更新触发Prometheus的自动加载机制,因此协程数量不断增长。在环境稳定的情况下,配置文件不会频繁变更,因此协程问题得以解决。

分析内存

我们先照图回顾一下Go应用的内存布局:

1
Sys = HeapSys + StackSys + MspanSys + MCacheSys + BuckHashSys + GCSys + OtherSys
  • Sys 代表了 RSS,也就是从操作系统的角度来看,Go进程(包括内部各个线程)总共占用的系统内存,也是此次关注的重点(9G)。从用途的角度来看,Go内存可以分为:堆,栈,内部数据结构,GC以及其他

这里面比较有意思的是HeapSys和StackSys,其他就不介绍了。

  • HeapSys == HeapIdle + HeapInuse 代表了作为堆用途的内存空间。堆内存以Span为单元进行管理,从Span中分配内存给对象,分为Idle(没有分配对象)和Inuse(有对象)两种类型。

  • HeapAlloc == Alloc 代表了堆上的未回收对象的实际占用空间(包括可回收部分)。注意,这和HeapInuse是有区别的。HeapInuse代表了分配对象的Span的大小,而HeapAlloc代表了实际对象的大小,因此可以认为一个是粗统计,一个是细统计,HeapAlloc <= HeapInuse

  • StackSys 代表协程栈大小,其实是Stack Span的大小,可以和Idle Span互相转换。不同版本Go语言的最小协程栈大小是不同的,这次问题里面的协程栈大小为8K,因此30万协程2.4G,和图中的Stack部分相符,但还不足以解释剩下6G的堆内存占用量。

从图中可以看出,6G堆内存中有接近1G处于Idle,剩下5G左右的实际对象占用量。

注意到堆上分配了4千多万个对象,如果每个对象都120B,几乎就可以解释5G大小的内存开销了。联想到Prometheus需要将近期指标加载到内存中,如果短时间内累计大量指标,确实有可能会导致这样的情况发生。

总结

这次排查问题,还是有不完善之处。

时间不够,也没有思路完整解决协程问题。

对于指标过多的问题,解决方案只能是减少指标监控数据,治标不治本。