实战Go内存泄露【转】

最近解决了咱们项目中的一个内存泄露问题,事实再次证实pprof是一个好工具,但掌握好工具的正确用法,才能发挥好工具的威力,否则就算你手里有屠龙刀,也成不了天下第一,本文就是带你用pprof定位内存泄露问题。html

关于Go的内存泄露有这么一句话不知道你听过没有:node

10次内存泄露,有9次是goroutine泄露。

我所解决的问题,也是goroutine泄露致使的内存泄露,因此这篇文章主要介绍Go程序的goroutine泄露,掌握了如何定位和解决goroutine泄露,就掌握了内存泄露的大部分场景git

本文草稿最初数据都是生产坏境数据,为了防止敏感内容泄露,所有替换成了demo数据,demo的数据比生产环境数据简单多了,更适合入门理解,有助于掌握pprof。

go pprof基本知识

定位goroutine泄露会使用到pprof,pprof是Go的性能工具,在开始介绍内存泄露前,先简单介绍下pprof的基本使用,更详细的使用给你们推荐了资料。github

什么是pprof

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信息浏览器

  1. block:goroutine的阻塞信息,本例就截取自一个goroutine阻塞的demo,但block为0,没掌握block的用法
  2. goroutine:全部goroutine的信息,下面的full goroutine stack dump是输出全部goroutine的调用栈,是goroutine的debug=2,后面会详细介绍。
  3. heap:堆内存的信息
  4. mutex:锁的信息
  5. threadcreate:线程信息

这篇文章咱们主要关注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个地方:

  1. 下载获得的文件:/home/ubuntu/pprof/pprof.demo.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz,这其中包含了程序名demo,profile类型alloc已分配的内存,inuse表明使用中的内存。
  2. help能够获取帮助,最早会列出支持的命令,想掌握pprof,要多看看,多尝试。

关于命令,本文只会用到3个,我认为也是最经常使用的:toplisttraces,分别介绍一下。

top

按指标大小列出前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个统计数据:

  • flat: 本函数占用的内存量。
  • flat%: 本函数内存占使用中内存总量的百分比。
  • sum%: 前面每一行flat百分比的和,好比第2行虽然的100% 是 100% + 0%。
  • cum: 是累计量,加入main函数调用了函数f,函数f占用的内存量,也会记进来。
  • cum%: 是累计量占总量的百分比。

list

查看某个函数的代码,以及该函数每行代码的指标信息,若是函数名不明确,会进行模糊匹配,好比list main会列出main.mainruntime.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中解释的同样。

traces

打印全部调用栈,以及调用栈的指标信息。

(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结合起来,但这不是本文的重点,因此就很少介绍了,为你们推荐几篇文章,必定要好好研读、实践:

  1. Go官方博客关于pprof的介绍,很详细,也包含样例,能够实操:Profiling Go Programs
  2. 跟煎鱼也讨论过pprof,煎鱼的这篇文章也很适合入门: Golang 大杀器之性能剖析 PProf

什么是内存泄露

内存泄露指的是程序运行过程当中已再也不使用的内存,没有被释放掉,致使这些内存没法被使用,直到程序结束这些内存才被释放的问题。

Go虽然有GC来回收再也不使用的堆内存,减轻了开发人员对内存的管理负担,但这并不意味着Go程序再也不有内存泄露问题。在Go程序中,若是没有Go语言的编程思惟,也不遵照良好的编程实践,就可能埋下隐患,形成内存泄露问题。

怎么发现内存泄露

在Go中发现内存泄露有2种方法,一个是通用的监控工具,另外一个是go pprof:

  1. 监控工具:固定周期对进程的内存占用状况进行采样,数据可视化后,根据内存占用走势(持续上升),很容易发现是否发生内存泄露。
  2. go pprof:适合没有监控工具的状况,使用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等表格工具,绘制内存占用图。

 

go pprof发现存在内存问题

有情提醒:若是对pprof不了解,能够先看 go pprof基本知识,这是下一节,看完再倒回来看。

若是你Google或者百度,Go程序内存泄露的文章,它总会告诉你使用pprof heap,可以生成漂亮的调用路径图,火焰图等等,而后你根据调用路径就能定位内存泄露问题,我最初也是对此深信不疑,尝试了若干天后,只是发现内存泄露跟某种场景有关,根本找不到内存泄露的根源,若是哪位朋友用heap就能定位内存泄露的线上问题,麻烦介绍下

后来读了Dave的《High Performance Go Workshop》,刷新了对heap的认识,内存pprof的简要内容以下:

 

 

 

Dave讲了如下几点:

  1. 内存profiling记录的是堆内存分配的状况,以及调用栈信息,并非进程完整的内存状况,猜想这也是在go pprof中称为heap而不是memory的缘由。
  2. 栈内存的分配是在调用栈结束后会被释放的内存,因此并不在内存profile中
  3. 内存profiling是基于抽样的,默认是每1000次堆内存分配,执行1次profile记录。
  4. 由于内存profiling是基于抽样和它跟踪的是已分配的内存,而不是使用中的内存,(好比有些内存已经分配,看似使用,但实际以及不使用的内存,好比内存泄露的那部分),因此不能使用内存profiling衡量程序整体的内存使用状况
  5. Dave我的观点:使用内存profiling不可以发现内存泄露

基于目前对heap的认知,我有2个观点:

  1. heap能帮助咱们发现内存问题,但不必定能发现内存泄露问题,这个见解与Dave是相似的。heap记录了内存分配的状况,咱们能经过heap观察内存的变化,增加与减小,内存主要被哪些代码占用了,程序存在内存问题,这只能说明内存有使用不合理的地方,但并不能说明这是内存泄露。
  2. heap在帮助定位内存泄露缘由上贡献的力量微乎其微。如第一条所言,能经过heap找到占用内存多的位置,但这个位置一般不必定是内存泄露,就算是内存泄露,也只是内存泄露的结果,并非真正致使内存泄露的根源。

接下来,我介绍怎么用heap发现问题,而后再解释为何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对比,先执行toptop的对比,而后执行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.mainruntime.mainmain.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“不能”定位内存泄露

heap能显示内存的分配状况,以及哪行代码占用了多少内存,咱们能轻易的找到占用内存最多的地方,若是这个地方的数值还在不断怎大,基本能够认定这里就是内存泄露的位置。

曾想按图索骥,从内存泄露的位置,根据调用栈向上查找,总能找到内存泄露的缘由,这种方案看起来是不错的,但实施起来却找不到内存泄露的缘由,结果是事半功倍。

缘由在于一个Go程序,其中有大量的goroutine,这其中的调用关系也许有点复杂,也许内存泄露是在某个三方包里。举个栗子,好比下面这幅图,每一个椭圆表明1个goroutine,其中的数字为编号,箭头表明调用关系。heap profile显示g111(最下方标红节点)这个协程的代码出现了泄露,任何一个从g101到g111的调用路径均可能形成了g111的内存泄露,有2类可能:

  1. 该goroutine只调用了少数几回,但消耗了大量的内存,说明每一个goroutine调用都消耗了很多内存,内存泄露的缘由基本就在该协程内部
  2. 该goroutine的调用次数很是多,虽然每一个协程调用过程当中消耗的内存很少,但该调用路径上,协程数量巨大,形成消耗大量的内存,而且这些goroutine因为某种缘由没法退出,占用的内存不会释放,内存泄露的缘由在到g111调用路径上某段代码实现有问题,形成建立了大量的g111

第2种状况,就是goroutine泄露,这是经过heap没法发现的,因此heap在定位内存泄露这件事上,发挥的做用不大

 


goroutine泄露怎么致使内存泄露

什么是goroutine泄露

若是你启动了1个goroutine,但并无符合预期的退出,直到程序结束,此goroutine才退出,这种状况就是goroutine泄露。

提早思考:什么会致使goroutine没法退出/阻塞?

goroutine泄露怎么致使内存泄露

每一个goroutine占用2KB内存,泄露1百万goroutine至少泄露2KB * 1000000 = 2GB内存,为何说至少呢?

goroutine执行过程当中还存在一些变量,若是这些变量指向堆内存中的内存,GC会认为这些内存仍在使用,不会对其进行回收,这些内存谁都没法使用,形成了内存泄露。

因此goroutine泄露有2种方式形成内存泄露:

  1. goroutine自己的栈所占用的空间形成内存泄露。
  2. goroutine中的变量所占用的堆内存致使堆内存泄露,这一部分是能经过heap profile体现出来的。

Dave在文章中也提到了,若是不知道什么时候中止一个goroutine,这个goroutine就是潜在的内存泄露:

7.1.1 Know when to stop a goroutine

If 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.

怎么肯定是goroutine泄露引起的内存泄露

掌握了前面的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泄露。


定位goroutine泄露的2种方法

使用pprof有2种方式,一种是web网页,一种是go tool pprof命令行交互,这两种方法查看goroutine都支持,但有轻微不一样,也有各自的优缺点。

咱们先看Web的方式,再看命令行交互的方式,这两种都很好使用,结合起来用也不错。

Web可视化查看

Web方式适合web服务器的端口能访问的状况,使用起来方便,有2种方式:

  1. 查看某条调用路径上,当前阻塞在此goroutine的数量
  2. 查看全部goroutine的运行栈(调用路径),能够显示阻塞在此的时间

方式一

url请求中设置debug=1:

http://ip:port/debug/pprof/goroutine?debug=1

效果以下:

 

看起来密密麻麻的,其实简单又十分有用,看上图标出来的部分,手机上图看起来可能不方便,那就放大图片,或直接看下面各字段的含义:

  1. goroutine profile: total 32023:32023是goroutine的总数量
  2. 32015 @ 0x42e15a 0x42e20e 0x40534b 0x4050e5 ...:32015表明当前有32015个goroutine运行这个调用栈,而且停在相同位置,@后面的十六进制,如今用不到这个数据,因此暂不深究了。
  3. 下面是当前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的信息:

  1. goroutine 20 [chan send, 2 minutes]:20是goroutine id,[]中是当前goroutine的状态,阻塞在写channel,而且阻塞了2分钟,长时间运行的系统,你能看到阻塞时间更长的状况。
  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=1debug=22中方式,并将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个命令就行了,上面介绍过了,详细的倒回去看toplisttraces

  1. top:显示正运行到某个函数goroutine的数量
  2. traces:显示全部goroutine的调用栈
  3. list:列出代码详细的信息。

咱们依然使用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泄露的本质

goroutine泄露的本质是channel阻塞,没法继续向下执行,致使此goroutine关联的内存都没法释放,进一步形成内存泄露。

goroutine泄露的发现和定位

利用好go pprof获取goroutine profile文件,而后利用3个命令top、traces、list定位内存泄露的缘由。

goroutine泄露的场景

泄露的场景不只限于如下两类,但因channel相关的泄露是最多的。

  1. channel的读或者写:

    1. 无缓冲channel的阻塞一般是写操做由于没有读而阻塞
    2. 有缓冲的channel由于缓冲区满了,写操做阻塞
    3. 期待从channel读数据,结果没有goroutine写
  2. select操做,select里也是channel操做,若是全部case上的操做阻塞,goroutine也没法继续执行。

编码goroutine泄露的建议

为避免goroutine泄露形成内存泄露,启动goroutine前要思考清楚:

  1. goroutine如何退出?
  2. 是否会有阻塞形成没法退出?若是有,那么这个路径是否会建立大量的goroutine?

示例源码

本文全部示例源码,及历史文章、代码都存储在Github,阅读原文可直接跳转,Github:https://github.com/Shitaibin/golang_step_by_step/tree/master/pprof 。

 

实践 Tips

如下是一些从其它项目借鉴或者本身总结的实践经验,它们只是建议,而不是准则,实际项目中应该以性能分析数据来做为优化的参考,避免过早优化。

  1. 对频繁分配的对象,使用 sync.Pool 对象池减小分配时GC压力
  2. 自动化的 DeepCopy 是很是耗时的,其中涉及到反射,内存分配,容器(如 map)扩展等,大概比手动拷贝慢一个数量级
  3. 用 atomic.Load/StoreXXX,atomic.Value, sync.Map 等代替 Mutex。(优先级递减)
  4. 使用高效的第三方库,如用fasthttp替代 net/http
  5. 在开发环境加上-race编译选项进行竞态检查
  6. 在开发或线上环境开启 net/http/pprof,方便实时pprof
  7. 将全部外部IO(网络IO,磁盘IO)作成异步

推荐阅读

这些既是参考资料也是推荐阅读的文章,不容错过。

【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...

Profiling Go Programs

profiling-and-optimizing-go-web-applications

 

原文: http://www.javashuo.com/article/p-zvldpdwt-kz.html

相关文章
相关标签/搜索