golang 内存分析/内存泄漏
pprof
pprof 是 Go 语言中分析程序运行性能的工具,它能提供各种性能数据:
类型 | 描述 |
---|---|
allocs | 内存分配情况的采样信息 |
blocks | 阻塞操作情况的采样信息 |
goroutine | 当前所有协程的堆栈信息 |
heap | 堆上内存的使用情况的采样信息 |
profile | CPU占用情况的采样信息 |
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