一,什么是内存泄漏
Go 中的并发性是以 goroutine(独立活动)和 channel(用于通信)的形式实现的。处理 goroutine 时,程序员需要小心翼翼地避免泄露。如果最终永远堵塞在 I/O 上(例如 channel 通信),或者陷入死循环,那么 goroutine 会发生泄露。即使是阻塞的 goroutine,也会消耗资源,因此,程序可能会使用比实际需要更多的内存,或者最终耗尽内存,从而导致崩溃。虽然我们知道goroutine在初始化的时候会会分配一个2kb的栈地址空间,(关于内存分配的问题可以参考http://ifeve.com/memory-barriers-or-fences/)但是如果大量的goroutine被阻塞,造成的内存浪费也是客观的。
我们知道go采用的gc是标记回收法:
从根变量来时遍历所有被引用对象,标记之后进行清除操作,对未标记对象进行回收。
阻塞状态是go调度的一个待唤醒的状态,是不能被gc的。
我们知道:
如向channel发送数据的时候,该goroutine会一直阻塞直到另一个goroutine接受该channel的数据,反之亦然,goroutine接受channel的数据的时候也会一直阻塞直到另一个goroutine向该channel发送数据
发送端的channel满了,那么发送端将处于阻塞状态知道里面的消息被消费
ch := make(chan int)
go func() {
ch <- 1
fmt.Println(111)
}()
接受端消费channel的时候发现为空
ch := make(chan int, 1)
go func() {
<-ch
fmt.Println(111)
}()
上述的两种中状态就是最简单的goroutine阻塞的情况,我们遇到goroutine阻塞,从而导致的内存泄漏的情况无非就是这样的。
内存泄漏是如何产生的呢?
1、发送一个没有接受者的channel
func query() int {
n := rand.Intn(100)
time.Sleep(time.Duration(n) * time.Millisecond)
return n
}
func queryAll() int {
ch := make(chan int)
go func() { ch <- query() }()
go func() { ch <- query() }()
go func() { ch <- query() }()
return <-ch
}
func main() {
for i := 0; i < 4; i++ {
queryAll()
fmt.Printf("#goroutines: %d", runtime.NumGoroutine())
}
}
输出:
#goroutines: 3
#goroutines: 5
#goroutines: 7
#goroutines: 9
每次调用 queryAll 后,goroutine 的数目会发生增长。问题在于,在接收到第一个响应后,“较慢的” goroutine 将会发送到另一端没有接收者的 channel 中。
2、nil channel
写入到nil channel会永远阻塞
package main
func main() {
var ch chan struct{}
ch <- struct{}{}
}
所以它导致死锁:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send (nil chan)]:
main.main()
...
当从 nil channel 读取数据时,同样的事情发生了:
var ch chan struct{}
<-ch
传递尚未初始化的channel,也可能发生
func main() {
var ch chan int
if false {
ch = make(chan int, 1)
ch <- 1
}
go func(ch chan int) {
<-ch
}(ch)
c := time.Tick(1 * time.Second)
for range c {
fmt.Printf("#goroutines: %d", runtime.NumGoroutine())
}
}
3、channel通讯超时
如果goroutine在通讯的时候,发送端的channel由于某种原因没有到达消费端的goroutine,那么下游消费的goroutine就会长时间处于阻塞的状态等待消息的唤醒。
/*
检查channel读写超时,并做超时的处理
*/
func testTimeout() {
g := make(chan int)
quit := make(chan bool)
go func() {
for {
select {
case v := <-g:
fmt.Println(v)
case <-time.After(time.Second * time.Duration(3)):
quit <- true
fmt.Println("超时,通知主线程退出")
return
}
}
}()
for i := 0; i < 3; i++ {
g <- i
}
<-quit
fmt.Println("收到退出通知,主线程退出")
}
二,如何排查内存的泄漏
我们可以使用pprof进行排查
那我们就来了解下pprof的基本知识点
什么是pprof
pprof是Go的性能分析工具,在程序运行过程中,可以记录程序的运行信息,可以是CPU使用情况、内存使用情况、goroutine运行情况等,当需要性能调优或者定位Bug时候,这些记录的信息是相当重要。
基本使用
使用pprof有多种方式,Go已经现成封装好了1个:net/http/pprof,使用简单的几行命令,就可以开启pprof,记录运行信息,并且提供了Web服务,能够通过浏览器和命令行2种方式获取运行数据。
import (
"fmt"
"net/http"
_ "net/http/pprof"
)
func main() {
// 开启pprof,监听请求
ip := "127.0.0.1:6060"
if err := http.ListenAndServe(ip, nil); err != nil {
fmt.Printf("start pprof failed on %s\n", ip)
}
}
我们输入ip:port/debug/pprof/打开pprof主页
例如我的地址
http://127.0.0.1:6060/debug/pprof/
会看到下面的信息
下面来分析一下上面的具体参数的意思
allocs:
A sampling of all past memory allocations
所有过去内存分配的采样
block:
Stack traces that led to blocking on synchronization primitives
导致同步原语阻塞的堆栈跟踪
cmdline:
The command line invocation of the current program
当前程序的命令行调用
goroutine:
Stack traces of all current goroutines
heap:
A sampling of memory allocations of live objects. You can specify the gc GET parameter to run GC before taking the heap sample.
活动对象内存分配的采样。在获取堆样本之前,可以指定gc get参数来运行gc。(也就是堆内存的信息)
mutex:
Stack traces of holders of contended mutexes
争用互斥锁持有者的堆栈跟踪(锁的信息)
profile:
CPU profile. You can specify the duration in the seconds GET parameter. After you get the profile file, use the go tool pprof command to investigate the profile.
CPU配置文件。可以在seconds get参数中指定持续时间。获取配置文件后,使用go tool pprof命令调查配置文件。
threadcreate:
Stack traces that led to the creation of new OS threads
导致创建新操作系统线程的堆栈跟踪(线程的信息)
trace:
A trace of execution of the current program. You can specify the duration in the seconds GET parameter. After you get the trace file, use the go tool trace command to investigate the trace.
对当前程序执行的跟踪。可以在seconds get参数中指定持续时间。获取跟踪文件后,使用go tool trace命令调查跟踪。
命令行方式
当连接在服务器终端上的时候,是没有浏览器可以使用的,Go提供了命令行的方式,能够获取以上5类信息,这种方式用起来更方便。
使用命令go tool pprof url可以获取指定的profile文件,此命令会发起http请求,然后下载数据到本地,之后进入交互式模式,就像gdb一样,可以使用命令查看运行信息,以下是5类请求的方式:
# 下载cpu profile,默认从当前开始收集30s的cpu使用情况,需要等待30s
go tool pprof http://localhost:6060/debug/pprof/profile # 30-second CPU profile
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=120 # wait 120s
# 下载heap profile
go tool pprof http://localhost:6060/debug/pprof/heap # heap profile
# 下载goroutine profile
go tool pprof http://localhost:6060/debug/pprof/goroutine # goroutine profile
# 下载block profile
go tool pprof http://localhost:6060/debug/pprof/block # goroutine blocking profile
# 下载mutex profile
go tool pprof http://localhost:6060/debug/pprof/mutex
内存泄露的发现
如果使用云平台部署Go程序,云平台都提供了内存查看的工具,可以查看OS的内存占用情况和某个进程的内存占用情况,比如阿里云,我们在1个云主机上只部署了1个Go服务,所以OS的内存占用情况,基本是也反映了进程内存占用情况,OS内存占用情况如下,可以看到随着时间的推进,内存的占用率在不断的提高,这是内存泄露的最明显现象:
怎么用heap发现内存问题
使用pprof的heap能够获取程序运行时的内存信息,在程序平稳运行的情况下,每个一段时间使用heap获取内存的profile,然后使用base能够对比两个profile文件的差别,就像diff命令一样显示出增加和减少的变化,使用一个简单的demo来说明heap和base的使用,依然使用demo2进行展示。
// 展示内存增长和pprof,并不是泄露
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
"os"
"time"
)
// 运行一段时间:fatal error: runtime: out of memory
func main() {
// 开启pprof
go func() {
ip := "0.0.0.0:6060"
if err := http.ListenAndServe(ip, nil); err != nil {
fmt.Printf("start pprof failed on %s\n", ip)
os.Exit(1)
}
}()
tick := time.Tick(time.Second / 100)
var buf []byte
for range tick {
buf = append(buf, make([]byte, 1024*1024)...)
}
}
将上面代码运行起来,执行以下命令获取profile文件,1分钟后再获取1次。
go tool pprof http://localhost:6060/debug/pprof/heap
我已经获取到了两个profile文件:
Administrator@SC-201807230940 MINGW64 ~/pprof
$ ls
pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz
pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.002.pb.gz
使用base把001文件作为基准,然后用002和001对比,先执行top看top的对比,然后执行list main列出main函数的内存对比,结果如下:
Administrator@SC-201807230940 MINGW64 ~/pprof
$ go tool pprof -base pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.002.pb.gz
结果
(pprof) top
Showing nodes accounting for 1.04GB, 50.58% of 2.06GB total
flat flat% sum% cum cum%
1.04GB 50.58% 50.58% 1.04GB 50.58% main.main
0 0% 50.58% 1.04GB 50.58% runtime.main
(pprof)
使用traces
Type: inuse_space
Time: Jul 29, 2019 at 8:48am (CST)
-----------+-------------------------------------------------------
bytes: 1.55GB
1.55GB main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 1.24GB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 1016.83MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 813.46MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 902.59kB
0 compress/flate.NewWriter
compress/gzip.(*Writer).Write
runtime/pprof.(*profileBuilder).build
runtime/pprof.writeHeapProto
runtime/pprof.writeHeap
runtime/pprof.(*Profile).WriteTo
net/http/pprof.handler.ServeHTTP
net/http/pprof.Index
net/http.HandlerFunc.ServeHTTP
net/http.(*ServeMux).ServeHTTP
net/http.serverHandler.ServeHTTP
net/http.(*conn).serve
-----------+-------------------------------------------------------
bytes: 650.77MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 520.61MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 416.48MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 333.19MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 266.55MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 213.23MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 170.59MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 136.47MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 109.17MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 87.34MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 69.87MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 55.89MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 44.71MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 35.77MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 28.61MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 22.88MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 18.30MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 14.64MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 11.71MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 9.37MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 7.49MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 5.99MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 4.79MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 3.07MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 2.46MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 1.16MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 1MB
1.16MB main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 520.61MB
-520.61MB main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 416.48MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 333.19MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 266.55MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 213.23MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 170.59MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 136.47MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 109.17MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 87.34MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 69.87MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 55.89MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 44.71MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 35.77MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 28.61MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 22.88MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 18.30MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 14.64MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 11.71MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 9.37MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 7.49MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 5.99MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 4.79MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 3.07MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 2.46MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 1.16MB
0 main.main
runtime.main
-----------+-------------------------------------------------------
bytes: 1MB
-1.16MB main.main
runtime.main
-----------+-------------------------------------------------------
(pprof)
使用list
(pprof) list main.main
Total: 2.06GB
ROUTINE ======================== main.main in D:\gowork\src\study\main\main.go
1.04GB 1.04GB (flat, cum) 50.58% of Total
. . 57: }()
. . 58:
. . 59: tick := time.Tick(time.Second / 100)
. . 60: var buf []byte
. . 61: for range tick {
1.04GB 1.04GB 62: buf = append(buf, make([]byte, 1024*1024)...)
. . 63: }
. . 64:}
. . 65:
. . 66:
. . 67:
(pprof)
heap“不能”定位内存泄露
heap能显示内存的分配情况,以及哪行代码占用了多少内存,我们能轻易的找到占用内存最多的地方,如果这个地方的数值还在不断怎大,基本可以认定这里就是内存泄露的位置。
曾想按图索骥,从内存泄露的位置,根据调用栈向上查找,总能找到内存泄露的原因,这种方案看起来是不错的,但实施起来却找不到内存泄露的原因,结果是事半功倍。
原因在于一个Go程序,其中有大量的goroutine,这其中的调用关系也许有点复杂,也许内存泄露是在某个三方包里。举个栗子,比如下面这幅图,每个椭圆代表1个goroutine,其中的数字为编号,箭头代表调用关系。heap profile显示g111(最下方标红节点)这个协程的代码出现了泄露,任何一个从g101到g111的调用路径都可能造成了g111的内存泄露,有2类可能:
该goroutine只调用了少数几次,但消耗了大量的内存,说明每个goroutine调用都消耗了不少内存,内存泄露的原因基本就在该协程内部。
该goroutine的调用次数非常多,虽然每个协程调用过程中消耗的内存不多,但该调用路径上,协程数量巨大,造成消耗大量的内存,并且这些goroutine由于某种原因无法退出,占用的内存不会释放,内存泄露的原因在到g111调用路径上某段代码实现有问题,造成创建了大量的g111。
第2种情况,就是goroutine泄露,这是通过heap无法发现的,所以heap在定位内存泄露这件事上,发挥的作用不大。
内存泄露的排查
Web可视化查看
Web方式适合web服务器的端口能访问的情况,使用起来方便,有2种方式:
查看某条调用路径上,当前阻塞在此goroutine的数量
查看所有goroutine的运行栈(调用路径),可以显示阻塞在此的时间
import (
"fmt"
"net/http"
_ "net/http/pprof"
"os"
"time"
)
func main() {
// 开启pprof
go func() {
ip := "0.0.0.0:6060"
if err := http.ListenAndServe(ip, nil); err != nil {
fmt.Printf("start pprof failed on %s\n", ip)
os.Exit(1)
}
}()
outCh := make(chan int)
for i := 1; i <= 5; i++ {
go func() {
outCh <- 1
}()
time.Sleep(time.Second)
}
///value := <-outCh
//fmt.Println("value : ", value)
//time
time.Sleep(100 * time.Second)
}
方式一
url请求中设置debug=1:
使用http://127.0.0.1:6060/debug/pprof/goroutine?debug=1
我们可以明显的看到有5个goroutine被阻塞了
其实应该是5个地方主goroutine应为时间也是被阻塞掉了
我们看到有5个goroutine被同一个资源阻塞掉了,并且指向的代码块是23行,那么我们就能很快的进行定位排查。
方式二
url请求中设置debug=2:
使用:http://127.0.0.1:6060/debug/pprof/goroutine?debug=2
我们可以看到阻塞的时间,同时也能看到阻塞的代码块
命令行交互式方法
top会列出5个统计数据:
flat: 本函数占用的内存量。
flat%: 本函数内存占使用中内存总量的百分比。
sum%: 前面每一行flat百分比的和,比如第2行虽然的100% 是 100% + 0%。
cum: 是累计量,加入main函数调用了函数f,函数f占用的内存量,也会记进来。
cum%: 是累计量占总量的百分比。
list
查看某个函数的代码,以及该函数每行代码的指标信息,如果函数名不明确,会进行模糊匹配,比如list main会列出main.main和runtime.main。
traces
打印所有调用栈,以及调用栈的指标信息。
下面是具体的排查流程
1、使用top
$ go tool pprof http://0.0.0.0:6060/debug/pprof/goroutine
Fetching profile over HTTP from http://0.0.0.0:6060/debug/pprof/goroutine
Saved profile in C:\Users\Administrator\pprof\pprof.goroutine.006.pb.gz
Type: goroutine
Time: Jul 29, 2019 at 8:06am (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Unrecognized command: "\x1b[A\x1b[Btop"
(pprof) top
Showing nodes accounting for 9, 100% of 9 total
Showing top 10 nodes out of 32
flat flat% sum% cum cum%
7 77.78% 77.78% 7 77.78% runtime.gopark
1 11.11% 88.89% 1 11.11% net/http.(*connReader).backgroundRead
1 11.11% 100% 1 11.11% runtime/pprof.writeRuntimeProfile
0 0% 100% 1 11.11% internal/poll.(*FD).Accept
0 0% 100% 1 11.11% internal/poll.(*FD).acceptOne
0 0% 100% 1 11.11% internal/poll.(*ioSrv).ExecIO
0 0% 100% 1 11.11% internal/poll.(*pollDesc).wait
0 0% 100% 1 11.11% internal/poll.runtime_pollWait
0 0% 100% 1 11.11% main.main
0 0% 100% 1 11.11% main.main.func1
(pprof)
我们可以看到有7个被阻塞了
我们通过traces打印出具体的调用链路
(pprof) traces
Type: goroutine
Time: Jul 29, 2019 at 8:06am (CST)
-----------+-------------------------------------------------------
5 runtime.gopark
runtime.goparkunlock
runtime.chansend
runtime.chansend1
main.main.func2
-----------+-------------------------------------------------------
1 runtime.gopark
runtime.netpollblock
internal/poll.runtime_pollWait
internal/poll.(*pollDesc).wait
internal/poll.(*ioSrv).ExecIO
internal/poll.(*FD).acceptOne
internal/poll.(*FD).Accept
net.(*netFD).accept
net.(*TCPListener).accept
net.(*TCPListener).AcceptTCP
net/http.tcpKeepAliveListener.Accept
net/http.(*Server).Serve
net/http.(*Server).ListenAndServe
net/http.ListenAndServe
main.main.func1
-----------+-------------------------------------------------------
1 runtime.gopark
runtime.goparkunlock
time.Sleep
main.main
runtime.main
-----------+-------------------------------------------------------
1 net/http.(*connReader).backgroundRead
-----------+-------------------------------------------------------
1 runtime/pprof.writeRuntimeProfile
runtime/pprof.writeGoroutine
runtime/pprof.(*Profile).WriteTo
net/http/pprof.handler.ServeHTTP
net/http/pprof.Index
net/http.HandlerFunc.ServeHTTP
net/http.(*ServeMux).ServeHTTP
net/http.serverHandler.ServeHTTP
net/http.(*conn).serve
-----------+-------------------------------------------------------
(pprof)
我们可以看到5个被阻塞到了 main.main.func2
然后我们可以使用list查看具体代码的阻塞
(pprof) list main.main.func2
Total: 9
ROUTINE ======================== main.main.func2 in D:\gowork\src\study\main\main.go
0 5 (flat, cum) 55.56% of Total
. . 29: }
. . 30: }()
. . 31: outCh := make(chan int)
. . 32: for i := 1; i <= 5; i++ {
. . 33: go func() {
. 5 34: outCh <- 1
. . 35: }()
. . 36: time.Sleep(time.Second)
. . 37: }
. . 38:
. . 39: ///value := <-outCh
(pprof)
我们可以看到已经将我们的代码阻塞块给打印出来了,我们就能很好的进行排查了。
参考:https://studygolang.com/articles/20529