前言
goroutine是golang的重点特色。由上层语言自实现的协程概念,在使用者层面,他的作用等价于线程,但是使用者不需要关注线程资源,线程调度,线程模型等底层原理。
使用一个go协程也非常简单
尽管他的设计和实现都非常轻量,但是在实际团队项目里,因为错误的协程使用,仍然会给团队带来不少隐患,相信大家都遇见过以下问题:
- 发现了内存泄漏问题,继而定位到了数量爆炸的goroutine数量。
- 因为想要在业务里,增加一段其他业务的异步操作,结果该操作意外panic了,导致原app崩溃,触发重启。
- 正常运行的app里,隐藏着大量僵尸协程,它们隐藏在pprof的goroutine簇里,很难定位。
- 协程的堆栈非常难定位发起的位置,尤其是你将协程里的函数封装了(因为堆栈debug.Stack()的起点,是仍然该协程,而调用这个
go f
的位置,是另一个协程,导致调用的位置,不会在stack里被打印)。
那么,基于团队管理,必须提出一个有效的协程使用规范,使goroutine的使用可以安全可靠,并且,可以让使用者,规避掉上述的问题。
分析
- 为什么会出现因为协程积压而内存泄漏的问题,僵尸协程出现的原因是什么?
大部分协程混乱,是因为使用者,错估了协程的生存时间。一条本该立即执行完并销毁的协程,因为for{}
循环,因为<-ch
阻塞,因为漏写select{ case: time.After():}
, 因为迟迟不返回的第三方调用结果,而卡在那里。
我敢说,绝大部分golang开发者,都没有关于此处卡住的报警(因为根本就不认为这里会卡住)。那么,在定位这个僵尸时,就会被动地等待
服务器内存报警
--> 发现内存泄漏
--> 查找内存泄漏app
--> 查看app pprof
--> 在混乱的goroutine簇去分析哪个协程出了问题
。
整个发现和处理的流程,缓慢,而且吃力。
对此,我们必须要求,使用协程的人,清楚,开启一条这样的协程,它预期的生命周期尾期是多少,一旦超过,立即报警, 当报警信息积压到每分钟几次后,触发通知。这样就能杜绝1和3的问题。
- 为什么会出现异步操作panic,导致节点崩溃和重启?
我们知道,go里,一旦出现panic操作,未经recover便会崩溃app。而问题在于,一个recover,只能修正所在协程的panic,当你go f
之后,原协程的recover是无法恢复f里面的panic的。
对此,我们强制,每一个go func(){}()
内部,必须加上recover机制。
而新的问题是,使用者,在使用go时出于以下两个原因,而不会加上recover。
- 因贪图快速开发,懒得写。
- 因对异步的业务代码过于自信,而认为不需要recover
所以,必须让这个recover机制,静默得在开启一个go协程生效。
至此,本次的解决方案,必须做到以下几点
- 可以让协程,从发现是一个僵尸协程时,可以立即报警出来。而不是等待服务器挂掉,再来被动寻找。
- 使用者不需要关注recover,调用时内部必须有recover机制。
- 必须有设置
处理panic
, 处理僵尸协程
的对外方法,以达到不同项目,不同解决方案都可以适配。
我们的目的是,从根本上,让新老组员,不会在goroutine上踩坑。
实现
仓库: https://github.com/fwhezfwhez/goroutine
镜像: https://gitee.com/fwhezfwhez/goroutine
原写法
现写法:
当然,这里更推荐在使用前,进行个性化定制
init.go
util.go
至此,在你的项目里,有需要开启协程的地方
最后,需要强调的是, 需要按照goroutine包的说明文档,注意它的注意事项:
- gouroutine包,是面向业务线的协程使用规范,使用场景是所有业务开go的场景。
- 这种安全性的协程在开销上,和轻便的
go
相比,会大。但是,它的开销和业务逻辑相比,那就可以基本忽略不计了。所以我们最好在业务逻辑里,使用这一套安全的goroutine。 - 如果你是写底层框架,那么推荐还是使用官方的goroutine。