golang 内存分析/内存泄漏

107

pprof

pprof 是 Go 语言中分析程序运行性能的工具,它能提供各种性能数据:

类型描述
allocs内存分配情况的采样信息
blocks阻塞操作情况的采样信息
goroutine当前所有协程的堆栈信息
heap堆上内存的使用情况的采样信息
profileCPU占用情况的采样信息
threadcreate系统线程创建情况的采样信息
trace程序运行跟踪信息

以内存分析为例:

推荐直接使用命令进入命令行交互模式:

go tool pprof -alloc_space http://localhost:6061/debug/pprof/heap

可以使用参数指明分析的类型:

inuse_space — amount of memory allocated and not released yet
inuse_objects— amount of objects allocated and not released yet
alloc_space — total amount of memory allocated (regardless of released)
alloc_objects — total amount of objects allocated (regardless of released)

进入交互式模式之后,比较常用的有 top、list、traces、web 等命令。

(1) top

(pprof) top
Showing nodes accounting for 15624.87MB, 50.48% of 30953.89MB total
Dropped 229 nodes (cum <= 154.77MB)
Showing top 10 nodes out of 167
flat  flat%   sum%        cum   cum%
6272.15MB 20.26% 20.26%  6272.15MB 20.26%  github.com/emicklei/go-restful.CurlyRouter.selectRoutes
1457.12MB  4.71% 30.48%  1457.12MB  4.71%  bytes.makeSlice
1177.26MB  3.80% 38.47%  1260.76MB  4.07%  net/textproto.(*Reader).ReadMIMEHeader
900.41MB  2.91% 41.38%   987.41MB  3.19%  google.golang.org/grpc/internal/transport.(*http2Client).createHeaderFields
780.13MB  2.52% 43.90%  3044.06MB  9.83%  net/http.(*conn).readRequest
705.24MB  2.28% 46.18%   705.24MB  2.28%  github.com/emicklei/go-restful.sortableCurlyRoutes.routes
678.09MB  2.19% 48.37%  1112.62MB  3.59%  google.golang.org/grpc/internal/transport.(*http2Client).newStream
653.03MB  2.11% 50.48%   653.03MB  2.11%  context.WithValue

top会列出5个统计数据:

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

(2) list

查看某个函数的代码,以及该函数每行代码的指标信息,如果函数名不明确,会进行模糊匹配,比如

(pprof) list github.com/emicklei/go-restful.CurlyRouter.selectRoutes
Total: 30.45GB
ROUTINE ======================== github.com/emicklei/go-restful.CurlyRouter.selectRoutes in /Users/michaelliu/go/pkg/mod/github.com/emicklei/go-restful@v2.12.0+incompatible/curly.go
6.13GB     6.13GB (flat, cum) 20.11% of Total
.          .     43:	return detectedService, selectedRoute, nil
.          .     44:}
.          .     45:
.          .     46:// selectRoutes return a collection of Route from a WebService that matches the path tokens from the request.
.          .     47:func (c CurlyRouter) selectRoutes(ws *WebService, requestTokens []string) sortableCurlyRoutes {
6.06GB     6.06GB     48:	candidates := make(sortableCurlyRoutes, 0, 8)
.          .     49:	for _, each := range ws.routes {
.          .     50:		matches, paramCount, staticCount := c.matchesRouteByPathTokens(each.pathParts, requestTokens, each.hasCustomVerb)
.          .     51:		if matches {
.          .     52:			candidates.add(curlyRoute{each, paramCount, staticCount}) // TODO make sure Routes() return pointers?
.          .     53:		}
.          .     54:	}
64.50MB    64.50MB     55:	sort.Sort(candidates)
.          .     56:	return candidates
.          .     57:}
.          .     58:
.          .     59:// matchesRouteByPathTokens computes whether it matches, howmany parameters do match and what the number of static path elements are.
.          .     60:func (c CurlyRouter) matchesRouteByPathTokens(routeTokens, requestTokens []string, routeHasCustomVerb bool) (matches bool, paramCount int, staticCount int) {

可以看到在github.com/emicklei/go-restful.CurlyRouter.selectRoutes中的第48行占用了6.06GB内存。

(3) traces

traces可以打印所有调用栈,以及调用栈的指标信息。

(pprof) traces github.com/emicklei/go-restful.CurlyRouter.selectRoutes
Type: alloc_space
Time: Sep 20, 2020 at 7:39pm (CST)
-----------+-------------------------------------------------------
bytes:  32B
64.50MB   github.com/emicklei/go-restful.CurlyRouter.selectRoutes
github.com/emicklei/go-restful.CurlyRouter.SelectRoute
github.com/emicklei/go-restful.(*Container).dispatch.func3
github.com/emicklei/go-restful.(*Container).dispatch
net/http.HandlerFunc.ServeHTTP
net/http.(*ServeMux).ServeHTTP
github.com/emicklei/go-restful.(*Container).ServeHTTP
net/http.serverHandler.ServeHTTP
net/http.(*conn).serve
-----------+-------------------------------------------------------
bytes:  3kB
6.06GB   github.com/emicklei/go-restful.CurlyRouter.selectRoutes
github.com/emicklei/go-restful.CurlyRouter.SelectRoute
github.com/emicklei/go-restful.(*Container).dispatch.func3
github.com/emicklei/go-restful.(*Container).dispatch
net/http.HandlerFunc.ServeHTTP
net/http.(*ServeMux).ServeHTTP
github.com/emicklei/go-restful.(*Container).ServeHTTP
net/http.serverHandler.ServeHTTP
net/http.(*conn).serve
-----------+-------------------------------------------------------

每个- - - - - 隔开的是一个调用栈。

内存泄露

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

内存profiling记录的是堆内存分配的情况,以及调用栈信息,并不是进程完整的内存情况。基于抽样和它跟踪的是已分配的内存,而不是使用中的内存,(比如有些内存已经分配,看似使用,但实际以及不使用的内存,比如内存泄露的那部分),所以不能使用内存profiling衡量程序总体的内存使用情况。

只能通过heap观察内存的变化,增长与减少,内存主要被哪些代码占用了,程序存在内存问题,这只能说明内存有使用不合理的地方,但并不能说明这是内存泄露。

heap在帮助定位内存泄露原因上贡献的力量微乎其微。能通过heap找到占用内存多的位置,但这个位置通常不一定是内存泄露,就算是内存泄露,也只是内存泄露的结果,并不是真正导致内存泄露的根源。

(1)怎么用heap发现内存问题

使用pprof的heap能够获取程序运行时的内存信息,在程序平稳运行的情况下,每个一段时间使用heap获取内存的profile,然后使用base能够对比两个profile文件的差别,就像diff命令一样显示出增加和减少的变化:

➜  pprof go tool pprof -alloc_space -base pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.149.pb.gz pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.150.pb.gz
Type: alloc_space
Time: Sep 20, 2020 at 7:23pm (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 221.95MB, 97.36% of 227.97MB total
Dropped 51 nodes (cum <= 1.14MB)
Showing top 10 nodes out of 55
flat  flat%   sum%        cum   cum%
199.29MB 87.42% 87.42%   199.29MB 87.42%  bytes.makeSlice
9.52MB  4.17% 91.59%     9.52MB  4.17%  regexp/syntax.(*compiler).inst (inline)
2.64MB  1.16% 92.75%     2.64MB  1.16%  compress/flate.NewWriter
2.50MB  1.10% 93.85%     4.50MB  1.97%  regexp/syntax.(*Regexp).Simplify
2MB  0.88% 94.73%        2MB  0.88%  regexp/syntax.simplify1 (inline)
2MB  0.88% 95.61%        2MB  0.88%  time.NewTimer
1.50MB  0.66% 96.26%     1.50MB  0.66%  os.lstatNolog
1.50MB  0.66% 96.92%     1.50MB  0.66%  regexp/syntax.(*parser).newRegexp (inline)
0.50MB  0.22% 97.14%     1.50MB  0.66%  github.com/go-chassis/go-chassis/pkg/scclient.(*RegistryClient).HTTPDo
0.50MB  0.22% 97.36%    16.01MB  7.02%  regexp.compile
(pprof) traces bytes.makeSlice
Type: alloc_space
Time: Sep 20, 2020 at 7:23pm (CST)
-----------+-------------------------------------------------------
bytes:  199.29MB
199.29MB   bytes.makeSlice
bytes.(*Buffer).grow
bytes.(*Buffer).Grow
io/ioutil.readAll
io/ioutil.ReadFile
github.com/go-chassis/go-chassis/core/lager.CopyFile
github.com/go-chassis/go-chassis/core/lager.doRollover
github.com/go-chassis/go-chassis/core/lager.logRotateFile
github.com/go-chassis/go-chassis/core/lager.LogRotate
github.com/go-chassis/go-chassis/core/lager.(*rotators).Rotate.func1
-----------+-------------------------------------------------------
bytes:  613.91MB
0   bytes.makeSlice
bytes.(*Buffer).grow
bytes.(*Buffer).ReadFrom
io/ioutil.readAll
io/ioutil.ReadFile
github.com/go-chassis/go-chassis/core/lager.CopyFile
github.com/go-chassis/go-chassis/core/lager.doRollover
github.com/go-chassis/go-chassis/core/lager.logRotateFile
github.com/go-chassis/go-chassis/core/lager.LogRotate
github.com/go-chassis/go-chassis/core/lager.(*rotators).Rotate.func1
-----------+-------------------------------------------------------
bytes:  306.95MB
0   bytes.makeSlice
bytes.(*Buffer).grow
bytes.(*Buffer).Grow
io/ioutil.readAll
io/ioutil.ReadFile
github.com/go-chassis/go-chassis/core/lager.CopyFile
github.com/go-chassis/go-chassis/core/lager.doRollover
github.com/go-chassis/go-chassis/core/lager.logRotateFile
github.com/go-chassis/go-chassis/core/lager.LogRotate
github.com/go-chassis/go-chassis/core/lager.(*rotators).Rotate.func1
-----------+-------------------------------------------------------

(2)goroutine泄露怎么导致内存泄露

每个goroutine占用2KB内存,泄露1百万goroutine至少泄露2KB * 1000000 = 2GB内存。此外goroutine执行过程中还存在一些变量,如果这些变量指向堆内存中的内存,GC会认为这些内存仍在使用,不会对其进行回收,这些内存谁都无法使用,造成了内存泄露。

所以goroutine泄露有2种方式造成内存泄露:

  • goroutine本身的栈所占用的空间造成内存泄露。
  • goroutine中的变量所占用的堆内存导致堆内存泄露,这一部分是能通过heap profile体现出来的。

分析goroutine本身的栈所占用的空间造成内存泄露,可以通过pprof来查找,方法与heap类似,都是取两次采样做比较。


我的博客即将同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=21ghm8x4iwrow