We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
通常,我们只会在两种情况下,会去分析一个程序的表现:
好了,开个玩笑,其实研究程序的性能对于每一个工程师来说,都很重要,我甚至可以这么说:这是一个工程师的必备技能。
下面来说说,我们如何去研究 Golang 程序的性能问题。
之前我也在 穷人的程序性能分析器 介绍过 C++ 的性能分析,以及很久之前也介绍过 Node.js 性能分析之火焰图 ,那么今天就轮到 Golang 了。
相比之下,Golang 的性能分析工具 pprof 可谓是豪华了,它内建支持以下几种分析:
pprof
heap
goroutine
mutex
凭借良好的工具带来的调试体验也是非常棒的,整个过程只需几个简单的命令,你就能进行分析个大概了。不过受限于篇幅,以及之前也多次提到过 CPU 的分析,因此今天只说说如何分析内存,也就是 Heap。
Heap 的使用一般是内存泄露,或者是你想优化内存的使用。
对于内存泄露,这类问题往往难以发现与分析,因为需要监控 Go 程序本身,或者看 Linux 的 dmesg 里面的 OOM 记录才能发现。
dmesg | grep oom-killer
当你发现一次 OOM 记录时,你就要考虑给本身忽略的监控加上了,因为这种问题会复现的(但是往往难以在自己的机器以及预发布环境中复现)。如果不知道是是什么监控参数,你可以看监控数据,简单定一个比例,比如当你的程序初始化的时候占用 10% 的内存,那么一旦 Go 程序的内存使用达到一定比例比如机器内存 50% 时,就要马上进行告警了,你也可以进场分析了。
不过,也不用大费周章,因为你只需用几行简单的代码,就能给你的 Go 程序增加 pprof 支持,不会影响程序的运行,并且是支持 Web 访问的:
import ( "net/http" _ "net/http/pprof" ) func main() { go func() { http.ListenAndServe("localhost:8080", nil) }() }
然后,使用 go 提供的 pprof 工具就能进行分析了,比如对于内存泄露问题:
go tool pprof http://localhost:8080/debug/pprof/heap
就会进入 pprof 的 REPL,在这里用一些简单的命令你就能定位问题所在。不过为了更好的分析体验,有两个地方需要注意:
-trimpath
-ldflag "-s -w"
接下来的我用的实际例子是属于内存使用分析优化,由于还没遇到 OOM,先用我遇到的一个小例子来代替,因为两个问题的分析方法是一致的。
第一步,先看 top10:
top10
(pprof) top10 Showing nodes accounting for 3759.91kB, 100% of 3759.91kB total Showing top 5 nodes out of 24 flat flat% sum% cum cum% 2345.25kB 62.38% 62.38% 2345.25kB 62.38% io.ReadAll 902.59kB 24.01% 86.38% 902.59kB 24.01% compress/flate.NewWriter 0 0% 100% 902.59kB 24.01% bufio.(*Writer).Flush 0 0% 100% 902.59kB 24.01% compress/gzip.(*Writer).Write (以下省略)...
这里需要提示下,flat 表示目前最右边的调用仍旧没有被释放的空间,而 cum 表示累计 (cumulative) 申请的空间。top 的默认排序是按照 flat 排序,你可以通过参数来切换排序方式:top10 -cum 。
flat
cum
top10 -cum
如果在这里看不到什么异常的地方,那么还有别的地方可以看,因为 Golang heap 的采样统计会区分成四个部分:
你可以通过类似于 sample_index=inuse_objects 的命令来切换。
sample_index=inuse_objects
在我的这个例子中,由于我这里确定第一项 io.ReadAll 为什么会在我的程序中,但是第二项的 compress/flate.NewWriter 让我觉得有异常,但是不知到是哪里调用的。因此,在确定异常项后,第二步可以通过 tree 来进一步确认调用链条:
io.ReadAll
compress/flate.NewWriter
tree
(pprof) tree 10 compress Active filters: focus=compress Showing nodes accounting for 2354.01kB, 29.36% of 8018.09kB total Showing top 10 nodes out of 11 ----------------------------------------------------------+------------- flat flat% sum% cum cum% calls calls% + context ----------------------------------------------------------+------------- 2354.01kB 100% | compress/gzip.(*Writer).Write 1805.17kB 22.51% 22.51% 2354.01kB 29.36% | compress/flate.NewWriter 548.84kB 23.32% | compress/flate.(*compressor).init ----------------------------------------------------------+------------- 548.84kB 100% | compress/flate.(*compressor).init (inline) 548.84kB 6.85% 29.36% 548.84kB 6.85% | compress/flate.(*compressor).initDeflate ----------------------------------------------------------+------------- 2354.01kB 100% | github.com/prometheus/common/expfmt.MetricFamilyToText.func1 0 0% 29.36% 2354.01kB 29.36% | bufio.(*Writer).Flush 2354.01kB 100% | compress/gzip.(*Writer).Write ----------------------------------------------------------+------------- 548.84kB 100% | compress/flate.NewWriter 0 0% 29.36% 548.84kB 6.85% | compress/flate.(*compressor).init 548.84kB 100% | compress/flate.(*compressor).initDeflate (inline) ----------------------------------------------------------+------------- 2354.01kB 100% | bufio.(*Writer).Flush 0 0% 29.36% 2354.01kB 29.36% | compress/gzip.(*Writer).Write 2354.01kB 100% | compress/flate.NewWriter ----------------------------------------------------------+------------- 2354.01kB 100% | github.com/prometheus/common/expfmt.NewEncoder.func7 0 0% 29.36% 2354.01kB 29.36% | github.com/prometheus/common/expfmt.MetricFamilyToText 2354.01kB 100% | github.com/prometheus/common/expfmt.MetricFamilyToText.func1 ----------------------------------------------------------+------------- 2354.01kB 100% | github.com/prometheus/common/expfmt.MetricFamilyToText 0 0% 29.36% 2354.01kB 29.36% | github.com/prometheus/common/expfmt.MetricFamilyToText.func1 2354.01kB 100% | bufio.(*Writer).Flush ----------------------------------------------------------+------------- 2354.01kB 100% | github.com/prometheus/common/expfmt.encoderCloser.Encode 0 0% 29.36% 2354.01kB 29.36% | github.com/prometheus/common/expfmt.NewEncoder.func7 2354.01kB 100% | github.com/prometheus/common/expfmt.MetricFamilyToText ----------------------------------------------------------+------------- 2354.01kB 100% | xizhibei-app/controllers/internal_rpc.(*SystemCtrl).GetMetrics 0 0% 29.36% 2354.01kB 29.36% | github.com/prometheus/common/expfmt.encoderCloser.Encode 2354.01kB 100% | github.com/prometheus/common/expfmt.NewEncoder.func7 ----------------------------------------------------------+------------- 0 0% 29.36% 2354.01kB 29.36% | xizhibei-app/controllers/internal_rpc.(*SystemCtrl).GetMetrics 2354.01kB 100% | github.com/prometheus/common/expfmt.encoderCloser.Encode ----------------------------------------------------------+-------------
现在,我们基本可以确认是在我实现的 GetMetrics 中,处理 prometheus 客户端的序列化压缩时候出了点小问题(但是还没有到内存泄露的地步)。另外,这里你也可以加个第三步:用 list 加上关键词的命令来查看精确到每一行代码级别的分析。
GetMetrics
list
定位到问题后,就是最后一步解决,我的解决方案是用 sync.Pool。在之前,我是直接使用 gzip.NewWriter 来压缩每次从 prometheus 中取出的指标文本,但是这样会造成 gzip 多次重复的内存申请以及初始化,所以当改用 sync.Pool 后,我的代码从:
sync.Pool
gzip.NewWriter
gzip
buf := new(bytes.Buffer) gzipWritter := gzip.NewWriter(buf)
变为:
var ( gzipPool = sync.Pool{ New: func() interface{} { return gzip.NewWriter(nil) }, } bufferPool = sync.Pool{ New: func() interface{} { return new(bytes.Buffer) }, } ) ... gzipWritter := gzipPool.Get().(*gzip.Writer) defer gzipPool.Put(gzipWritter) buf := bufferPool.Get().(*bytes.Buffer) defer bufferPool.Put(buf) buf.Reset() gzipWritter.Reset(buf)
我们可以写个 benchmark 来测试下:
goos: linux goarch: amd64 cpu: Intel(R) Core(TM) i9-9820X CPU @ 3.30GHz BenchmarkEncode-20 2422 504022 ns/op 851822 B/op 129 allocs/op BenchmarkEncodeWithSyncPool-20 7654 150188 ns/op 48799 B/op 108 allocs/op
可以看到,内存的 allocs 从 129 降到了 108。
allocs
好了,分析就暂时到这。
对于大多数人来说,在网页上用鼠标点击分析问题更简单,因为目前 Go pprof 这个工具做到了一条龙服务,你可以直接在网页上看到调用图表以及火焰图(这里需要着重艾特下 C/C++,咱还能不能把调试体验做好点了)。
go tool pprof -http=:6000 http://localhost:8080/debug/pprof/heap
Go 会打开一个本地 6000 端口的网页,但如果你在云服务器上,你有两种选择:
wget http://localhost:8080/debug/pprof/heap
ssh -L 8080:127.0.0.1:8080 user@server
The text was updated successfully, but these errors were encountered:
No branches or pull requests
通常,我们只会在两种情况下,会去分析一个程序的表现:
好了,开个玩笑,其实研究程序的性能对于每一个工程师来说,都很重要,我甚至可以这么说:这是一个工程师的必备技能。
下面来说说,我们如何去研究 Golang 程序的性能问题。
介绍
之前我也在 穷人的程序性能分析器 介绍过 C++ 的性能分析,以及很久之前也介绍过 Node.js 性能分析之火焰图 ,那么今天就轮到 Golang 了。
相比之下,Golang 的性能分析工具
pprof
可谓是豪华了,它内建支持以下几种分析:heap
相对的,是 CPU 的采样,可以用来分析程序的耗时瓶颈;goroutine
的栈追踪;mutex
栈追踪;凭借良好的工具带来的调试体验也是非常棒的,整个过程只需几个简单的命令,你就能进行分析个大概了。不过受限于篇幅,以及之前也多次提到过 CPU 的分析,因此今天只说说如何分析内存,也就是 Heap。
Heap 的使用一般是内存泄露,或者是你想优化内存的使用。
内存泄露与内存优化
对于内存泄露,这类问题往往难以发现与分析,因为需要监控 Go 程序本身,或者看 Linux 的 dmesg 里面的 OOM 记录才能发现。
dmesg | grep oom-killer
当你发现一次 OOM 记录时,你就要考虑给本身忽略的监控加上了,因为这种问题会复现的(但是往往难以在自己的机器以及预发布环境中复现)。如果不知道是是什么监控参数,你可以看监控数据,简单定一个比例,比如当你的程序初始化的时候占用 10% 的内存,那么一旦 Go 程序的内存使用达到一定比例比如机器内存 50% 时,就要马上进行告警了,你也可以进场分析了。
不过,也不用大费周章,因为你只需用几行简单的代码,就能给你的 Go 程序增加 pprof 支持,不会影响程序的运行,并且是支持 Web 访问的:
然后,使用 go 提供的
pprof
工具就能进行分析了,比如对于内存泄露问题:就会进入 pprof 的 REPL,在这里用一些简单的命令你就能定位问题所在。不过为了更好的分析体验,有两个地方需要注意:
-trimpath
以及-ldflag "-s -w"
,最好去掉,不然会影响到你定位问题;接下来的我用的实际例子是属于内存使用分析优化,由于还没遇到 OOM,先用我遇到的一个小例子来代替,因为两个问题的分析方法是一致的。
如何使用 pprof
第一步,先看
top10
:这里需要提示下,
flat
表示目前最右边的调用仍旧没有被释放的空间,而cum
表示累计 (cumulative) 申请的空间。top 的默认排序是按照 flat 排序,你可以通过参数来切换排序方式:top10 -cum
。如果在这里看不到什么异常的地方,那么还有别的地方可以看,因为 Golang heap 的采样统计会区分成四个部分:
你可以通过类似于
sample_index=inuse_objects
的命令来切换。在我的这个例子中,由于我这里确定第一项
io.ReadAll
为什么会在我的程序中,但是第二项的compress/flate.NewWriter
让我觉得有异常,但是不知到是哪里调用的。因此,在确定异常项后,第二步可以通过tree
来进一步确认调用链条:现在,我们基本可以确认是在我实现的
GetMetrics
中,处理 prometheus 客户端的序列化压缩时候出了点小问题(但是还没有到内存泄露的地步)。另外,这里你也可以加个第三步:用list
加上关键词的命令来查看精确到每一行代码级别的分析。定位到问题后,就是最后一步解决,我的解决方案是用
sync.Pool
。在之前,我是直接使用gzip.NewWriter
来压缩每次从 prometheus 中取出的指标文本,但是这样会造成gzip
多次重复的内存申请以及初始化,所以当改用sync.Pool
后,我的代码从:变为:
我们可以写个 benchmark 来测试下:
可以看到,内存的
allocs
从 129 降到了 108。好了,分析就暂时到这。
P.S.
对于大多数人来说,在网页上用鼠标点击分析问题更简单,因为目前 Go pprof 这个工具做到了一条龙服务,你可以直接在网页上看到调用图表以及火焰图(这里需要着重艾特下 C/C++,咱还能不能把调试体验做好点了)。
Go 会打开一个本地 6000 端口的网页,但如果你在云服务器上,你有两种选择:
wget http://localhost:8080/debug/pprof/heap
,然后拷贝到本地进行分析;ssh -L 8080:127.0.0.1:8080 user@server
;Ref
The text was updated successfully, but these errors were encountered: