相信不少人都听过“雷神 3”关于性能优化的故事。在一个 3D 游戏引擎的源码里,John Carmack 将 1/sqrt(x)
这个函数的执行效率优化到了极致。html
通常咱们使用二分法,或者牛顿迭代法计算一个浮点数的平方根。但在这个函数里,做者使用了一个“魔数”,根本没有迭代,两步就直接算出了平方根。使人叹为观止!node
由于它是最底层的函数,而游戏里涉及到大量的这种运算,使得在运算资源极其紧张的 DOS 时代,游戏也能够流畅地运行。这就是性能优化的魅力!git
工做中,当业务量比较小的时候,用的机器也少,体会不到性能优化带来的收益。而当一个业务使用了几千台机器的时候,性能优化 20%,那就能省下几百台机器,一年能省几百万。省下来的这些钱,给员工发年终奖,那得多 Happy!github
通常而言,性能分析能够从三个层次来考虑:应用层、系统层、代码层。golang
应用层主要是梳理业务方的使用方式,让他们更合理地使用,在知足使用方需求的前提下,减小无心义的调用;系统层关注服务的架构,例如增长一层缓存;代码层则关心函数的执行效率,例如使用效率更高的开方算法等。web
作任何事,都要讲究方法。在不少状况下,迅速把事情最关键的部分完成,就能拿到绝大部分的收益了。其余的一些边边角角,能够慢慢地缝合。一上来就想完成 100%,每每会陷入付出了巨大的努力,却收获寥寥的境地。算法
性能优化这件事也同样,识别出性能瓶颈,会让咱们付出最小的努力,而获得最大的回报。shell
Go 语言里,pprof 就是这样一个工具,帮助咱们快速找到性能瓶颈,进而进行有针对性地优化。segmentfault
代码上线前,咱们经过压测能够获知系统的性能,例如每秒能处理的请求数,平均响应时间,错误率等指标。这样,咱们对本身服务的性能算是有个底。浏览器
可是压测是线下的模拟流量,若是到了线上呢?会遇到高并发、大流量,不靠谱的上下游,突发的尖峰流量等等场景,这些都是不可预知的。
线上忽然大量报警,接口超时,错误数增长,除了看日志、监控,就是用性能分析工具分析程序的性能,找到瓶颈。固然,通常这种情形不会让你有机会去分析,降级、限流、回滚才是首先要作的,要先止损嘛。回归正常以后,经过线上流量回放,或者压测等手段,制造性能问题,再经过工具来分析系统的瓶颈。
通常而言,性能分析主要关注 CPU、内存、磁盘 IO、网络这些指标。
Profiling
是指在程序执行过程当中,收集可以反映程序执行状态的数据。在软件工程中,性能分析(performance analysis,也称为 profiling),是以收集程序运行时信息为手段研究程序行为的分析方法,是一种动态程序分析的方法。
Go 语言自带的 pprof 库就能够分析程序的运行状况,而且提供可视化的功能。它包含两个相关的库:
runtime/pprof 对于只跑一次的程序,例如天天只跑一次的离线预处理程序,调用 pprof 包提供的函数,手动开启性能数据采集。
net/http/pprof 对于在线服务,对于一个 HTTP Server,访问 pprof 提供的 HTTP 接口,得到性能数据。固然,实际上这里底层也是调用的 runtime/pprof 提供的函数,封装成接口对外提供网络访问。
pprof
是 Go 语言中分析程序运行性能的工具,它能提供各类性能数据:
allocs
和 heap
采样的信息一致,不过前者是全部对象的内存分配,而 heap 则是活跃对象的内存分配。
The difference between the two is the way the pprof tool reads there at start time. Allocs profile will start pprof in a mode which displays the total number of bytes allocated since the program began (including garbage-collected bytes).
上图来自参考资料【wolfogre】的一篇 pprof 实战的文章,提供了一个样例程序,经过 pprof 来排查、分析、解决性能问题,很是精彩。
- 当 CPU 性能分析启用后,Go runtime 会每 10ms 就暂停一下,记录当前运行的 goroutine 的调用堆栈及相关数据。当性能分析数据保存到硬盘后,咱们就能够分析代码中的热点了。
- 内存性能分析则是在堆(Heap)分配的时候,记录一下调用堆栈。默认状况下,是每 1000 次分配,取样一次,这个数值能够改变。栈(Stack)分配 因为会随时释放,所以不会被内存分析所记录。因为内存分析是取样方式,而且也由于其记录的是分配内存,而不是使用内存。所以使用内存性能分析工具来准确判断程序具体的内存使用是比较困难的。
- 阻塞分析是一个很独特的分析,它有点儿相似于 CPU 性能分析,可是它所记录的是 goroutine 等待资源所花的时间。阻塞分析对分析程序并发瓶颈很是有帮助,阻塞性能分析能够显示出何时出现了大批的 goroutine 被阻塞了。阻塞性能分析是特殊的分析工具,在排除 CPU 和内存瓶颈前,不该该用它来分析。
咱们能够经过
报告生成
、Web 可视化界面
、交互式终端
三种方式来使用pprof
。
—— 煎鱼《Golang 大杀器之性能剖析 PProf》
拿 CPU profiling 举例,增长两行代码,调用 pprof.StartCPUProfile
启动 cpu profiling,调用 pprof.StopCPUProfile()
将数据刷到文件里:
import "runtime/pprof"
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
func main() {
// …………
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// …………
}
复制代码
启动一个端口(和正常提供业务服务的端口不一样)监听 pprof 请求:
import _ "net/http/pprof"
func initPprofMonitor() error {
pPort := global.Conf.MustInt("http_server", "pprofport", 8080)
var err error
addr := ":" + strconv.Itoa(pPort)
go func() {
err = http.ListenAndServe(addr, nil)
if err != nil {
logger.Error("funcRetErr=http.ListenAndServe||err=%s", err.Error())
}
}()
return err
}
复制代码
pprof
包会自动注册 handler, 处理相关的请求:
// src/net/http/pprof/pprof.go:71
func init() {
http.Handle("/debug/pprof/", http.HandlerFunc(Index))
http.Handle("/debug/pprof/cmdline", http.HandlerFunc(Cmdline))
http.Handle("/debug/pprof/profile", http.HandlerFunc(Profile))
http.Handle("/debug/pprof/symbol", http.HandlerFunc(Symbol))
http.Handle("/debug/pprof/trace", http.HandlerFunc(Trace))
}
复制代码
第一个路径 /debug/pprof/
下面其实还有 5 个子路径:
goroutine threadcreate heap block mutex
启动服务后,直接在浏览器访问:
就能够获得一个汇总页面:
能够直接点击上面的连接,进入子页面,查看相关的汇总信息。
关于 goroutine 的信息有两个连接,goroutine
和 full goroutine stack dump
,前者是一个汇总的消息,能够查看 goroutines 的整体状况,后者则能够看到每个 goroutine 的状态。页面具体内容的解读能够参考【大彬】的文章。
点击 profile
和 trace
则会在后台进行一段时间的数据采样,采样完成后,返回给浏览器一个 profile 文件,以后在本地经过 go tool pprof
工具进行分析。
当咱们下载获得了 profile 文件后,执行命令:
go tool pprof ~/Downloads/profile
复制代码
就能够进入命令行交互式使用模式。执行 go tool pprof -help
能够查看帮助信息。
直接使用以下命令,则不须要经过点击浏览器上的连接就能进入命令行交互模式:
go tool pprof http://47.93.238.9:8080/debug/pprof/profile
复制代码
固然也是须要前后台采集一段时间的数据,再将数据文件下载到本地,最后进行分析。上述的 Url 后面还能够带上时间参数:?seconds=60
,自定义 CPU Profiling 的时长。
相似的命令还有:
# 下载 cpu profile,默认从当前开始收集 30s 的 cpu 使用状况,须要等待 30s
go tool pprof http://47.93.238.9:8080/debug/pprof/profile
# wait 120s
go tool pprof http://47.93.238.9:8080/debug/pprof/profile?seconds=120
# 下载 heap profile
go tool pprof http://47.93.238.9:8080/debug/pprof/heap
# 下载 goroutine profile
go tool pprof http://47.93.238.9:8080/debug/pprof/goroutine
# 下载 block profile
go tool pprof http://47.93.238.9:8080/debug/pprof/block
# 下载 mutex profile
go tool pprof http://47.93.238.9:8080/debug/pprof/mutex
复制代码
进入交互式模式以后,比较经常使用的有 top
、list
、web
等命令。
执行 top
:
获得四列数据:
列名 | 含义 |
---|---|
flat | 本函数的执行耗时 |
flat% | flat 占 CPU 总时间的比例。程序总耗时 16.22s, Eat 的 16.19s 占了 99.82% |
sum% | 前面每一行的 flat 占比总和 |
cum | 累计量。指该函数加上该函数调用的函数总耗时 |
cum% | cum 占 CPU 总时间的比例 |
其余类型,如 heap 的 flat, sum, cum 的意义和上面的相似,只不过计算的东西不一样,一个是 CPU 耗时,一个是内存大小。
执行 list
,使用正则
匹配,找到相关的代码:
list Eat
复制代码
直接定位到了相关长耗时的代码处:
执行 web
(须要安装 graphviz,pprof 可以借助 grapgviz 生成程序的调用图),会生成一个 svg 格式的文件,直接在浏览器里打开(可能须要设置一下 .svg 文件格式的默认打开方式):
图中的连线表明对方法的调用,连线上的标签表明指定的方法调用的采样值(例如时间、内存分配大小等),方框的大小与方法运行的采样值的大小有关。
每一个方框由两个标签组成:在 cpu profile 中,一个是方法运行的时间占比,一个是它在采样的堆栈中出现的时间占比(前者是 flat 时间,后者则是 cumulate 时间占比);框越大,表明耗时越多或是内存分配越多。
另外,traces
命令还能够列出函数的调用栈:
除了上面讲到的两种方式(报告生成、命令行交互),还能够在浏览器里进行交互。先生成 profile 文件,再执行命令:
go tool pprof --http=:8080 ~/Downloads/profile
复制代码
进入一个可视化操做界面:
点击菜单栏能够在:Top/Graph/Peek/Source 之间进行切换,甚至能够看到火焰图(Flame Graph):
它和通常的火焰图相比恰好倒过来了,调用关系的展示是从上到下。形状越长,表示执行时间越长。注:我这里使用的 go 版本是 1.13,更老一些的版本 pprof 工具不支持 -http
的参数。固然也能够下载其余的库查看火焰图,例如:
go get -u github.com/google/pprof
或者
go get github.com/uber/go-torch
复制代码
我在参考资料部分给出了一些使用 pprof 工具进行性能分析的实战文章,能够跟着动手实践一下,以后再用到本身的平时工做中。
这部分主要内容来自参考资料【Ross Cox】,学习一下大牛的优化思路。
事情的原由是这样的,有人发表了一篇文章,用各类语言实现了一个算法,结果用 go 写的程序很是慢,而 C++ 则最快。而后 Russ Cox 就鸣不平了,哪受得了这个气?立刻启用 pprof 大杀器进行优化。最后,程序不只更快,并且使用的内存更少了!
首先,增长 cpu profiling 的代码:
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
func main() {
flag.Parse()
if *cpuprofile != "" {
f, err := os.Create(*cpuprofile)
if err != nil {
log.Fatal(err)
}
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
}
...
}
复制代码
使用 pprof 观察耗时 top5
的函数,发现一个读 map 的函数耗时最长:mapaccess1_fast64
,而它出如今一个递归函数中。
一眼就能看到框最大的 mapacess1_fast64
函数。执行 web mapaccess1
命令,更聚焦一些:
调用 mapaccess1_fast64
函数最多的就是 main.FindLoops 和 main.DFS,是时候定位到具体的代码了,执行命令:list DFS
,定位到相关的代码。
优化的方法是将 map 改为 slice,能这样作的缘由固然和 key 的类型是 int 并且不是太稀疏有关。
The take away will be that for smaller data sets, you shouldn’t use maps where slices would suffice, as maps have a large overhead.
修改完以后,再次经过 cpu profiling,发现递归函数的耗时已经不在 top5 中了。可是新增了长耗时函数:runtime.mallocgc,占比 54.2%,而这和分存分配以及垃圾回收相关。
下一步,增长采集内存数据的代码:
var memprofile = flag.String("memprofile", "", "write memory profile to this file")
func main() {
// …………
FindHavlakLoops(cfgraph, lsgraph)
if *memprofile != "" {
f, err := os.Create(*memprofile)
if err != nil {
log.Fatal(err)
}
pprof.WriteHeapProfile(f)
f.Close()
return
}
// …………
}
复制代码
继续经过 top5
、list
命令找到内存分配最多的代码位置,发现这回是向 map 里插入元素使用的内存比较多。改进方式一样是用 slice 代替 map,但 map 还有一个特色是能够重复插入元素,所以新写了一个向 slice 插入元素的函数:
func appendUnique(a []int, x int) []int {
for _, y := range a {
if x == y {
return a
}
}
return append(a, x)
}
复制代码
好了,如今程序比最初的时候快了 2.1 倍。再次查看 cpu profile 数据,发现 runtime.mallocgc
降了一些,但仍然占比 50.9%。
Another way to look at why the system is garbage collecting is to look at the allocations that are causing the collections, the ones that spend most of the time in mallocgc.
所以须要查看垃圾回收到底在回收哪些内容,这些内容就是致使频繁垃圾回收的“罪魁祸首”。
使用 web mallocgc
命令,将和 mallocgc 相关的函数用矢量图的方式展示出来,可是有太多样本量不多的节点影响观察,增长过滤命令:
go tool pprof --nodefraction=0.1 profile
复制代码
将少于 10%
的采样点过滤掉,新的矢量图能够直观地看出,FindLoops
触发了最多的垃圾回收操做。继续使用命令 list FindLoops
直接找到代码的位置。
原来,每次执行 FindLoops
函数时,都要 make
一些临时变量,这会加剧垃圾回收器的负担。改进方式是增长一个全局变量 cache,能够重复利用。坏处是,如今不是线程安全的了。
使用 pprof 工具进行的优化到这就结束了。最后的结果很不错,基本上能达到和 C++ 同等的速度和同等的内存分配大小。
咱们能获得的启发就是先使用 cpu profile 找出耗时最多的函数,进行优化。若是发现 gc 执行比较多的时候,找出内存分配最多的代码以及引起内存分配的函数,进行优化。
原文很精彩,虽然写做时间比较久远(最初写于 2011 年)了,但仍然值得一看。另外,参考资料【wolfogre】的实战文章也很是精彩,并且用的招式和这篇文章差很少,可是你能够运行文章提供的样例程序,一步步地解决性能问题,颇有意思!
内存分配既能够发生在堆上也能够在栈上。堆上分配的内存须要垃圾回收或者手动回收(对于没有垃圾回收的语言,例如 C++),栈上的内存则一般在函数退出后自动释放。
Go 语言经过逃逸分析会将尽量多的对象分配到栈上,以使程序能够运行地更快。
这里说明一下,有两种内存分析策略:一种是当前的(这一次采集)内存或对象的分配,称为 inuse
;另外一种是从程序运行到如今全部的内存分配,无论是否已经被 gc 过了,称为 alloc
。
As mentioned above, there are two main memory analysis strategies with pprof. One is around looking at the current allocations (bytes or object count), called inuse. The other is looking at all the allocated bytes or object count throughout the run-time of the program, called alloc. This means regardless if it was gc-ed, a summation of everything sampled.
加上 -sample_index
参数后,能够切换内存分析的类型:
go tool pprof -sample_index=alloc_space http://47.93.238.9:8080/debug/pprof/heap
复制代码
共有 4 种:
类型 | 含义 |
---|---|
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) |
参考资料【大彬 实战内存泄露】讲述了如何经过相似于 diff 的方式找到先后两个时刻多出的 goroutine,进而找到 goroutine 泄露的缘由,并无直接使用 heap 或者 goroutine 的 profile 文件。一样推荐阅读!
pprof
是进行 Go 程序性能分析的有力工具,它经过采样、收集运行中的 Go 程序性能相关的数据,生成 profile 文件。以后,提供三种不一样的展示形式,让咱们能更直观地看到相关的性能数据。
获得性能数据后,可使用 top
、web
、list
等命令迅速定位到相应的代码处,并进行优化。
“过早的优化是万恶之源”。实际工做中,不多有人会关注性能,但当你写出的程序存在性能瓶颈,qa 压测时,qps 上不去,为了展现一下技术实力,仍是要经过 pprof 观察性能瓶颈,进行相应的性能优化。
【Russ Cox 优化过程,并附上代码】blog.golang.org/profiling-g…
【google pprof】github.com/google/ppro…
【使用 pprof 和火焰图调试 golang 应用】cizixs.com/2017/09/11/…
【资源合集】jvns.ca/blog/2017/0…
【Profiling your Golang app in 3 steps】coder.today/tech/2018-1…
【案例,压测 Golang remote profiling and flamegraphs】matoski.com/article/gol…
【煎鱼 pprof】segmentfault.com/a/119000001…
【鸟窝 pprof】colobu.com/2017/03/02/…
【关于 Go 的 7 种性能分析方法】blog.lab99.org/post/golang…
【pprof 比较全】juejin.im/entry/5ac9c…
【经过实例来说解分析、优化过程】artem.krylysov.com/blog/2017/0…
【Go 做者 Dmitry Vyukov】github.com/golang/go/w…
【wolfogre 很是精彩的实战文章】blog.wolfogre.com/posts/go-pp…
【dave.cheney】dave.cheney.net/high-perfor…
【实战案例】www.cnblogs.com/sunsky303/p…
【大彬 实战内存泄露】segmentfault.com/a/119000001…
【查找内存泄露】www.freecodecamp.org/news/how-i-…
【雷神 3 性能优化】diducoder.com/sotry-about…