最近在用golang开发一个内容推荐的项目, 在打算进行压测前就发现容器每过一段时间就会重启,查看机器内存情况时发现自启动来内存一直在上升,然后到达一个容器最大可用内存阈值后重启。如下图:
通过上图表基本可以断定,内存泄漏了。
排查过程有一点需要说明的就是由于golang是基于goroutine进行调度的,所以goland的内存泄漏九成是来自于goroutine内存泄漏, 我们只需要盯着goroutine的最多的那几个地方,基本就能找到内存泄漏的源头。
我使用了go的pprof工具进行了调试排查, 首先在项目中引入这个组件, 并且把可能泄漏业务信息的代码全部脱敏了。
首先看一下启动时啥流量没有的情况下,CPU和内存的分配情况:
我们点进goroutine的链接里去可以看到总共21个goroutine, 最多的5个是用来跑定时任务的。目前看是项目正常的。
由于我的服务有多个对外接口,所以我先把其他接口路由关了,然后使用压测工具进行压测(这里我使用的是go-stress-testing)
然后我们再观察一下pprof的各项指标
发现goroutine暴涨了,点进去发现居然有1001个redis pool对象, 正常我们的redis连接池是只会有1个的。
我的代码是这样的
redisClient
发现了吧,原来go-redis包下的redis.Client是一个连接池对象而不是一个简单的客户端连接
知道了问题我们只需要将redis.Client初始化为一个全局对象,每次需要用到时直接复用之前的连接池就行。
代码调整如下:
每次请求过来时直接复用redis.Client就行:
重新启动,再压测一下:
发现基本上goroutine的数量保持在一个合理的值了。
再把代码发布测试容器,然后过段时间看监控里面的内存使用情况基本也正常了。
- golang得内存泄漏大部分情况是由于goroutine泄漏导致的,所以排查时我们先关注整体的goroutine数量。
- 像redis,mysql这种比较稀缺的资源使用时可以使用连接池 但是要注意控制池的大小,以及每个连接的超时时间,使用后要注意及时释放。而且go对redis还有mysql都有内置连接池,使用的是要一定要搞清楚初始化的实例到底是一个什么东西。