最近在用golang开发一个内容推荐的项目, 在打算进行压测前就发现容器每过一段时间就会重启,查看机器内存情况时发现自启动来内存一直在上升,然后到达一个容器最大可用内存阈值后重启。如下图:
通过上图表基本可以断定,内存泄漏了。
有一点需要说明的就是由于golang是基于goroutine进行调度的,所以goland的内存泄漏九成是来自于goroutine内存泄漏, 我们只需要盯着goroutine的最多的那几个地方,基本就能找到内存泄漏的源头。
我使用了go的pprof工具进行了调试排查, 首先在项目中引入这个组件, 并且把可能泄漏业务信息的代码全部脱敏了。
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
)
func main() {
// 忽略其他代码
//pprof server
http.ListenAndServe("localhost:8080", nil)
// 忽略其他代码
}
首先看一下启动时啥流量没有的情况下,CPU和内存的分配情况:
我们点进goroutine的链接里去可以看到总共21个goroutine, 最多的5个是用来跑定时任务的。目前看是项目正常的。
由于我的服务有多个对外接口,所以我先把其他接口路由关了,然后使用压测工具进行压测(这里我使用的是go-stress-testing)
然后我们再观察一下pprof的各项指标
发现goroutine暴涨了,点进去发现居然有1001个redis pool对象, 正常我们的redis连接池是只会有1个的。
我的代码是这样的
package utils
import (
"fmt"
"github.com/go-redis/redis/v8"
"time"
)
func GetRedisClient() *redis.Client {
redisCli := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
PoolSize: 20,
MinIdleConns: 10,
IdleTimeout: 3 * time.Minute,
})
if redisCli != nil {
return redisCli
} else {
fmt.Sprint("redis client is nil")
return nil
}
}
使用redisClient
的地方代码是
func (s baseService) Multiply(ctx context.Context, in *pb.MultiplyRequest) (errCode int32, resp *pb.MultiplyResponse, err error) {
// 忽略其他代码
redisCli := utils.GetRedisClient()
userFeat, err := redisCli.Get(ctx, "user_feat:" + uid).Result()
if err != nil {
return comn.SUCC.Code, &pb.MultiplyResponse{
Code: comn.ErrRequestParamIllegal.Code,
Msg: comn.ErrRequestParamIllegal.Msg,
}, nil
}
// 忽略其他代码
}
发现了吧,原来go-redis包下的redis.Client是一个连接池对象而不是一个简单的客户端连接
知道了问题我们只需要将redis.Client初始化为一个全局对象,每次需要用到时直接复用之前的连接池就行。
代码调整如下:
import (
"fmt"
"github.com/go-redis/redis/v8"
"time"
)
var RedisClient *redis.Client
func init() {
RedisClient = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
PoolSize: 20,
MinIdleConns: 10,
IdleTimeout: 3 * time.Minute,
})
fmt.Sprint(RedisClient)
}
每次请求过来时直接复用redis.Client就行:
func (s baseService) Multiply(ctx context.Context, in *pb.MultiplyRequest) (errCode int32, resp *pb.MultiplyResponse, err error) {
// 忽略其他代码
userFeat, err := utils.RedisClient.Get(ctx, "user_feat:" + uid).Result()
if err != nil {
return comn.SUCC.Code, &pb.MultiplyResponse{
Code: comn.ErrRequestParamIllegal.Code,
Msg: comn.ErrRequestParamIllegal.Msg,
}, nil
}
// 忽略其他代码
}
重新启动,再压测一下:
发现基本上goroutine的数量保持在一个合理的值了。
再把代码发布测试容器,然后过段时间看监控里面的内存使用情况基本也正常了。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。