找回密码
 会员注册
查看: 12|回复: 0

GoroutineLeaks引发的思考(Go语言)

[复制链接]

2万

主题

0

回帖

7万

积分

超级版主

积分
72365
发表于 2024-10-6 10:36:45 | 显示全部楼层 |阅读模式
本期作者邹靓哔哩哔哩创平高级测试开发引言:江湖中有个传说:10次内存泄露,9次都是goroutine泄露。(Go语言背景前段时间,通过监控观察到,我们业务中的某个服务突然出现goroutine数量、堆对象数量激增的情况,作为测试人员的我们跟着开发一步一步揭开了问题的真相,同时通过这个问题的排查也引发了我们的一些思考,如果你也有遇到过类似问题,毫无头绪的时候,欢迎阅读本文,跟我一起拨开层层迷雾,找到问题真凶。(一切问题都只是纸老虎~)排查过程当发生goroutine数量激增的时候,我们的主要排查的思路一般都是先通过直接调用runtime.NumGoroutine()( Go 的runtime包里面包含了一些与Go运行时系统交互的操作,比如控制goroutine的函数。NumGoroutine函数就是查看goroutines运行数量)或者使用可视化的goroutine数量监控工具来查看goroutine数量,通过对比异常时间段前后goroutine的数量是不是持续不正常增长(业务不在高峰期的时候突然翻N倍被视为异常)来确认是否是 goroutine泄漏,一旦确认,那么接下来就可通过找到goroutine泄漏的问题作为切入点(忘记设置默认的请求超时时间、跟redis、sql交互超时、向已关闭的通道发数据等等)从而找到根因。那么我们回到这个业务例子吧!本次可以通过监控看到,问题发生时间前后数量差距巨大且是持续增长的趋势,确定是goroutine泄漏,到底是什么导致了泄漏呢?那么接下来,就必须找到切入点根治它。1、切入点1:找出最耗时的地方在哪里?当我们遇到goroutine相关的问题,第一个步骤就可以先打印异常的goroutine stack trace信息,这样需要选择合适的工具以及方法,目前获取stack trace异常信息的方式主要有:1.使用panic来获取异常退出的stack trace信息 (不巧的是,本次事件没有明显错误异常);2.使用SIGQUIT信号来终止没有panic但是有异常的程序,并获取core文件来查看stack trace信息(但这个方式可能破坏第一现场);3.使用开源工具例如gops[1]?进行堆栈跟踪(自行到下载第三方开源工具并使用);4.使用go tool pprof http://ip:port/debug/pprof/goroutine获取内存统计信息、goroutine信息以及其他性能分析数据[2](根据个人喜好,本次笔者使用的是pprof获取goroutine信息)。从上图中的耗时数据可以看出,rutime.gopark函数占了大头,这代表着存在大量goroutine在调用了runtime.gopark函数后都被暂停了,即被放入等待状态,这可能导致goroutine线程阻塞,这显然是不健康的状态,那么既然知道了时间大量花费在rutime.gopark函数的执行上,接着开始找的就是什么东西在内存中占据了大量位置。2、切入点2:找到最耗内存的地方在哪里那么,最耗内存的,究竟是何方妖孽呢?我们仍然是通过pprof执行go tool pprof -alloc_space http://ip:port/debug/pprof/heap和top命令找到top1耗内存的位置是:google.golang.org/grpc/internal/transport.newBufWriter ( golang软件包中gRPC的内部方法),这代表我们这次的泄漏极有可能跟gRPC使用不当相关,接下来让我们来找找证据,确定这个猜想。?3、找到证据,确认猜想:是否是gRPC引起goroutine的阻塞?我们还是通过pprof执行http://ip:port/debug/pprof/goroutine?debug=1 or 2(debug=1 可以查看某条调用路径上,阻塞在此goroutine的数量。debug=2则可以查看所有goroutine的运行栈(调用路径),可以显示阻塞在此的时间[3])两种命令方式都可以查看当前阻塞在此goroutine的数量以及运行栈,乍一看这些密密麻麻的调用栈信息确实让人眼花缭乱,所以标记出了最重要的信息含义方便理解(图1[3]所示)。通过登陆问题服务器,可以明显看到大量gotoutine在调用栈 google.golang.org/grpc@v1.37.0/internal/transport/http2_client.go:340 的地方阻塞着(图2中标记黄色的代表阻塞的goroutine数量以及调用栈,代码函数;标记红色的地方代表goroutine阻塞的总数量),随即查看 TCP 连接数果然在问题发生时间段前后有出现断层、且数量在持续激增(图3所示中标记红色的地方可以看出存在明显断层,标记黄色的地方可以看到TCP连接数持续激增的趋势)。通过切入点的查看最终可以基本确认了我们之前的猜想,是由于gRPC-Client的异常创建引起goroutine的数量阻塞,并引起了本次goroutine泄漏。4、罪魁祸首锁定:找到造成本次泄漏的代码通过开发的code review问题发生时间段上线的新代码可以看到在业务代码中使用到了Google Cloud Client Libraries for Go ,这类库通常在服务端使用gRPC来连接Google Cloud API[4]。本次问题代码中Client应该写成单例模式使用,不应该写在具体的函数中每次请求就创建一个新的Client ,且go t.keepalive() 并发调用导致大量请求下会有越来越多的client keepalive(图4、图5)而这些client连接都是长连接不会立马关闭,最终导致出现了TCP连接数量和groutine数量激增的情况。答疑解惑本次问题已经排查完毕,找到原因并解决,但是仍有几个疑问。1、goroutine到底是个啥?在java/c++中我们要实现并发编程的时候,我们通常需要自己维护一个线程池,并且需要自己去包装一个又一个的任务,同时需要自己去调度线程执行任务并维护上下文切换,少了任何一步都不可以完成并发编程,而Go语言中的为了实现简单方便的的并发编程的机制,引入了goroutine(协程)跟必不可少的GMP模型。简单来说就是Go程序在运行的时候会智能地将goroutine中的任务合理地分配给每个CPU。在Go语言编程中你不需要去自己写进程、线程、协程,当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个goroutine去执行这个函数就可以了( go func()?),非常简单粗暴[6]。2、Go不是有GC吗,为什么还会有不同形式的内存泄漏的发生?GC是清理不再使用的对象,GO的GC主要的触发机制有手动跟自动,手动回收需要开发自己调用runtime.GC(),自动回收的触发时机分为两种,第一种是超过内存大小阈值(控制器计算的触发堆大小)、第二种是定时触发(默认2min触发一次)。在本次线上问题中我们遇到的是gRpc-client长连接不断重复创建新的 goroutine但是没有被正确的合理复用导致的泄漏问题(属于持续不断地产生新的 goroutine、在goroutine生命周期没有结束的情况下GC不会清理这块内存)。3、goroutine泄露除了本次的场景,到底还有多少其他场景?除了错误创建gRPC-Client连接还有关于通道(channel)的场景,比如 channel 的读或者写:1.无缓冲channel的阻塞通常是写操作因为没有读而阻塞;2.有缓冲的channel因为缓冲区满了,写操作阻塞;3.期待从channel读数据,结果没有goroutine写数据;4.使用是当有一个全局对象时,可能不经意间将某个变量附着在其上,且忽略的将其进行释放,则该内存永远不会得到释放;5.select操作,select里也是channel操作,如果所有case上的操作阻塞,goroutine也无法继续执行[6];6.一个goroutine尝试向一个没有接收方的无缓冲channel发送消息。当然还有别的类型的内存泄漏问题,从这几个例子中简单举几个demo说明,方便以后遇到了踩坑。比如第4种、跟第6种形式的内存泄漏例子具体如下[7]:测试函数?第4种错误使用。例如:var cache = map[interface{}]interface{}{}func keepalloc() { ? ? ? ? ? ?for i := 0; i /dev/null & echo -n "." || echo -e "\n$test failed"; done总结通过这次的goroutine泄漏的学习印证了Dave的那句话:“Never start a goroutine without knowing how it will stop.” 当一个启用跟运行成本低的goroutine却被人遗忘它也会占用其他成本(CPU、RAM)时,往往会发生滥用、无人管制最终酿成泄漏的悲惨下场。我们在使用gRPC-Client的时候应该做好复用client ,并在使用完的时候做好close ,不然会出现TCP连接数过多的内存溢出。参考[1]https://github.com/google/gops[2]https://pkg.go.dev/net/http/pprof[3]https://lessisbetter.site/2019/05/18/go-goroutine-leak/[4]https://github.com/googleapis/google-cloud-go[5]https://learnku.com/articles/58641[6]https://www.topgoer.com/[7]https://www.bookstack.cn/read/qcrao-Go-Questions/spilt.7.GC-GC.md[8]https://github.com/uber-go/goleak
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-11 12:39 , Processed in 0.499948 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表