最近解决了咱们项目中的一个内存泄露问题,事实再次证实pprof是一个好工具,但掌握好工具的正确用法,才能发挥好工具的威力,否则就算你手里有屠龙刀,也成不了天下第一,本文就是带你用pprof定位内存泄露问题。html
关于Go的内存泄露有这么一句话不知道你听过没有:node
10次内存泄露,有9次是goroutine泄露。
我所解决的问题,也是goroutine泄露致使的内存泄露,因此这篇文章主要介绍Go程序的goroutine泄露,掌握了如何定位和解决goroutine泄露,就掌握了内存泄露的大部分场景。git
本文草稿最初数据都是生产坏境数据,为了防止敏感内容泄露,所有替换成了demo数据,demo的数据比生产环境数据简单多了,更适合入门理解,有助于掌握pprof。
定位goroutine泄露会使用到pprof,pprof是Go的性能工具,在开始介绍内存泄露前,先简单介绍下pprof的基本使用,更详细的使用给你们推荐了资料。github
pprof是Go的性能分析工具,在程序运行过程当中,能够记录程序的运行信息,能够是CPU使用状况、内存使用状况、goroutine运行状况等,当须要性能调优或者定位Bug时候,这些记录的信息是至关重要。golang
使用pprof有多种方式,Go已经现成封装好了1个:net/http/pprof
,使用简单的几行命令,就能够开启pprof,记录运行信息,而且提供了Web服务,可以经过浏览器和命令行2种方式获取运行数据。web
看个最简单的pprof的例子:编程
文件:golang_step_by_step/pprof/pprof/demo.goubuntu
package main import ( "fmt" "net/http" _ "net/http/pprof" ) func main() { // 开启pprof,监听请求 ip := "0.0.0.0:6060" if err := http.ListenAndServe(ip, nil); err != nil { fmt.Printf("start pprof failed on %s\n", ip) } }
提醒:本文全部代码部分可左右滑动segmentfault
输入网址ip:port/debug/pprof/
打开pprof主页,从上到下依次是5类profile信息:浏览器
full goroutine stack dump
是输出全部goroutine的调用栈,是goroutine的debug=2,后面会详细介绍。这篇文章咱们主要关注goroutine和heap,这两个都会打印调用栈信息,goroutine里面还会包含goroutine的数量信息,heap则是内存分配信息,本文用不到的地方就不展现了,最后推荐几篇文章你们去看。
当链接在服务器终端上的时候,是没有浏览器可使用的,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
上面的pprof/demo.go
太简单了,若是去获取内存profile,几乎获取不到什么,换一个Demo进行内存profile的展现:
文件:golang_step_by_step/pprof/heap/demo2.go
// 展现内存增加和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)...) } }
上面这个demo会不断的申请内存,把它编译运行起来,而后执行:
$ go tool pprof http://localhost:6060/debug/pprof/heap Fetching profile over HTTP from http://localhost:6060/debug/pprof/heap Saved profile in /home/ubuntu/pprof/pprof.demo.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz //<--- 下载到的内存profile文件 File: demo // 程序名称 Build ID: a9069a125ee9c0df3713b2149ca859e8d4d11d5a Type: inuse_space Time: May 16, 2019 at 8:55pm (CST) Entering interactive mode (type "help" for commands, "o" for options) (pprof) (pprof) (pprof) help // 使用help打印全部可用命令 Commands: callgrind Outputs a graph in callgrind format comments Output all profile comments disasm Output assembly listings annotated with samples dot Outputs a graph in DOT format eog Visualize graph through eog evince Visualize graph through evince gif Outputs a graph image in GIF format gv Visualize graph through gv kcachegrind Visualize report in KCachegrind list Output annotated source for functions matching regexp pdf Outputs a graph in PDF format peek Output callers/callees of functions matching regexp png Outputs a graph image in PNG format proto Outputs the profile in compressed protobuf format ps Outputs a graph in PS format raw Outputs a text representation of the raw profile svg Outputs a graph in SVG format tags Outputs all tags in the profile text Outputs top entries in text form top Outputs top entries in text form topproto Outputs top entries in compressed protobuf format traces Outputs all profile samples in text form tree Outputs a text rendering of call graph web Visualize graph through web browser weblist Display annotated source in a web browser o/options List options and their current values quit/exit/^D Exit pprof ....
以上信息咱们只关注2个地方:
/home/ubuntu/pprof/pprof.demo.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz
,这其中包含了程序名demo
,profile类型alloc
已分配的内存,inuse
表明使用中的内存。help
能够获取帮助,最早会列出支持的命令,想掌握pprof,要多看看,多尝试。关于命令,本文只会用到3个,我认为也是最经常使用的:top
、list
、traces
,分别介绍一下。
按指标大小列出前10个函数,好比内存是按内存占用多少,CPU是按执行时间多少。
(pprof) top Showing nodes accounting for 814.62MB, 100% of 814.62MB total flat flat% sum% cum cum% 814.62MB 100% 100% 814.62MB 100% main.main 0 0% 100% 814.62MB 100% runtime.main
top会列出5个统计数据:
查看某个函数的代码,以及该函数每行代码的指标信息,若是函数名不明确,会进行模糊匹配,好比list main
会列出main.main
和runtime.main
。
(pprof) list main.main // 精确列出函数 Total: 814.62MB ROUTINE ======================== main.main in /home/ubuntu/heap/demo2.go 814.62MB 814.62MB (flat, cum) 100% of Total . . 20: }() . . 21: . . 22: tick := time.Tick(time.Second / 100) . . 23: var buf []byte . . 24: for range tick { 814.62MB 814.62MB 25: buf = append(buf, make([]byte, 1024*1024)...) . . 26: } . . 27:} . . 28: (pprof) list main // 匹配全部函数名带main的函数 Total: 814.62MB ROUTINE ======================== main.main in /home/ubuntu/heap/demo2.go 814.62MB 814.62MB (flat, cum) 100% of Total . . 20: }() . . 21: ..... // 省略几行 . . 28: ROUTINE ======================== runtime.main in /usr/lib/go-1.10/src/runtime/proc.go 0 814.62MB (flat, cum) 100% of Total . . 193: // A program compiled with -buildmode=c-archive or c-shared ..... // 省略几行
能够看到在main.main
中的第25行占用了814.62MB内存,左右2个数据分别是flat和cum,含义和top中解释的同样。
打印全部调用栈,以及调用栈的指标信息。
(pprof) traces File: demo2 Type: inuse_space Time: May 16, 2019 at 7:08pm (CST) -----------+------------------------------------------------------- bytes: 813.46MB 813.46MB main.main runtime.main -----------+------------------------------------------------------- bytes: 650.77MB 0 main.main runtime.main ....... // 省略几十行
每一个- - - - -
隔开的是一个调用栈,能看到runtime.main
调用了main.main
,而且main.main
中占用了813.46MB内存。
其余的profile操做和内存是相似的,这里就不展现了。
这里只是简单介绍本文用到的pprof的功能,pprof功能很强大,也常常和benchmark结合起来,但这不是本文的重点,因此就很少介绍了,为你们推荐几篇文章,必定要好好研读、实践:
内存泄露指的是程序运行过程当中已再也不使用的内存,没有被释放掉,致使这些内存没法被使用,直到程序结束这些内存才被释放的问题。
Go虽然有GC来回收再也不使用的堆内存,减轻了开发人员对内存的管理负担,但这并不意味着Go程序再也不有内存泄露问题。在Go程序中,若是没有Go语言的编程思惟,也不遵照良好的编程实践,就可能埋下隐患,形成内存泄露问题。
在Go中发现内存泄露有2种方法,一个是通用的监控工具,另外一个是go pprof:
这2种方式分别介绍一下。
若是使用云平台部署Go程序,云平台都提供了内存查看的工具,能够查看OS的内存占用状况和某个进程的内存占用状况,好比阿里云,咱们在1个云主机上只部署了1个Go服务,因此OS的内存占用状况,基本是也反映了进程内存占用状况,OS内存占用状况以下,能够看到随着时间的推动,内存的占用率在不断的提升,这是内存泄露的最明显现象:
若是没有云平台这种内存监控工具,能够制做一个简单的内存记录工具。
一、创建一个脚本prog_mem.sh
,获取进程占用的物理内存状况,脚本内容以下:
#!/bin/bash prog_name="your_programe_name" prog_mem=$(pidstat -r -u -h -C $prog_name |awk 'NR==4{print $12}') time=$(date "+%Y-%m-%d %H:%M:%S") echo $time"\tmemory(Byte)\t"$prog_mem >>~/record/prog_mem.log
二、而后使用crontab
创建定时任务,每分钟记录1次。使用crontab -e
编辑crontab配置,在最后增长1行:
*/1 * * * * ~/record/prog_mem.sh
脚本输出的内容保存在prog_mem.log
,只要大致浏览一下就能够发现内存的增加状况,判断是否存在内存泄露。若是须要可视化,能够直接黏贴prog_mem.log
内容到Excel等表格工具,绘制内存占用图。
有情提醒:若是对pprof不了解,能够先看 go pprof基本知识,这是下一节,看完再倒回来看。
若是你Google或者百度,Go程序内存泄露的文章,它总会告诉你使用pprof heap,可以生成漂亮的调用路径图,火焰图等等,而后你根据调用路径就能定位内存泄露问题,我最初也是对此深信不疑,尝试了若干天后,只是发现内存泄露跟某种场景有关,根本找不到内存泄露的根源,若是哪位朋友用heap就能定位内存泄露的线上问题,麻烦介绍下。
后来读了Dave的《High Performance Go Workshop》,刷新了对heap的认识,内存pprof的简要内容以下:
Dave讲了如下几点:
基于目前对heap的认知,我有2个观点:
接下来,我介绍怎么用heap发现问题,而后再解释为何heap几乎不能定位内存泄露的根因。
使用pprof的heap可以获取程序运行时的内存信息,在程序平稳运行的状况下,每一个一段时间使用heap获取内存的profile,而后使用base
可以对比两个profile文件的差异,就像diff
命令同样显示出增长和减小的变化,使用一个简单的demo来讲明heap和base的使用,依然使用demo2进行展现。
文件:golang_step_by_step/pprof/heap/demo2.go
// 展现内存增加和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文件,Ctrl-D退出,1分钟后再获取1次。
go tool pprof http://localhost:6060/debug/pprof/heap
我已经获取到了两个profile文件:
$ ls pprof.demo2.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz pprof.demo2.alloc_objects.alloc_space.inuse_objects.inuse_space.002.pb.gz
使用base
把001文件做为基准,而后用002和001对比,先执行top
看top
的对比,而后执行list main
列出main
函数的内存对比,结果以下:
$ go tool pprof -base pprof.demo2.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz pprof.demo2.alloc_objects.alloc_space.inuse_objects.inuse_space.002.pb.gz File: demo2 Type: inuse_space Time: May 14, 2019 at 2:33pm (CST) Entering interactive mode (type "help" for commands, "o" for options) (pprof) (pprof) (pprof) top Showing nodes accounting for 970.34MB, 32.30% of 3003.99MB total flat flat% sum% cum cum% 970.34MB 32.30% 32.30% 970.34MB 32.30% main.main // 看这 0 0% 32.30% 970.34MB 32.30% runtime.main (pprof) (pprof) (pprof) list main.main Total: 2.93GB ROUTINE ======================== main.main in /home/ubuntu/heap/demo2.go 970.34MB 970.34MB (flat, cum) 32.30% of Total . . 20: }() . . 21: . . 22: tick := time.Tick(time.Second / 100) . . 23: var buf []byte . . 24: for range tick { 970.34MB 970.34MB 25: buf = append(buf, make([]byte, 1024*1024)...) // 看这 . . 26: } . . 27:} . . 28:
top
列出了main.main
和runtime.main
,main.main
就是咱们编写的main函数,runtime.main
是runtime包中的main函数,也就是全部main函数的入口,这里很少介绍了,有兴趣能够看以前的调度器文章《Go调度器系列(2)宏观看调度器》。
top
显示main.main
第2次内存占用,比第1次内存占用多了970.34MB。
list main.main
告诉了咱们增加的内存都在这一行:
buf = append(buf, make([]byte, 1024*1024)...)
001和002 profile的文件不进去看了,你本地测试下计算差值,绝对是刚才对比出的970.34MB。
heap能显示内存的分配状况,以及哪行代码占用了多少内存,咱们能轻易的找到占用内存最多的地方,若是这个地方的数值还在不断怎大,基本能够认定这里就是内存泄露的位置。
曾想按图索骥,从内存泄露的位置,根据调用栈向上查找,总能找到内存泄露的缘由,这种方案看起来是不错的,但实施起来却找不到内存泄露的缘由,结果是事半功倍。
缘由在于一个Go程序,其中有大量的goroutine,这其中的调用关系也许有点复杂,也许内存泄露是在某个三方包里。举个栗子,好比下面这幅图,每一个椭圆表明1个goroutine,其中的数字为编号,箭头表明调用关系。heap profile显示g111(最下方标红节点)这个协程的代码出现了泄露,任何一个从g101到g111的调用路径均可能形成了g111的内存泄露,有2类可能:
第2种状况,就是goroutine泄露,这是经过heap没法发现的,因此heap在定位内存泄露这件事上,发挥的做用不大。
若是你启动了1个goroutine,但并无符合预期的退出,直到程序结束,此goroutine才退出,这种状况就是goroutine泄露。
提早思考:什么会致使goroutine没法退出/阻塞?
每一个goroutine占用2KB内存,泄露1百万goroutine至少泄露2KB * 1000000 = 2GB
内存,为何说至少呢?
goroutine执行过程当中还存在一些变量,若是这些变量指向堆内存中的内存,GC会认为这些内存仍在使用,不会对其进行回收,这些内存谁都没法使用,形成了内存泄露。
因此goroutine泄露有2种方式形成内存泄露:
Dave在文章中也提到了,若是不知道什么时候中止一个goroutine,这个goroutine就是潜在的内存泄露:
7.1.1 Know when to stop a goroutineIf you don’t know the answer, that’s a potential memory leak as the goroutine will pin its stack’s memory on the heap, as well as any heap allocated variables reachable from the stack.
掌握了前面的pprof命令行的基本用法,很快就能够确认是不是goroutine泄露致使内存泄露,若是你不记得了,立刻回去看一下go pprof基本知识。
判断依据:在节点正常运行的状况下,隔一段时间获取goroutine的数量,若是后面获取的那次,某些goroutine比前一次多,若是多获取几回,是持续增加的,就极有多是goroutine泄露。
goroutine致使内存泄露的demo:
文件:golang_step_by_step/pprof/goroutine/leak_demo1.go
// goroutine泄露致使内存泄露 package main 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) // 死代码,永不读取 go func() { if false { <-outCh } select {} }() // 每s起100个goroutine,goroutine会阻塞,不释放内存 tick := time.Tick(time.Second / 100) i := 0 for range tick { i++ fmt.Println(i) alloc1(outCh) } } func alloc1(outCh chan<- int) { go alloc2(outCh) } func alloc2(outCh chan<- int) { func() { defer fmt.Println("alloc-fm exit") // 分配内存,假用一下 buf := make([]byte, 1024*1024*10) _ = len(buf) fmt.Println("alloc done") outCh <- 0 // 53行 }() }
编译并运行以上代码,而后使用go tool pprof
获取gorourine的profile文件。
go tool pprof http://localhost:6060/debug/pprof/goroutine
已经经过pprof命令获取了2个goroutine的profile文件:
$ ls /home/ubuntu/pprof/pprof.leak_demo.goroutine.001.pb.gz /home/ubuntu/pprof/pprof.leak_demo.goroutine.002.pb.gz
同heap同样,咱们可使用base
对比2个goroutine profile文件:
$go tool pprof -base pprof.leak_demo.goroutine.001.pb.gz pprof.leak_demo.goroutine.002.pb.gz File: leak_demo Type: goroutine Time: May 16, 2019 at 2:44pm (CST) Entering interactive mode (type "help" for commands, "o" for options) (pprof) (pprof) top Showing nodes accounting for 20312, 100% of 20312 total flat flat% sum% cum cum% 20312 100% 100% 20312 100% runtime.gopark 0 0% 100% 20312 100% main.alloc2 0 0% 100% 20312 100% main.alloc2.func1 0 0% 100% 20312 100% runtime.chansend 0 0% 100% 20312 100% runtime.chansend1 0 0% 100% 20312 100% runtime.goparkunlock (pprof)
能够看到运行到runtime.gopark
的goroutine数量增长了20312个。再经过002文件,看一眼执行到gopark
的goroutine数量,即挂起的goroutine数量:
go tool pprof pprof.leak_demo.goroutine.002.pb.gz File: leak_demo Type: goroutine Time: May 16, 2019 at 2:47pm (CST) Entering interactive mode (type "help" for commands, "o" for options) (pprof) top Showing nodes accounting for 24330, 100% of 24331 total Dropped 32 nodes (cum <= 121) flat flat% sum% cum cum% 24330 100% 100% 24330 100% runtime.gopark 0 0% 100% 24326 100% main.alloc2 0 0% 100% 24326 100% main.alloc2.func1 0 0% 100% 24326 100% runtime.chansend 0 0% 100% 24326 100% runtime.chansend1 0 0% 100% 24327 100% runtime.goparkunlock
显示有24330个goroutine被挂起,这不是goroutine泄露这是啥?已经能肯定八九成goroutine泄露了。
是什么致使如此多的goroutine被挂起而没法退出?接下来就看怎么定位goroutine泄露。
使用pprof有2种方式,一种是web网页,一种是go tool pprof
命令行交互,这两种方法查看goroutine都支持,但有轻微不一样,也有各自的优缺点。
咱们先看Web的方式,再看命令行交互的方式,这两种都很好使用,结合起来用也不错。
Web方式适合web服务器的端口能访问的状况,使用起来方便,有2种方式:
url请求中设置debug=1:
http://ip:port/debug/pprof/goroutine?debug=1
效果以下:
看起来密密麻麻的,其实简单又十分有用,看上图标出来的部分,手机上图看起来可能不方便,那就放大图片,或直接看下面各字段的含义:
goroutine profile: total 32023
:32023是goroutine的总数量,32015 @ 0x42e15a 0x42e20e 0x40534b 0x4050e5 ...
:32015表明当前有32015个goroutine运行这个调用栈,而且停在相同位置,@后面的十六进制,如今用不到这个数据,因此暂不深究了。32015 @ 0x42e15a 0x42e20e 0x40534b 0x4050e5 0x6d8559 0x6d831b 0x45abe1 # 0x6d8558 main.alloc2.func1+0xf8 /home/ubuntu/heap/leak_demo.go:53 # 0x6d831a main.alloc2+0x2a /home/ubuntu/heap/leak_demo.go:54
根据上面的提示,就能判断32015个goroutine运行到leak_demo.go
的53行:
func alloc2(outCh chan<- int) { func() { defer fmt.Println("alloc-fm exit") // 分配内存,假用一下 buf := make([]byte, 1024*1024*10) _ = len(buf) fmt.Println("alloc done") outCh <- 0 // 53行 }() }
阻塞的缘由是outCh这个写操做没法完成,outCh是无缓冲的通道,而且因为如下代码是死代码,因此goroutine始终没有从outCh读数据,形成outCh阻塞,进而形成无数个alloc2的goroutine阻塞,造成内存泄露:
if false { <-outCh }
url请求中设置debug=2:
http://ip:port/debug/pprof/goroutine?debug=2
第2种方式和第1种方式是互补的,它能够看到每一个goroutine的信息:
goroutine 20 [chan send, 2 minutes]
:20是goroutine id,[]
中是当前goroutine的状态,阻塞在写channel,而且阻塞了2分钟,长时间运行的系统,你能看到阻塞时间更长的状况。leak_demo.go
的53行,goroutine 20 [chan send, 2 minutes]: main.alloc2.func1(0xc42015e060) /home/ubuntu/heap/leak_demo.go:53 +0xf9 // 这 main.alloc2(0xc42015e060) /home/ubuntu/heap/leak_demo.go:54 +0x2b created by main.alloc1 /home/ubuntu/heap/leak_demo.go:42 +0x3f
Web的方法是简单粗暴,无需登陆服务器,浏览器打开看看就好了。但就像前面提的,没有浏览器可访问时,命令行交互式才是最佳的方式,而且也是手到擒来,感受比Web同样方便。
命令行交互式只有1种获取goroutine profile的方法,不像Web网页分debug=1
和debug=2
2中方式,并将profile文件保存到本地:
// 注意命令没有`debug=1`,debug=1,加debug有些版本的go不支持 $ go tool pprof http://0.0.0.0:6060/debug/pprof/goroutine Fetching profile over HTTP from http://localhost:6061/debug/pprof/goroutine Saved profile in /home/ubuntu/pprof/pprof.leak_demo.goroutine.001.pb.gz // profile文件保存位置 File: leak_demo Type: goroutine Time: May 16, 2019 at 2:44pm (CST) Entering interactive mode (type "help" for commands, "o" for options) (pprof)
命令行只须要掌握3个命令就行了,上面介绍过了,详细的倒回去看top, list, traces:
咱们依然使用leak_demo.go
这个demo,
$ go tool pprof -base pprof.leak_demo.goroutine.001.pb.gz pprof.leak_demo.goroutine.002.pb.gz File: leak_demo Type: goroutine Time: May 16, 2019 at 2:44pm (CST) Entering interactive mode (type "help" for commands, "o" for options) (pprof) (pprof) (pprof) top Showing nodes accounting for 20312, 100% of 20312 total flat flat% sum% cum cum% 20312 100% 100% 20312 100% runtime.gopark 0 0% 100% 20312 100% main.alloc2 0 0% 100% 20312 100% main.alloc2.func1 0 0% 100% 20312 100% runtime.chansend 0 0% 100% 20312 100% runtime.chansend1 0 0% 100% 20312 100% runtime.goparkunlock (pprof) (pprof) traces File: leak_demo Type: goroutine Time: May 16, 2019 at 2:44pm (CST) -----------+------------------------------------------------------- 20312 runtime.gopark runtime.goparkunlock runtime.chansend runtime.chansend1 // channel发送 main.alloc2.func1 // alloc2中的匿名函数 main.alloc2 -----------+-------------------------------------------------------
top命令在怎么肯定是goroutine泄露引起的内存泄露介绍过了,直接看traces命令,traces能列出002中比001中多的那些goroutine的调用栈,这里只有1个调用栈,有20312个goroutine都执行这个调用路径,能够看到alloc2中的匿名函数alloc2.func1
调用了写channel的操做,而后阻塞挂起了goroutine,使用list列出alloc2.func1
的代码,显示有20312个goroutine阻塞在53行:
(pprof) list main.alloc2.func1 Total: 20312 ROUTINE ======================== main.alloc2.func1 in /home/ubuntu/heap/leak_demo.go 0 20312 (flat, cum) 100% of Total . . 48: // 分配内存,假用一下 . . 49: buf := make([]byte, 1024*1024*10) . . 50: _ = len(buf) . . 51: fmt.Println("alloc done") . . 52: . 20312 53: outCh <- 0 // 看这 . . 54: }() . . 55:} . . 56:
友情提醒:使用list命令的前提是程序的源码在当前机器,否则可无法列出源码。服务器上,一般没有源码,那咱们咋办呢?刚才介绍了Web查看的方式,那里会列出代码行数,咱们可使用wget
下载网页:
$ wget http://localhost:6060/debug/pprof/goroutine?debug=1
下载网页后,使用编辑器打开文件,使用关键字main.alloc2.func1
进行搜索,找到与当前相同的调用栈,就能够看到该goroutine阻塞在哪一行了,不要忘记使用debug=2
还能够看到阻塞了多久和缘由,Web方式中已经介绍了,此处省略代码几十行。
文章略长,但全是干货,感谢阅读到这。然读到着了,跟定很想掌握pprof,建议实践一把,如今和你们温习一把本文的主要内容。
goroutine泄露的本质是channel阻塞,没法继续向下执行,致使此goroutine关联的内存都没法释放,进一步形成内存泄露。
利用好go pprof获取goroutine profile文件,而后利用3个命令top、traces、list定位内存泄露的缘由。
泄露的场景不只限于如下两类,但因channel相关的泄露是最多的。
channel的读或者写:
为避免goroutine泄露形成内存泄露,启动goroutine前要思考清楚:
本文全部示例源码,及历史文章、代码都存储在Github,阅读原文可直接跳转,Github:https://github.com/Shitaibin/golang_step_by_step/tree/master/pprof 。
这些既是参考资料也是推荐阅读的文章,不容错过。
【Go Blog关于pprof详细介绍和Demo】 https://blog.golang.org/profi...
【Dave关于高性能Go程序的workshop】 https://dave.cheney.net/high-...
【煎鱼pprof文章,很适合入门 Golang大杀器之性能剖析PProf】 https://segmentfault.com/a/11...
【SO上goroutine调用栈各字段的介绍】https://stackoverflow.com/a/3...
【个人老文,有runtime.main的介绍,想学习调度器,能够看下系列文章 Go调度器系列(2)宏观看调度器】http://lessisbetter.site/2019...
- 若是这篇文章对你有帮助,不妨关注下个人Github,有文章会收到通知。
- 本文做者:大彬
- 若是喜欢本文,随意转载,但请保留此原文连接:http://lessisbetter.site/2019/05/18/go-goroutine-leak/