Loading [MathJax]/jax/input/TeX/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Go 内存泄漏那些事

Go 内存泄漏那些事

作者头像
王小明_HIT
发布于 2024-02-29 08:32:13
发布于 2024-02-29 08:32:13
17300
代码可运行
举报
文章被收录于专栏:程序员奇点程序员奇点
运行总次数:0
代码可运行

Go 内存泄漏那些事

遵循一个约定:如果goroutine负责创建goroutine,它也负责确保他可以停止 goroutine

channel 泄漏

发送不接收,一般来说发送者,正常发送,接收者正常接收,这样没啥问题。但是一旦接收者异常,发送者会被阻塞,造成泄漏。

select case 导致协程泄漏

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func leakOfMemory() {
 errChan := make(chan error) //a.
 go func() {
  time.Sleep(2 * time.Second)
  errChan <- errors.New("chan error") // b.
  fmt.Println("finish ending ")
 }()

 select {
 case <-time.After(time.Second): 
  fmt.Println("超时") //c
 case err := <-errChan: //d.
  fmt.Println("err:", err)
 }
 fmt.Println("leakOfMemory exit")
}

func TestLeakOfMemory(t *testing.T) {
 leakOfMemory()
 time.Sleep(3 * time.Second)
 fmt.Println("main exit...")
 fmt.Println("NumGoroutine:", runtime.NumGoroutine())
}

上面的代码执行结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
=== RUN   TestLeakOfMemory
超时
leakOfMemory exit
main exit...
NumGoroutine: 3
--- PASS: TestLeakOfMemory (4.00s)
PASS

最开始只有两个 goruntine ,为啥执行后有三个 goruntine ?

由于没有往 errChan 中发送消息,所以 d 处 会一直阻塞,1s 后 ,c 处打印超时,程序退出,此时,有个协程在 b 处往协程中塞值,但是此时外面的 goruntine 已经退出了,此时 errChan 没有接收者,那么就会在 b处阻塞,因此协程一直没有退出,造成了泄漏,如果有很多类似的代码,会造成 OOM

for range 导致的协程泄漏

看如下代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func leakOfMemory_1(nums ...int) {
 out := make(chan int)
 // sender
 go func() {
  defer close(out)
  for _, n := range nums { // c.
   out <- n
   time.Sleep(time.Second)
  }
 }()

 // receiver
 go func() {
  ctx, cancel := context.WithTimeout(context.Background(), time.Second)
  defer cancel()
  for n := range out { //b.
   if ctx.Err() != nil { //a.
    fmt.Println("ctx timeout ")
    return
   }
   fmt.Println(n)
  }
 }()

}

func TestLeakOfMemory(t *testing.T) {
 fmt.Println("NumGoroutine:", runtime.NumGoroutine())
 leakOfMemory_1(1, 2, 3, 4, 5, 6, 7)
 time.Sleep(3 * time.Second)
 fmt.Println("main exit...")
 fmt.Println("NumGoroutine:", runtime.NumGoroutine())
}

上述代码执行结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
=== RUN   TestLeakOfMemory
NumGoroutine: 2
1
2
ctx timeout 
main exit...
NumGoroutine: 3
--- PASS: TestLeakOfMemory (3.00s)
PASS

理论上,是不是最开始只有2个goruntine ,实际上执行完出现了3个gorountine, 说明 leakOfMemory_1 里面起码有一个协程没有退出。 因为时间到了,在 a 出,程序就准备退出了,也就是说 b 这个就退出了,没有接收者继续接受 chan 中的数据了,c处往chan 写数据就阻塞了,因此协程一直没有退出,就造成了泄漏。

如何解决上面说的协程泄漏问题?

可以加个管道通知来防止内存泄漏。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func leakOfMemory_2(done chan struct{}, nums ...int) {
 out := make(chan int)
 // sender
 go func() {
  defer close(out)
  for _, n := range nums {
   select {
   case out <- n:
   case <-done:
    return
   }
   time.Sleep(time.Second)
  }
 }()

 // receiver
 go func() {
  ctx, cancel := context.WithTimeout(context.Background(), time.Second)
  defer cancel()
  for n := range out {
   if ctx.Err() != nil {
    fmt.Println("ctx timeout ")
    return
   }
   fmt.Println(n)
  }
 }()
}
func TestLeakOfMemory(t *testing.T) {
 fmt.Println("NumGoroutine:", runtime.NumGoroutine())
 done := make(chan struct{})
 defer close(done)
 leakOfMemory_2(done, 1, 2, 3, 4, 5, 6, 7)
 time.Sleep(3 * time.Second)
 done <- struct{}{}
 fmt.Println("main exit...")
 fmt.Println("NumGoroutine:", runtime.NumGoroutine())
}

代码执行结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
=== RUN   TestLeakOfMemory
NumGoroutine: 2
1
2
ctx timeout 
main exit...
NumGoroutine: 2
--- PASS: TestLeakOfMemory (3.00s)
PASS

最开始是 2个 goruntine 程序结束后还2个 goruntine,没有协程泄漏。

goruntine 中 map 并发

map 是引用类型,函数值传值是调用,参数副本依然指向m,因为值传递的是引用,对于共享变量,资源并发读写会产生竞争,故共享资源遭受到破坏。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func TestConcurrencyMap(t *testing.T) {
 m := make(map[int]int)
 go func() {
  for {
   m[3] = 3
  }

 }()
 go func() {
  for {
   m[2] = 2
  }
 }()
 //select {}
 time.Sleep(10 * time.Second)
}

上诉代码执行结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
=== RUN   TestConcurrencyMap
fatal error: concurrent map writes

goroutine 5 [running]:
runtime.throw({0x1121440?, 0x0?})
 /go/go1.18.8/src/runtime/panic.go:992 +0x71 fp=0xc000049f78 sp=0xc000049f48 pc=0x10333b1
...

用火焰图分析下内存泄漏问题

首先,程序代码运行前,需要加这个代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import (
 "context"
 "errors"
 "fmt"
 "log"
 "net/http"
 _ "net/http/pprof"
 "runtime"
 "testing"
 "time"
)

func TestLeakOfMemory(t *testing.T) {

 //leakOfMemory()
 fmt.Println("NumGoroutine:", runtime.NumGoroutine())
 for i := 0; i < 1000; i++ {
  go leakOfMemory_1(1, 2, 3, 4, 5, 6, 7)
 }
 //done := make(chan struct{})
 //defer close(done)
 //leakOfMemory_2(done, 1, 2, 3, 4, 5, 6, 7)
 time.Sleep(3 * time.Second)
 //done <- struct{}{}
 fmt.Println("main exit...")
 fmt.Println("NumGoroutine:", runtime.NumGoroutine())
 log.Println(http.ListenAndServe("localhost:6060", nil))
}

上面的执行后,登陆网址 http://localhost:6060/debug/pprof/goroutine?debug=1,可以看到下面的页面:

但是看不到图形界面,怎么办?

需要安装 graphviz

在控制台执行如下命令

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
brew install graphviz # 安装graphviz,只需要安装一次就行了
go tool pprof -http=":8081" http://localhost:6060/debug/pprof/goroutine?debug=1

然后可以登陆网页:http://localhost:8081/ui/ 看到下图:

image.png

发现有一个程序//GoProject/main/concurrency/channel.leakOfMemory_1.func1占用 cpu 特别大. 想看下这个程序是啥?

分析协程泄漏

使用如下结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
go tool pprof http://localhost:6060/debug/pprof/goroutine

火焰图分析:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Total:总共采样次数,100次。
Flat:函数在样本中处于运行状态的次数。简单来说就是函数出现在栈顶的次数,而函数在栈顶则意味着它在使用CPU。
Flat%:Flat / Total。
Sum%:自己以及所有前面的Flat%的累积值。解读方式:表中第3行Sum% 32.4%,意思是前3个函数(运行状态)的计数占了总样本数的32.4%
Cum:函数在样本中出现的次数。只要这个函数出现在栈中那么就算进去,这个和Flat不同(必须是栈顶才能算进去)。也可以解读为这个函数的调用次数。
Cum%:Cum / Total

进入控制台,输入 top

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Type: goroutine
Time: Feb 5, 2024 at 10:02am (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 1003, 99.90% of 1004 total
Dropped 35 nodes (cum <= 5)
      flat  flat%   sum%        cum   cum%
      1003 99.90% 99.90%       1003 99.90%  runtime.gopark
         0     0% 99.90%       1000 99.60%  //GoProject/main/concurrency/channel.leakOfMemory_1.func1
         0     0% 99.90%       1000 99.60%  runtime.chansend
         0     0% 99.90%       1000 99.60%  runtime.chansend1
(pprof)

其中 其中runtime.gopark即可认为是挂起的goroutine数量。发现有大量协程被 runtime.gopark

然后输入 traces runtime.gopark

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
(pprof) traces  runtime.gopark
Type: goroutine
Time: Feb 5, 2024 at 10:02am (CST)
-----------+-------------------------------------------------------
      1000   runtime.gopark
             runtime.chansend
             runtime.chansend1
             //GoProject/main/concurrency/channel.leakOfMemory_1.func1
-----------+-------------------------------------------------------
         1   runtime.gopark
             runtime.chanrecv
             runtime.chanrecv1
             testing.(*T).Run
             testing.runTests.func1
             testing.tRunner
             testing.runTests
             testing.(*M).Run
             main.main
             runtime.main
-----------+-------------------------------------------------------
         1   runtime.gopark
             runtime.netpollblock
             internal/poll.runtime_pollWait
             internal/poll.(*pollDesc).wait
             internal/poll.(*pollDesc).waitRead (inline)
             internal/poll.(*FD).Read
             net.(*netFD).Read
             net.(*conn).Read
             net/http.(*connReader).backgroundRead
-----------+-------------------------------------------------------
         1   runtime.gopark
             runtime.netpollblock
             internal/poll.runtime_pollWait
             internal/poll.(*pollDesc).wait
             internal/poll.(*pollDesc).waitRead (inline)
             internal/poll.(*FD).Accept
             net.(*netFD).accept
             net.(*TCPListener).accept
             net.(*TCPListener).Accept
             net/http.(*Server).Serve
             net/http.(*Server).ListenAndServe
             net/http.ListenAndServe (inline)
             //GoProject/main/concurrency/channel.TestLeakOfMemory
             testing.tRunner
-----------+-------------------------------------------------------
(pprof)

可以发现泄漏了 1000 个 goruntine

然后通过调用栈,可以看到调用链路:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
channel.leakOfMemory_1.func1->runtime.chansend1->runtime.chansend->runtime.gopark

runtime.chansend1 是阻塞的调用,协程最终被 runtime.gopark 挂起,从而导致泄漏。

然后再输入 list GoProject/main/concurrency/channel. leakOfMemory_1.func1 可以看到如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
(pprof) list //GoProject/main/concurrency/channel.
leakOfMemory_1.func1
Total: 1004
ROUTINE ======================== //GoProject/main/concurrency/channel.leakOfMemory_1.func1 in /Users/bytedance/go/src///GoProject/main/concurrency/channel/channel_test.go
         0       1000 (flat, cum) 99.60% of Total
         .          .     62: out := make(chan int)
         .          .     63: // sender
         .          .     64: go func() {
         .          .     65:  defer close(out)
         .          .     66:  for _, n := range nums {
         .       1000     67:   out <- n
         .          .     68:   time.Sleep(time.Second)
         .          .     69:  }
         .          .     70: }()
         .          .     71:
         .          .     72: // receiver

可以看到使用了一个非缓冲的 channel, 上面已经分析了,没有接收者,发送者out 在写入channel 时阻塞, 协程无法退出,因此有协程泄漏。

分析内存增长泄漏

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
go tool pprof http://localhost:6060/debug/pprof/heap

然后输入 top

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
(pprof) top
Showing nodes accounting for 6662.08kB, 86.68% of 7686.14kB total
Showing top 10 nodes out of 24
      flat  flat%   sum%        cum   cum%
 5125.63kB 66.69% 66.69%  5125.63kB 66.69%  runtime.allocm
 1024.41kB 13.33% 80.01%  1024.41kB 13.33%  runtime.malg
  512.05kB  6.66% 86.68%   512.05kB  6.66%  internal/poll.runtime_Semacquire
         0     0% 86.68%   512.05kB  6.66%  GoProject/main/concurrency/channel.leakOfMemory_1.func2
         0     0% 86.68%   512.05kB  6.66%  fmt.Fprintln
         0     0% 86.68%   512.05kB  6.66%  fmt.Println (inline)
         0     0% 86.68%   512.05kB  6.66%  internal/poll.(*FD).Write
         0     0% 86.68%   512.05kB  6.66%  internal/poll.(*FD).writeLock (inline)
         0     0% 86.68%   512.05kB  6.66%  internal/poll.(*fdMutex).rwlock
         0     0% 86.68%   512.05kB  6.66%  os.(*File).Write
(pprof)

看着不是很大,达不到内存增长泄漏的级别。

参考资料

  • https://medium.com/golangspec/goroutine-leak-400063aef468
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-02-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员奇点 微信公众号,前往查看

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

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
浅谈Golang内存泄漏
内存泄漏并不是指物理上的内存消失,而是在写程序的过程中,由于程序的设计不合理导致对之前使用的内存失去控制,无法再利用这块内存区域;短期内的内存泄漏可能看不出什么影响,但是当时间长了之后,日积月累,浪费的内存越来越多,导致可用的内存空间减少,轻则影响程序性能,严重可导致正在运行的程序突然崩溃。
素履coder
2022/10/05
2.6K0
浅谈Golang内存泄漏
Goroutine泄露的危害、成因、检测与防治
Go内存泄露,相当多数都是goroutine泄露导致的。 虽然每个goroutine仅占用少量(栈)内存,但当大量goroutine被创建却不会释放时(即发生了goroutine泄露),也会消耗大量内存,造成内存泄露。
fliter
2023/06/18
1.1K0
Goroutine泄露的危害、成因、检测与防治
实战Go内存泄露
最近解决了我们项目中的一个内存泄露问题,事实再次证明pprof是一个好工具,但掌握好工具的正确用法,才能发挥好工具的威力,不然就算你手里有屠龙刀,也成不了天下第一,本文就是带你用pprof定位内存泄露问题。
大彬
2019/05/28
5.1K0
Go 笔记之如何防止 goroutine 泄露
Go 的并发模型与其他语言不同,虽说它简化了并发程序的开发难度,但如果不了解使用方法,常常会遇到 goroutine 泄露的问题。虽然 goroutine 是轻量级的线程,占用资源很少,但如果一直得不到释放并且还在不断创建新协程,毫无疑问是有问题的,并且是要在程序运行几天,甚至更长的时间才能发现的问题。
波罗学
2019/07/31
8930
从实例出发,深入理解pprof原理与应用
内存泄漏是指在计算机程序中,由于程序未能正确释放已经申请的内存空间,导致系统的可用内存持续减少,最终可能导致程序性能下降甚至崩溃的问题。
Michel_Rolle
2023/12/11
2.9K9
go的net/http有哪些值得关注的细节?
golang的net/http库是我们平时写代码中,非常常用的标准库。由于go语言拥有goroutine,goroutine的上下文切换成本比普通线程低很多,net/http库充分利用了这个优势,因此,它的内部实现跟其他语言会有一些区别。
小白debug
2023/09/01
5350
go的net/http有哪些值得关注的细节?
后台填坑记——Golang内存泄漏问题排查(一)
本文记录了Golang服务内存泄漏问题的排查过程,通过多维度工具分析和代码溯源,最终定位到Redis/SQL连接池未正确关闭导致的协程泄漏问题。文章系统性地展示了从现象分析到根因定位的全流程方法,对后台服务稳定性维护具有实践指导意义。
粲然忧生
2025/04/22
3810
后台填坑记——Golang内存泄漏问题排查(一)
Go语言的runtime包深入解析
Go语言的runtime包是Go标准库中非常重要的一部分,它包含了与Go运行时系统(包括内存分配、垃圾回收、并发调度等)相关的底层函数和数据结构。理解runtime包的工作机制,有助于开发者更好地优化Go应用程序的性能。
数字扫地僧
2024/07/01
5600
go语言协程实现原理初探
golang作为一门现代语言,有其独特之处,比如一个go func(){}()语句即可实现协程,但也存在一些让人诟病的地方,比如错误处理等等。但是想必人无完人,无物完物。我们今天聊聊golang的协程(也叫goroutine)。首先提到协程,我们会想到进程,线程,那么协程是什么呢?协程是一种用户态的线程,他可以由用户自行创建和销毁,不需要内核调度,创建和销毁不需要占用太多系统资源的用户态线程。所以通常情况下,对于大并发的业务,我们通常能创建数以万计的协程来并发处理我们的业务,而不用担心资源占用过多。所以go的协程的作用就是为了实现并发编程,它是由go自己实现的调度器实现资源调度,从而开发者不用太多关心并发实现,从而可以安心的写一些牛逼的业务代码。
金鹏
2024/02/07
7293
go语言协程实现原理初探
go的并发编程
如果了解了GMP模型之后,自然了解go的并发特点,协程之间都可能是多线程并发执行的,通过开协程就可以实现并发:
仙士可
2022/02/22
4110
go的并发编程
《快学 Go 语言》第 11 课 —— 千军万马跑协程
协程和通道是 Go 语言作为并发编程语言最为重要的特色之一,初学者可以完全将协程理解为线程,但是用起来比线程更加简单,占用的资源也更少。通常在一个进程里启动上万个线程就已经不堪重负,但是 Go 语言允许你启动百万协程也可以轻松应付。如果把协程比喻成小岛,那通道就是岛屿之间的交流桥梁,数据搭乘通道从一个协程流转到另一个协程。通道是并发安全的数据结构,它类似于内存消息队列,允许很多的协程并发对通道进行读写。
老钱
2018/12/21
9180
​Golang 并发编程指南
作者:dcguo,腾讯 CSIG 电子签开放平台中心 分享 Golang 并发基础库,扩展以及三方库的一些常见问题、使用介绍和技巧,以及对一些并发库的选择和优化探讨。 go 原生/扩展库 提倡的原则 不要通过共享内存进行通信;相反,通过通信来共享内存。 Goroutine goroutine 并发模型 调度器主要结构 主要调度器结构是 M,P,G M,内核级别线程,goroutine 基于 M 之上,代表执行者,底层线程,物理线程 P,处理器,用来执行 goroutine,因此维护了一个 gorout
腾讯技术工程官方号
2021/12/20
1.4K0
Go 语言常见的编程面试题
文章目录 启动两个协程,交替打印 123456 写一个程序,控制 Goruntine 数量 记录 Go 语言常见的编程面试题。 启动两个协程,交替打印 123456 func main() { // 创建 3 个通道,A、B 和 Exit A := make(chan bool) B := make(chan bool) Exit := make(chan bool) go func() { // 如果 A 通道是 true,则执行 for i :=
CG国斌
2022/11/28
3830
说说channel哪些事-上篇
channel中文翻译为通道,它是Go语言内置的数据类型,使用channel不需要导入任何包,像int/float一样直接使用。它主要用于goroutine之间的消息传递和事件通知。 在Go语言中流传着一句话,就是说不要通过共享内存来通信,而是应该通过通信来共享内存。
数据小冰
2022/08/15
3780
说说channel哪些事-上篇
如何控制golang协程的并发数量问题
最近搞压测,写了一个压测的工具,就是想通过go来实现一秒发多少个请求,然后我写了一段这样的代码,如下,当然压测的代码我就没有贴了,我贴了主要核心代码,主要是起一个定时器,然后通过但仅此去读定时器的通道,然后for循环起goroutine,goroutine里面是进行并发的逻辑。
公众号-利志分享
2022/04/25
2.2K0
Golang Channel 实战技巧和说明
channel 不需要通过 close 来释放资源,这个是它与 socket、file 等不一样的地方,对于 channel 而言,唯一需要 close 的就是我们想通过 close 触发 channel 读事件。
Allen.Wu
2022/11/29
7380
time.After和select搭配使用时存在的"坑"
在许多大公司代码仓库里,一搜<- time.After关键字有一大堆,而且后面的时间不少都是几分钟。
fliter
2023/06/18
3280
time.After和select搭配使用时存在的"坑"
如何在测试中发现goroutine泄漏
不知道你们在日常开发中是否有遇到过goroutine泄漏,goroutine泄漏其实就是goroutine阻塞,这些阻塞的goroutine会一直存活直到进程终结,他们占用的栈内存一直无法释放,从而导致系统的可用内存会越来越少,直至崩溃!简单总结了几种常见的泄漏原因:
Golang梦工厂
2022/07/11
4680
修改golang源代码实现无竞争版ThreadLocal
书接上文 修改golang源代码获取goroutine id实现ThreadLocal。上文实现的版本由于map是多个goroutine共享的,存在竞争,影响了性能,实现思路类似java初期的ThreadLocal,今天我们借鉴现代版java的ThreadLocal来实现。
左手java右手go
2019/08/20
1K0
修改golang源代码实现无竞争版ThreadLocal
Golang pprof 性能问题分析优化和实战经验
Go 自带了一个 pprof 的性能优化和分析的工具,这个工具包括 cpuprof 、memprof ,并且还提供了 Lookup 功能用于获取堆状态信息、线程状态信息、 goroutine 状态信息等。官方的博客有一篇文章介绍用法:《Profiling Go Programs》[1]
Allen.Wu
2023/03/01
2.9K0
Golang pprof 性能问题分析优化和实战经验
相关推荐
浅谈Golang内存泄漏
更多 >
LV.2
中金支付高级研发工程师
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验