1. 如何分析程序的运行时间与CPU利用率情况?
1.1 shell自带的time指令
time go run xxx.go
real:从程序开始到结束,实际度过的时间;user:程序在用户态度过的时间;sys:程序在内核态度过的时间。
一般情况下 real >= user + sys,因为系统还有其它进程(进程切换占用时间)。
1.2 /usr/bin/time指令
使用时需要用绝对路径,加上参数-v
/usr/bin/time go run xxx.go
2. 如何分析golang程序的内存使用情况?
2.1 demo引入
package main
import (
"log"
"runtime"
"time"
)
func test() {
//slice 会动态扩容,用slice来做堆内存申请
container := make([]int, 8)
log.Println(" ===> loop begin.")
for i := 0; i < 32*1000*1000; i++ {
container = append(container, i)
}
log.Println(" ===> loop end.")
}
func main() {
log.Println("Start.")
test()
log.Println("force gc.")
runtime.GC() //强制调用gc回收
log.Println("Done.")
time.Sleep(3600 * time.Second) //睡眠,保持程序不退出
}
编译,运行
go build -o mem ./main.go
./mem
在./mem进程没有执行完时,再开一个窗口,通过top命令查看进程的内存占用情况
top -p $(pidof mem)
进程占用了1G内存。
按理说程序在test()函数执行完后,切片contaner的内存应该被释放,不应该占用1G那么大。
下面让我们使用GODEBUG来分析程序的内存使用情况。
2.2 GODEBUG与gctrace
执行mem程序之前添加环境变量GODEBUG='gctrace=1'来跟踪打印垃圾回收器信息
$ GODEBUG='gctrace=1' ./mem
设置gctrace=1会使得垃圾回收器在每次回收时汇总所回收内存的大小以及耗时,并将这些内容汇总成单行内容打印到标准错误输出中。
格式
gc # @#s #%: #+#+# ms clock, #+#/#/#+# ms cpu, #->#-># MB, # MB goal, # P
含义
gc # GC次数的编号,每次GC时递增
@#s 距离程序开始执行时的时间
#% GC占用的执行时间百分比
#+...+# GC使用的时间
#->#-># MB GC开始,结束,以及当前活跃堆内存的大小,单位M
# MB goal 全局堆内存大小
# P 使用processor的数量
如果每条信息最后,以(forced)结尾,那么该信息是由runtime.GC()调用触发
我们选择其中一行来解释一下:
gc 17 @0.149s 1%: 0.004+36+0.003 ms clock, 0.009+0/0.051/36+0.006 ms cpu, 181->181->101 MB, 182 MB goal, 2 P
gc 17: Gc 调试编号为17@0.149s:此时程序已经执行了0.149s1%: 0.149s中其中gc模块占用了1%的时间0.004+36+0.003 ms clock: 垃圾回收的时间,分别为STW清扫的时间+并发标记和扫描的时间+STW标记的时间0.009+0/0.051/36+0.006 ms cpu: 垃圾回收占用cpu时间181->181->101 MB: GC开始前堆内存181M, GC结束后堆内存181M,当前活跃的堆内存101M182 MB goal: 全局堆内存大小2 P: 本次GC使用了2个P(调度器中的Processer)
分析
test()函数执行完后打印的gc 17那行的信息。516->516->0 MB, 1032 MB goal表示垃圾回收器已经把516M的内存标记为非活跃的内存。
再看下一个记录gc 18。0->0->0 MB, 4 MB goal表示垃圾回收器中的全局堆内存大小由1032M下降为4M。
结论
1、在test()函数执行完后,demo程序中的切片容器所申请的堆空间都被垃圾回收器回收了。
2、如果此时在top指令查询内存的时候,如果依然是800+MB,说明垃圾回收器回收了应用层的内存后,(可能)并不会立即将内存归还给系统。
2.3 runtime.ReadMemStats
换一种方式查看内存
package main
import (
"log"
"runtime"
"time"
)
func readMemStats() {
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
log.Printf(" ===> Alloc:%d(bytes) HeapIdle:%d(bytes) HeapReleased:%d(bytes)", ms.Alloc, ms.HeapIdle, ms.HeapReleased)
}
func test() {
//slice 会动态扩容,用slice来做堆内存申请
container := make([]int, 8)
log.Println(" ===> loop begin.")
for i := 0; i < 32*1000*1000; i++ {
container = append(container, i)
if ( i == 16*1000*1000) {
readMemStats()
}
}
log.Println(" ===> loop end.")
}
func main() {
log.Println(" ===> [Start].")
readMemStats()
test()
readMemStats()
log.Println(" ===> [force gc].")
runtime.GC() //强制调用gc回收
log.Println(" ===> [Done].")
readMemStats()
go func() {
for {
readMemStats()
time.Sleep(10 * time.Second)
}
}()
time.Sleep(3600 * time.Second) //睡眠,保持程序不退出
}
封装函数readMemStats(),调用runtime中的ReadMemStats()方法获得内存信息,通过log打印出来。
可以看到,打印[Done].之后那条trace信息,Alloc已经下降,即内存已被垃圾回收器回收。在最后两条trace信息中,HeapReleased开始上升,即垃圾回收器把内存归还给系统。
2.4 pprof工具
在原先代码中添加个携程启动pprof即可
package main
import (
"log"
"runtime"
"time"
"net/http"
_ "net/http/pprof"
)
func readMemStats() {
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
log.Printf(" ===> Alloc:%d(bytes) HeapIdle:%d(bytes) HeapReleased:%d(bytes)", ms.Alloc, ms.HeapIdle, ms.HeapReleased)
}
func test() {
//slice 会动态扩容,用slice来做堆内存申请
container := make([]int, 8)
log.Println(" ===> loop begin.")
for i := 0; i < 32*1000*1000; i++ {
container = append(container, i)
if ( i == 16*1000*1000) {
readMemStats()
}
}
log.Println(" ===> loop end.")
}
func main() {
//启动pprof
go func() {
log.Println(http.ListenAndServe("0.0.0.0:10000", nil))
}()
log.Println(" ===> [Start].")
readMemStats()
test()
readMemStats()
log.Println(" ===> [force gc].")
runtime.GC() //强制调用gc回收
log.Println(" ===> [Done].")
readMemStats()
go func() {
for {
readMemStats()
time.Sleep(10 * time.Second)
}
}()
time.Sleep(3600 * time.Second) //睡眠,保持程序不退出
}
正常运行程序,同时打开:http://127.0.0.1:10000/debug/pprof/heap?debug=1
浏览器的内容其中有一部分如下,记录了目前的内存情况
3. 如何分析Golang程序的CPU性能情况?
package main
import (
"bytes"
"log"
"math/rand"
"net/http"
_ "net/http/pprof"
"time"
)
func test() {
log.Println(" ===> loop begin.")
for i := 0; i < 1000; i++ {
log.Println(genSomeBytes())
}
log.Println(" ===> loop end.")
}
//生成一个随机字符串
func genSomeBytes() *bytes.Buffer {
var buff bytes.Buffer
for i := 1; i < 20000; i++ {
buff.Write([]byte{'0' + byte(rand.Intn(10))})
}
return &buff
}
func main() {
go func() {
for {
test()
time.Sleep(time.Second * 1)
}
}()
http.ListenAndServe("0.0.0.0:10000", nil)
}
main()里的流程很简单,启动一个goroutine去无限循环调用test()方法,休眠1s.
test()的流程是生成1000个20000个字符的随机字符串.并且打印.
我们将上面的代码编译成可执行的二进制文件 demo
go build -o demo ./main.go
启动程序,无限循环的打印字符串.
接下来我们通过几种方式来查看进程的cpu性能情况.
3.1 Web界面查看
浏览器访问http://127.0.0.1:10000/debug/pprof/
这里面能够通过pprof查看包括(阻塞信息、cpu信息、内存堆信息、锁信息、goroutine信息等等)
我们要是想得到cpu性能,就是要获取到当前进程的profile文件,这个文件默认是30s生成一个,所以你的程序要至少运行30s以上(参数也可以修改,稍后我们介绍)
我们可以直接点击网页的profile,浏览器会给我们下载一个profile文件.
把profile文件和demo文件放一个目录下
3.2 使用pprof工具查看
go tool pprof [binary] [profile]
go tool pprof demo profile
可以通过top查看cpu详细情况
- flat:当前函数占用CPU的耗时
- flat%::当前函数占用CPU的耗时百分比
- sum%:函数占用CPU的耗时累计百分比
- cum:当前函数加上调用当前函数的函数占用CPU的总耗时
- cum%:当前函数加上调用当前函数的函数占用CPU的总耗时百分比
- 最后一列:函数名称
3.3 通过go tool pprof得到profile文件
上面的profile文件是通过web浏览器下载的,这个profile的经过时间是30s的,默认值我们在浏览器上修改不了,如果你想得到时间更长的cpu利用率,可以通过go tool pprof指令与程序交互来获取到
首先启动程序后打开一个新的终端
go tool pprof http://localhost:10000/debug/pprof/profile?seconds=60
这里制定了生成profile文件的时间间隔60s
60s之后, 终端就会有结果出来,可以使用top来查看.
3.4 可视化查看
yum install graphviz
进入profile文件查看,输入web指令.
go tool pprof demo profile
(pprof) web
这样我们就能比较清晰的看到函数之间的调用关系,方块越大的表示cpu的占用越大.