背景

最近在用golang开发一个内容推荐的项目, 在打算进行压测前就发现容器每过一段时间就会重启,查看机器内存情况时发现自启动来内存一直在上升,然后到达一个容器最大可用内存阈值后重启。如下图:

image.png

通过上图表基本可以断定,内存泄漏了。

排查过程

有一点需要说明的就是由于golang是基于goroutine进行调度的,所以goland的内存泄漏九成是来自于goroutine内存泄漏, 我们只需要盯着goroutine的最多的那几个地方,基本就能找到内存泄漏的源头。

我使用了go的pprof工具进行了调试排查, 首先在项目中引入这个组件, 并且把可能泄漏业务信息的代码全部脱敏了。

首先看一下启动时啥流量没有的情况下,CPU和内存的分配情况:

image.png

我们点进goroutine的链接里去可以看到总共21个goroutine, 最多的5个是用来跑定时任务的。目前看是项目正常的。

image.png

由于我的服务有多个对外接口,所以我先把其他接口路由关了,然后使用压测工具进行压测(这里我使用的是go-stress-testing)

image.png

然后我们再观察一下pprof的各项指标

发现goroutine暴涨了,点进去发现居然有1001个redis pool对象, 正常我们的redis连接池是只会有1个的。

我的代码是这样的

redisClient

发现了吧,原来go-redis包下的redis.Client是一个连接池对象而不是一个简单的客户端连接

知道了问题我们只需要将redis.Client初始化为一个全局对象,每次需要用到时直接复用之前的连接池就行。

代码调整如下:

每次请求过来时直接复用redis.Client就行:

重新启动,再压测一下:

image.png

发现基本上goroutine的数量保持在一个合理的值了。

image.png

再把代码发布测试容器,然后过段时间看监控里面的内存使用情况基本也正常了。

image.png
总结
  • golang得内存泄漏大部分情况是由于goroutine泄漏导致的,所以排查时我们先关注整体的goroutine数量。
  • 像redis,mysql这种比较稀缺的资源使用时可以使用连接池 但是要注意控制池的大小,以及每个连接的超时时间,使用后要注意及时释放。而且go对redis还有mysql都有内置连接池,使用的是要一定要搞清楚初始化的实例到底是一个什么东西。