前言
最近用 Golang 实现了一个日志搜集上报程序(内部称 logger
项目),线上灰度测试过程发现 logger
占用 CPU 非常高(80% - 100%)。而此项目之前就在线上使用,用于消费 NSQ
任务, CPU 占用一直在 1%,最近的修改只是添加了基于磁盘队列的生产者消费者服务,生产者使用 go-gin 实现了一个 httpserver,接收数据后写入磁盘队列;消费者为单个 goroutine 循环 POST
数据。而 httpserver 压力不大(小于 100 QPS),不至于占用如此高的 CPU,大致 review 代码后未发现异常,借助 pprof
和 flame-graph
来分析定位问题。
pprof
pprof
我理解是 program profile(即程序性能剖析之意),Golang 提供的两个官方包
runtime/pprof,
net/http/pprof
能方便的采集程序运行的堆栈、goroutine、内存分配和占用、io 等信息的 .prof
文件,然后可以使用 go tool pprof 分析 .prof
文件。两个包的作用是一样的,只是使用方式的差异。
runtime/pprof
如果程序为非 httpserver 类型,使用此方式;在 main 函数中嵌入如下代码:
1 | import "runtime/pprof" |
运行程序
1 | ./logger -cpuprofile cpu.prof -memprofile mem.prof |
可以得到 cpu.prof 和 mem.prof 文件,使用 go tool pprof 分析。
1 | go tool pprof logger cpu.prof |
net/http/pprof
如果程序为 httpserver 类型, 则只需要导入该包:
1 | import _ "net/http/pprof" |
如果 httpserver 使用 go-gin
包,而不是使用默认的 http
包启动,则需要手动添加 /debug/pprof
对应的 handler,github 有封装好的模版:
1 | import "github.com/DeanThompson/ginpprof" |
导入包重新编译程序后运行,在浏览器中访问 http://host:port/debug/pprof
可以看到如下信息,这里 host
和 port
是程序绑定的 host
和 port
,例如我自己的 logger 程序,访问如下地址:
http://127.0.0.1:4500/debug/pprof/
1 | /debug/pprof/ |
点击对应的 profile 可以查看具体信息,通过浏览器查看的数据不能直观反映程序性能问题,go tool pprof
命令行工具提供了丰富的工具集:
查看 heap 信息
1 | go tool pprof http://127.0.0.1:4500/debug/pprof/heap |
查看 30s 的 CPU 采样信息
1 | go tool pprof http://127.0.0.1:4500/debug/pprof/profile |
其他功能使用参见
官方 net/http/pprof 库
pprof CPU 分析
采集 profile 数据之后,可以分析 CPU 热点代码。
执行下面命令:
1 | go tool pprof http://127.0.0.1:4500/debug/pprof/profile |
会采集 30s 的 profile 数据,之后进入终端交互模式,输入 top
指令。
1 | ~ # go tool pprof http://127.0.0.1:4500/debug/pprof/profile |
从统计可以 top5
操作全是数据压缩操作, logger 程序本身开启了压缩等级为 9 的 gzip 压缩,如果希望减少压缩 CPU 占用,可以调整压缩等级。
pprof mem 分析
同时 pprof 也支持内存相关数据分析
--inuse_space
分析常驻内存
1 | go tool pprof -alloc_space http://127.0.0.1:4500/debug/pprof/heap |
1 | ~ # go tool pprof -alloc_space http://127.0.0.1:4500/debug/pprof/heap |
--alloc_objects
分析临时内存
1 | go tool pprof -inuse_space http://127.0.0.1:4500/debug/pprof/heap |
1 | ~ # go tool pprof -inuse_space http://127.0.0.1:4500/debug/pprof/heap |
通过常驻内存和临时内存分配 top 值,可以查看当前程序的内存占用情况和热点内存使用的代码,结合代码分析热点代码是否存在 bug、是否有优化的空间。
go-torch
通过上面的 go tool pprof
工具和 top 指令,我们能定位出程序的热点代码,但缺乏对程序运行情况的整体感知,能不能有类似火焰图
的效果让我们对整个堆栈统计信息有个一目了然的效果呢?这里要感谢 uber
开源的工具 go-torch,能让我们将 profile 信息转换成火焰图,具体安装和使用过程见项目的介绍。
安装好 go-torch 后,运行
1 | go-torch -u http://127.0.0.1:4500 |
生成 CPU 火焰图
从图中能一眼看到 publish 函数中的压缩操作占了 70% 左右的 CPU。
而 gin httpserver 只占用了 2% 左右的 CPU, 和我们使用 go tool pprof
的 top 命令分析的结果一致。
默认情况下 go-torch 采集的是 CPU 的 profile, 这里介绍下 mem 火焰图的采集。
inuse_space
火焰图
1 | go-torch -inuse_space http://127.0.0.1:4500/debug/pprof/heap --colors=mem |
alloc_space
火焰图
1 | go-torch -alloc_space http://127.0.0.1:4500/debug/pprof/heap --colors=mem |
logger 100% CPU 分析
前面介绍了 go tool pprof
和火焰图的使用方法,这里使用火焰图复现 logger 100% CPU 问题。
先看现象, 用 wrk 压测 logger
1 | wrk -t1 -c100 -d30 --script=post.lua 'http://127.0.0.1:4500/marco/log' |
查看 CPU 占用情况
采集 30s 的 CPU profile 火焰图
图中红色标记部分 startSink
函数中 runtime.selectgo
消耗了大量 CPU, 而 runtime.selectgo
上面只有 runtime.sellock
和 runtime.selunlock
两个操作,即大量 CPU 耗费在 select
操作上,火焰图呈秃顶状态,即瓶颈所在。
查看 startSink
实现
1 | for { |
本希望通过 moveforward 来控制 fakeRead 是否取值,而如果 fakeRead 为 nil 时, 整个 select 会一直阻塞,所以加上了 default
操作,让 select 变成非阻塞,但因为一直没有读取内容,count 没有增加而不会触发 sleep 操作。最终导致非阻塞的 select 一直空转循环,类似一个空 while 循环,占用了大量 CPU。
优化
改用其他方法实现这部分逻辑,这里不再贴出来了,重在分享发现问题的过程,改进后的火焰图在前面已给出。
总结
Golang 应用通常只要能编译通过,很少有运行时问题;而当应用遇到高CPU 、高内存占用或者作为 http 服务端响应时间长,QPS 上不去等,且不能 code review 解决时,可以尝试使用pprof
和 Flame-Graph
来分析定位问题,有奇效。当然 Golang 程序的调试及调优还有很多方法,比如直接结合go test
和 benchmark
通过测用例分析热点代码、使用go pprof
分析汇编代码等。