前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >记一次golang内存泄漏问题的排查

记一次golang内存泄漏问题的排查

原创
作者头像
Johns
修改2022-06-30 10:33:32
4.4K0
修改2022-06-30 10:33:32
举报
文章被收录于专栏:代码工具代码工具

背景

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

image.png
image.png

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

排查过程

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

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

代码语言:txt
复制
package main

import (
	"fmt"
	"net/http"
	_ "net/http/pprof"
)

func main() {
   	// 忽略其他代码
//pprof server
http.ListenAndServe("localhost:8080", nil)
  	// 忽略其他代码
}

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

image.png
image.png

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

image.png
image.png

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

image.png
image.png

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

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

我的代码是这样的

代码语言:txt
复制
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的地方代码是

代码语言:txt
复制
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初始化为一个全局对象,每次需要用到时直接复用之前的连接池就行。

代码调整如下:

代码语言:txt
复制
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就行:

代码语言:txt
复制
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
	}
	// 忽略其他代码
}

重新启动,再压测一下:

image.png
image.png

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

image.png
image.png

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

image.png
image.png

总结

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 排查过程
  • 总结
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
http://www.vxiaotou.com