项目场景: golang中内存泄露的发现与排查一直是来是go开发者头疼的一件事,恰巧最近负责的风控项目,内存使用暴涨,发生了内存直接打满而挂掉的情况。刚开始以为是随着业务的增长,数据处理不过来导致的。但是经过业务的分析和对比,发现并不是如此。幸好go语言有性能大杀器可以监控到,最终根据pprof,问题定位到为go协程泄漏问题。解决完问题后进行记录和分享,写的不对之处,请大家多多指出。 问题描述: 运维同学发出告警提示,服务器的内存即将打满,看看服务是否异常。收到这个消息后,赶紧看服务器的资源使用情况(top命令即可),发现32g内存已经使用了28g,幸好早期在项目中集成了pprof这个性能大杀器,web访问方式的代码如下:
	func main() {
		orm.Debug = true
		go func() {
			log.Println(http.ListenAndServe("0.0.0.0:3351", nil))
		}()
		prod()
	}

pprof是什么:

pprof是Go的性能分析工具,在程序运行过程中,可以记录程序的运行信息,可以是CPU使用情况、内存使用情况、goroutine运行情况等,当需要性能调优或者定位Bug时候,这些记录的信息是相当重要。

问题排查: 打开网页:[监控web地址](http://x.x.x.x:3351/debug/pprof/),(此图为后续相同代码复现的情况,并非当时的情况)显示如下:

监控图
看下具体参数的含义

allocs: 内存分配情况的抽象情况
block: 阻塞堆栈的采样信息
cmdline: 程序启动命令及其参数
goroutine: 当前协程的堆栈信息
heap: 堆内存的采样信息
mutex: 锁竞争的采样信息
profile: cpu使用情况的采样信息
threadcreate: 系统程序创建情况的采样信息
trace: 程序运行的跟踪信息

刷新一下,可以看到goroutine 在持续的增长,所以怀疑是goroutine泄漏导致的。点击 goroutine,发现代码中有一处产生了绝大部分的协程,而且还在在持续性的增长中。
goroutine监控

在这里插入图片描述

找到代码:
事故代码

按道理来说,goroutine+chan 正常的生产消费是不会出现泄漏的问题。对着代码苦思不得其解,喝了杯冰可乐让自己冷静下来,突然发现在代码的181行,也会进行结果的输出,但是没有return,也就意味着如果上面获取redis中的数据,根据json解析异常后,进入异常,则会写入到chan中两次结果。上服务器日志进行排查,发现果然是这块的问题,由于redis中缓存的数据不存在,导致json解析异常,进行了双倍写入,导致了goroutine一直在增长,无法释放:
服务器日志

解决方案: 至此,事故发生的原因已经定位清楚了,修复后的代码如下。
		redisResult := redis.RedisHGet(finalKey, field)
		//get key value from redis
		common.InfoLogger.Infof("finalKey : %v ,filed : %v , result : %v ", finalKey, field, redisResult)
		//防止redis中没有数据,
		if redisResult == ""{
			result <- JobResult{job.JobId, 0, []string{}, errors.New("redis 取不到数据")}
			return
		}
		if err := json.Unmarshal([]byte(redisResult), &tempJson); err != nil {
			common.ErrorLogger.Infof("json err : %v", err)
			result <- JobResult{job.JobId, 0, []string{}, errors.New("redis 取不到数据")}
			return
		}
		result <- JobResult{job.JobId, tempJson[hkey[2]], []string{finalKey}, nil}
心得: pprof这个“性能大杀器”的称号不是白来的,能够非常方便的监控代码运行性能。当然pprof的使用方式还有很多种,不只是本文的这一种方式,更多使用方式,可以参考网上的示例。