共计 5523 个字符,预计需要花费 14 分钟才能阅读完成。
背景
Go 是 GC 类语言,内存主动回收。如果程序中已动态分配的堆内存因为某种原因程序未开释或无奈开释,这时候就会产生 内存泄露,造成零碎内存节约,导致程序运行减慢甚至零碎 OOM。相似地,程序在运行过程中可能会呈现 CPU 问题。
性能剖析是排查内存泄露等问题的常见伎俩,能够让咱们理解和捕捉到程序堆内存和 Profile 信息,不便定位问题。Go 原生提供了丰盛的剖析工具,上面是通过几个内存泄露的案例对 Go 程序做性能剖析,并介绍相干工具原理和细节。
<!–more–>
GODEBUG
通过 GODEBUG 开启 debug 模式后,可做内存 trace 和调度器的 trace,runtime 每次垃圾回收时会记录 GC 信息。
设置内存追踪模式: GODEBUG=gctrace=1 go run example.go
过程完结后就能够打印 GC 信息(这里 gctrace 只有在程序完结后才会打印 GC 信息,编译后的程序是守护模式的常驻过程,没有显示 gctrace 信息)
官网 runtime 对 gctrace 做了具体的阐明:
gctrace: setting gctrace=1 causes the garbage collector to emit a single line to standard error at each collection, summarizing the amount of memory collected and the length of the pause. Setting gctrace=2 emits the same summary but also repeats each collection. The format of this line is subject to change.
也就是说垃圾收集器在每次触发 GC 时就会打印日志。
GC 日志阐明
gctrace 每一行打印的日志格局如下:
gc {0} @{1}s {2}%: {3}+...+{4} ms clock, {5}+...+{6} ms cpu, {7}->{8}->{9} MB, {10} MB goal, {11} P
每一个变量的具体定义:
- {0}: gc 运行次数
- {1}: 程序已运行的工夫
- {2}: gc 占用程序 CPU 的百分比
- {3}: 执行工夫,包含程序提早和资源期待
- {4}: 也是执行工夫, 个别看这个
- {5}: CPU clock
- {6}: CPU clock
- {7}: GC 启动前的堆内存
- {8}: GC 运行后的堆内存
- {9}: 以后堆内存
- {10}: GC 指标
- {11}: 过程数
举个例子,有上面一条 gctrace 日志:
gc 3 @2.387s 0%: 0.021+23+0.053 ms clock, 0.086+0.011/17/6.0+0.21 ms cpu, 165->165->127 MB, 166 MB goal, 4 P
每个 trace 元信息对应的含意
- gc 3:第 3 次 GC
- @2.387: 以后是程序启动后的 2.387s
- 0%: 程序启动后到当初共破费 0% 的工夫在 GC 上
-
0.021+23+0.053 ms clock
- 0.021:示意单个 P 在 mark 阶段的 STW 工夫
- 23:示意所有 P 的 mark concurrent(并发标记)所应用的工夫
- 0.053:示意单个 P 的 markTermination 阶段的 STW 工夫
-
0.086+0.011/17/6.0+0.21 ms cpu
- 0.086 示意整个过程在 mark 阶段 STW 进展的工夫
- 0.011/17/6.0:0.011 示意 mutator assist 占用的工夫,17 示意 dedicated + fractional 占用的工夫,6.0 示意 idle 占用的工夫
- 0.21ms:0.21 示意整个过程在 markTermination 阶段 STW 工夫
-
165->165->127 MB:
- 165:示意开始 mark 阶段前的 heap_live 大小
- 165:示意开始 markTermination 阶段前的 heap_live 大小
- 127:示意被标记对象的大小。
- 166 MB goal:示意下一次触发 GC 回收的阈值是 166 MB
- 4 P:本次 GC 一共波及 4 个 过程
波及到一些 GC 过程的术语和深层细节,能够参考:用 GODEBUG 看 GC
演示
上面是一块存在内存泄露的代码段,通过 gctrace, 咱们来看下每次 GC 时堆内存的变动:
package main
import (
"os"
"os/signal"
"syscall"
)
func main() {
// Create a Goroutine that leaks memory. Dumping key-value pairs to put tons of allocation.
go func() {m := make(map[int]int)
for i := 0; ; i++ {m[i] = i
}
}()
// Shutdown the program with Ctrl-C
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)
<-sig
}
执行 GODEBUG=gctrace=1 go run memory_tracing.go
, 查看运行时的内存状况(图一):
上图最初一行的 trace 日志:示意第 8 次 GC, 以后是程序启动后的 9.530s, 本次 GC 占用 0% 工夫。GC 前占 610M 内存,GC 后占 610M 内存,以后堆内存 509M。下一次触发 GC 的阈值是 611 MB。本次 GC 一共波及到 4 个过程。
能够看到程序在运行过程中,前面的 GC,堆内存在逐步增大,触发 GC 后堆内存大小并没有稳固在一个值或者缩小,比方第 8 次 GC, 本次 GC 没有占用程序运行工夫(0%),GC 后内存 509MB 比上一次 GC 后增大 382MB,内存开释的速度慢于内存增长速度,这是一个比拟显著的内存泄露场景。
GOGC 和 Scheduler Trace
GODEBUG 还反对设置以下变量:
- GOGC:扭转堆增长形式 —— 设置初始的 GC 指标百分比。当新分配内存,与上一次采集后残余的实时数据的比例达到这个百分比时,才会触发一次 GC。默认值是 GOGC=100。设置
GOGC=off
则齐全禁用垃圾收集器。举个例子,比方GODEBUG=gctrace=1 GOGC=200 go run memory_tracing.go
, 示意堆内存增长到 200% 会触发一次 GC, 打印一条 trace 日志。和 图一 比拟,能够看到内存 8M 左右时触发 GC (200%)
- schedtrace:设置
schedtrace=X
, 每 X 毫秒打印一次调度器状态 —— 包含调度器、处理器、线程和 goroutine:
PPROF
下面通过 GODEBUG 设置 gctrace 参数,来剖析 Go 程序在执行过程中的运行状况,并对 trace 日志含意做了阐明。通过一段内存泄露的样例,来剖析 GC 过程的堆内存变动。然而能够看到,仅仅是通过 gctrace,只能大抵确定内存泄露问题是否存在,作用无限 (比方我的项目十分宏大简单,或高并发程序)。Go 官网提供了更加弱小的内存剖析利器:pprof。通过 pprof 对 Web 利用和服务进行配置,以 查看性能或内存被占用的确切地位。
原理
Go 的 runtime 包 反对收集和生成 pprof 可视化工具 所定义格局的 profiling 文件(采样数据)。
采样数据次要有三种采集形式:
- runtime/pprof: 在程序里埋点, 调用
runtime.StartCPUProfile
和runtime.StopCPUProfile
API,生成采样数据 - net/http/pprof : import 到程序后注入
/debug/pprof/
接口,通过 HTTP 服务器采集数据,以 runtime/pprof 实现 - go test: 个别对函数进行测试并生成采样数据,如
go test -bench=. -cpuprofile=cpu.prof
runtime/pprof 提供了多种类型的采样,官网的反对如下:
捕捉到采样数据,导入到 pprof 可视化工具,就能够进行咱们想要的性能剖析。
演示
还是用下面的代码段为例子,注入了 net/http/pprof
,来反对 prof 数据采集,memory_tracing_01.go 代码如下:
package main
import (
"net/http"
_ "net/http/pprof"
)
var m = make(map[int]int)
func main() {go func() {
for i := 0; ; i++ {if i%(9*60) == 0 && i%7 == 0 { // 增加判断条件,避免增长过快打满内存
m[i] = i
}
}
}()
http.ListenAndServe("0.0.0.0:8080", nil)
}
执行 go run memory_tracing_01.go
让程序运行,在浏览器输出 http://localhost:8080/debug/pprof/, 能够看到 pprof 界面:
因为程序存在内存泄露,刷新页面能发现 allocs 和 heap 指标始终在增长。
能够间接在界面提供的采样数据进行问题定位,然而剖析起来比拟麻烦,这里抉择间接下载 heap 的采样数据(采样 20s):
curl http://localhost:8080/debug/pprof/heap\?seconds\=20 -o memory_tracing_01.prof
内存剖析
拿到 性能采样数据 后,下一步就是通过 go tool pprof
进行内存剖析。
对于 pprof 的应用姿态,这里举荐两篇很具体的博客:
- Profiling Go Programs
- Golang 调优技巧
当初网上相干博客和文档十分的多,这里总结了大部分文章举荐的应用姿态:
go tool pprof XXX.prof
间接进入调试终端- svg/pdf 拿到 函数调用的流程图
- topN -cum 按资源占用递加排序
- list FuncXXX,而后看具体函数里那个点 占最多 CPU, 或者吃最多内存
- 装置 Uber 的 go-torch,生成一张火焰图,进行下一步剖析
其实能够发现,下面这一套下来学习老本有点高,须要在学习采样 profile 数据后,再理解 3~4 个左右的步骤,能力定位到程序在哪里呈现了问题。然而个别线上程序的内存泄露,CPU 飙升或者 goroutine 泄露等紧急问题,如果不晓得如何应用 pprof, 一时半刻解决不了是挺重大的。所以是否有更间接简略的姿态,来进步问题定位的效率?
答案是有的。go tool pprof
目前曾经反对 UI,通过在 Web 界面上的操作,不须要学习指令就能够更快地做性能剖析(google 的 pprof, 也提供弱小易上手的 UI 反对)。
以刚采样到的 memory_tracing_01.prof 为例,执行:
go tool pprof -http=:8080 memory_tracing_01.prof
在浏览器输出 pprof UI 地址:http://localhost:8080/ui/
首页是函数调用流程图,能够直观地看到 main.main.founc1 占用了 93.06MB 内存,底下是内存具体调配。能够晓得大部的内存耗费在 func1 函数里。
点击菜单栏 VIEW -> Source,能够查看具体到哪一行代理,占用多少内存 (以占用内存多少倒序):
由上图 line 14 能够显著看到,m 这个 map 占用了 93.06MB 的内存,占采样到的内存样本的 100%。因而,能够晓得内存泄露问题是 m 导致的。
pprof 的 Web 界面 另一大利器 是 反对火焰图(Flame Graph),能够更直观查看整个程序函数调用和资源应用。与 go-torch 相比,它最大长处是动静的,调用程序由上到下(A -> B -> C -> D),每一块代表一个函数,越大代表占用 CPU 的工夫更长(或占用更多内存)。反对点击块深刻进行剖析!
至此,曾经通过 go tool pprof
实现内存泄露的定位,下一步要做的是解决 m 的内存问题,这里不探讨优化。
除了下面提到的 Graph、Flame Graph 和 Source,Web 界面还反对 Top、Peek 和 Disassemble 的数据可视化。
net/http/pprof 的缺点和解决
当初根本很多我的项目会通过引入 net/http/pprof
进行数据采样和性能剖析,尽管 pprof 做 profiling 不会减少很多开销,但也存在一些缺点:
- 裸露了 /debug 的 API 路由。黑客能够通过发送长时延的 profiling 查问申请,来影响整体利用性能。
- 采样的数据裸露了利用外部细节,不应该对外展现。
- 如果利用是一个 offline worker, 引入一个 pprof HTTP Server 无异于减少了平安危险
net/http/pprof
带来的一些平安问题,有以下一些解决方案:
- 确保只有通过受权的申请能力采集 profile 数据
- 能够在应用程序前加层反向代理来限度拜访,或者将 pprof 服务器从主服务器上移出,重定向到不同 TCP 端口,通过 ACL 容许其余用户拜访
- 应用其余形式采样 profile 数据,再通过 pprof 进行剖析
总结
本文围绕一个简略内存泄露的例子,来开展介绍如何对 Go 程序做性能剖析定位问题,具体阐明 GODEBUG 和 PPROF 的原理和一些应用姿态:GODEBUG 更多是进行 Profile tracing, 打印 gc 时日志和 schedtrace,PPROF 作为官网的剖析工具来定位问题。并介绍go tool pprof
通过 Web 界面来更分明间接地剖析问题,最初是阐明引入 net/http/pprof
存在的一些平安缺点和对应解决方案。
本篇尽管只是一个简略内存泄露开展,但相似像 CPU 飙升的问题也能够是同一个思路。以上仅仅是性能剖析的冰山一角,对于这块有十分多的细节和常识。
参考
- Memory Tracing
- Golang 调优技巧
- Profiling a Larger Web Service
- runtime
- Diagnostics
- Continuous Profiling and Go
- 用 GODEBUG 看 GC