内容简介:之前组内一个线上服务的内存使用率稳定上扬, 查看监控,发现内存的使用趋势如下图,这种趋势是典型的内存泄露,不解决的话服务会OOM。于是尝试用pprof定位问题,俗话说:Go里面10次内存泄露9次是goroutine泄露。这次也不例外, 系统的内存泄露是由类似下面的代码引起的:这里的
image
问题描述
之前组内一个线上服务的内存使用率稳定上扬, 查看监控,发现内存的使用趋势如下图,这种趋势是典型的内存泄露,不解决的话服务会OOM。
image
问题定位
于是尝试用pprof定位问题,俗话说:Go里面10次内存泄露9次是goroutine泄露。这次也不例外, 系统的内存泄露是由类似下面的代码引起的:
package main
import (
"fmt"
"net/http"
"sync"
_ "net/http/pprof"
)
func bug(_ http.ResponseWriter, _ *http.Request) {
taskChan := make(chan int, 100)
for i := 0; i < 100; i++ {
taskChan <- i
}
consumer := func() {
for task := range taskChan {
fmt.Println(task)
}
}
for i := 0; i < 100; i++ {
go consumer()
}
}
func main() {
http.HandleFunc("/bug", bug)
http.ListenAndServe(":8000", nil)
}
这里的 bug 函数是一个典型的 生产者-消费者 模型,逻辑是这样的:
- 声明一个容量为100的队列buffer
- 开一个循环,生产消息并发送至队列
- 声明消费者,消费者会对队列里的元素进行rpc操作 (这里用fmt.Print代替)
- 开100个协程进行消费
让我们尝试用 pprof 工具定位一下问题,首先在浏览器里访问若干次这个url: http://127.0.0.1:8000/bug ,然后访问pprof的url查看系统运行情况: http://127.0.0.1:8000/debug/pprof/ , 发现系统中有大量的goroutine阻塞着(平时只有200~300个)。
image
点击goroutine,进入 http://127.0.0.1:8000/debug/pprof/goroutine?debug=1 这个链接,此时系统中一共有1704个goroutine,其中有1700个goroutine阻塞在 /go/src/go-test/bug/chan_OOM.go:18 这一行:
image
把url参数改为 debug=2 ,可以看到每个goroutine的信息,用刚刚的 调用栈 以及 行数 作为查找参数,随意找一个goroutine查看详细信息:
image
这里显示ID为448的goroutine当前的状态为 chan receive ,阻塞了12分钟,阻塞在18行。
这个bug接口会被定时访问,每次访问都会新起100个goroutine。这些goroutine一直处于[chan receive] 的状态无法释放,导致goroutine占用的内存无法释放,系统长期运行下去,最终服务无可用内存,OOM~
问题的原因以及解决
goroutine泄露大概有两个场景:
-
channel操作阻塞导致runtime期间goroutine一直在阻塞等待;
-
goroutine有死循环;
这段代码发生泄露的原因就是channel没有关闭,goroutine一直引用channel,没有得到退出信号,导致其一直存活。知道了这个原因之后,还是比较好改的,把channel关闭即可。
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
"sync"
)
func bug(_ http.ResponseWriter, _ *http.Request) {
taskChan := make(chan int, 100)
for i := 0; i < 100; i++ {
taskChan <- i
}
consumer := func() {
for task := range taskChan {
fmt.Println(task)
}
}
for i := 0; i < 100; i++ {
go consumer()
}
}
func bugfix(_ http.ResponseWriter, _ *http.Request) {
taskChan := make(chan int, 100)
for i := 0; i < 100; i++ {
taskChan <- i
}
consumer := func() {
for task := range taskChan {
fmt.Println(task)
}
}
for i := 0; i < 100; i++ {
go consumer()
}
close(taskChan) // bugfix
}
func main() {
http.HandleFunc("/bug", bug)
http.HandleFunc("/bugfix", bugfix)
http.ListenAndServe(":8000", nil)
}
总结
俗话说: Go里面10次内存泄露9次是goroutine泄露引起的。而goroutine泄露大多是由于channel使用不当造成的。这个bug虽然不是我写的,但是要引以为戒~ 平时多看看channel不同模型的使用案例,不然容易踩坑~
欢迎关注我们的微信公众号,每天学习 Go 知识
以上所述就是小编给大家介绍的《[Golang]记一次Goroutine泄露问题的排查》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Goroutine 泄露排查
- Netty堆外内存泄露排查盛宴
- RN 容器的内存泄露排查手记
- Jedis的socket内存泄露问题排查
- 震精!Spring Boot内存泄露,排查竟这么难!
- 一次漫长的 Dubbo 网关内存泄露排查经历
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
嵌入式系统软件设计中的常用算法
周航慈 / 2010-1 / 24.00元
《嵌入式系统软件设计中的常用算法》根据嵌入式系统软件设计需要的常用算法知识编写而成。基本内容有:线性方程组求解、代数插值和曲线拟合、数值积分、能谱处理、数字滤波、数理统计、自动控制、数据排序、数据压缩和检错纠错等常用算法。从嵌入式系统的实际应用出发,用通俗易懂的语言代替枯燥难懂的数学推导,使读者能在比较轻松的条件下学到最基本的常用算法,并为继续学习其他算法打下基础。 《嵌入式系统软件设计中的......一起来看看 《嵌入式系统软件设计中的常用算法》 这本书的介绍吧!