分析与追踪

分析与追踪

这里主要关注两种性能分型。针对阻塞和互斥锁所做的性能分析价值不是特别大,那是针对程序中很小的一部分,例如阻塞情况而做的,所以不太能够看出程序的整体的情况,当然,接下来说明的技巧也可以用在这个上面。

  • CPU 层面的
  • 内存层面的

通过追踪不仅能程序里发生了什么,还能知道哪些事情没有发生。

  • 从 CPU 层面,我们要关注的是程序把大部分时间都耗在哪些函数上面
  • 从内存层面,要关注两项指标
    • 程序要给堆上面放多少个值,如果这样的值比较多且有效期比较短,那么垃圾收集工作就得花费很长时间才能完成。
    • 堆的总大小,能不能少用一些空间

栈追踪

程序发生 panic 时,终端会输出栈调用信息。

微观优化

关注单个函数,让这个函数运行更快,可能要查看 CPU 的使用情况,或堆内存的使用情况分析。

使用 -memprofile 对基准测试生成内存报告

1
go test -run none -bench . -benchtime 3s -benchmem -memprofile m.out

go tool pprof 可以查看涉及 CPU,内存,以及阻塞的分析文件。

1
2
3
go tool pprof -alloc_space m.out
# 通过 list 命令查看分析
list <函数名>
  • -alloc_space 查看内存分配情况
  • -alloc_objects 查看各种内容的数量
  • -inuse_sapce 默认选项

如下图所示,红框有两栏。第一栏 flat 表示该行的内存分配,假如查看的 Profile 是针对 CPU 生成的,则该栏是 CPU 的用量。第二栏 cum 表示从这一行算起,一直沿着调用栈往下累计,看看它本身及调用的代码总共分配了多少内存。

比如下图中的意思是,26 行本身没有分配内存,但是它调用栈往下累计,发生了分配 512.05kb 内存。

image-20220424150735919

也可以使用网页版

Flat 该函数本次运行耗费资源
Flat% 占总资源的比例
Sum% 该数据累计耗费资源比例
Cum 该函数及调用栈总耗费资源
Cum% 比例
Name 函数名
资源 -
inuse_spce 已分配尚未释放的内存
inuse_objects 已分配但尚未释放的对象数量
alloc_space 分配的内存总量
alloc_objects 分配的对象总数

image-20220425150026375

1
2
3
4
5
6
# 安装图形工具, 用于绘制图形
brew install graphviz
# 生成 cpu.out 文件
go test -bench . -cpuprofile cpu.out
#  见图2, 输入 web 后生成 图3
go tool pprof -http :3333 cpu.out 

image-20200414103515673

image-20200414103747452

根据图中显示, 哪一块最消耗性能, 针对修改, 使用 pprof 继续进行测试, 以完成性能优化

image-20200414104013837

宏观优化

查看程序调度信息

每 1000 毫秒生成一份调度信息

1
GODEBUG=schedtrace=1000 ./main > /dev/null

终端会输出正在运行的跟可运行的 goroutine。

  • 第一栏是追踪信息的时刻
  • 第二栏是有多少个 procs可以使用
  • 第三栏闲置的 procs 数量
  • 第四栏有多少操作系统级别的线程
  • 第五栏 spinningthreads ,指的是某个 procs 发现自己一直没事做,所以纵向从别处拿一点儿任务过来,或者就是想让自己在这里空转,防止系统把自己给切换掉
  • 第六栏是闲置的系统级线程
  • 第七栏是全局运行队列
  • 最后是本地运行队列,方括号的每个数字都表示一个逻辑处理器(procs)

image-20220424171349886

给系统增加负载做测试

1
hey -m GET -c 100 -n 10000 "http://localhost:1323/app"

如果发生了 goroutine 泄露,那么这些 goroutine 就会进入 waiting 状态,于是我们可以看到全局运行队列与局部队列里的数字就会不断增长。相反如果任务完成,这些数字都会降到 0,说明服务器是很健康的,并没有泄露什么。

查看程序垃圾收集信息

每次垃圾收集打打印一条追踪信息

1
GODEBUG=gctrace=1 ./main > /dev/null
  • gc 1gc 2… 分别表示第几次垃圾收集
  • 第二栏表示在生成这条信息时,程序已经运行了多长时间
  • 第三栏表示为了完成这次垃圾收集工作,使用了多少 CPU 资源,图中为 0%,说明任务完成的相当快
  • 第四栏显示了垃圾收集的三个阶段所花的时间,比如0.022+0.75+0.035 是 STW + Concurrent +STW,两个 STW 加起来不应该超过 100 微秒(0.1 毫秒),如果堆正在膨胀,那么偶尔可能会超过这个值,但不应该频繁发生。我们要关注的是 Concurrent 实际经过时间。
  • 第四栏是 CPU 所花的时间,最左边和最右边和上面一样,也是 STW 所花的时间,但它将中间 Concurrent 分成了三个小的部分,这里重点是检查两个 STW 所花的时间。
  • 第五栏,如果你怀疑程序有内存泄露问题,那只能通过这个地方来判断。4->5->1 MB,收集垃圾之前的堆大小,完成收集垃圾后的堆大小,最后是活跃堆的大小。

image-20220424173017710

web 应用引入 pprof 监测性能

1
2
// 引入包,仅仅执行初始化逻辑
_ "net/http/pprof"
  • allocs 对过去所有的内存分配行为采样,性能调优查看重点此项。
  • block 导致阻塞的堆栈跟踪
  • cmdline 当前程序通过什么样命令调用的
  • goroutine 所有当前 goroutine 的堆栈痕迹
  • heap 活跃对象内存分配情况,heap 是以前的老路径,allocs 是1.11 加进来了新路径。

pprof 会注册到net/http 包默认的服务上,建议单独开一个专门 pprof 的服务,端口不要暴露到公网。

分析正在运行的 Go 程序 CPU

这跟 heap 方面的数据有点区别,那个只需要根据以往的堆数据就能统计出来。但 CPU 方面的不同,要想得到有效的报告,必须给程序增加负载,默认的标准中,在生成 CPU 报告时,需要让程序运行 30 秒,以便收集足够量的数据。

如果你想自定义时间

1
http://localhost:6060/debug/pprof/profile?seconds=5

分析 CPU

1
go tool pprof -http :5050 "http://localhost:6060/debug/pprof/profile"

另一种办法做性能分析

除了命令行 go tool pprof ,还有另一种办法做性能分析。

程序会向标准输出端输出一份分析报告。 注意: 与没有做性能分析时相比,这次的程序花的时间会长一些。

1
2
3
4
5
6
7
import "runtime/pprof"

func main() {
  pprof.StartCPUProfile(os.Stdout)
  defer pprof.StopCPUProfile()
  // ...
}
1
2
3
main > cpu.out

go tool pprof cpu.out

有时,你会发现大部分性能消耗在操作系统调用上,profile 没能发挥作用,试试 trace。

用 Tracer 追踪程序运行情况

做 trace 也有很多种办法:

  • 在命令行里执行
  • benchmark 的时候增加 -trace 选项
  • 标准库里的函数

有时候我们要看的不是程序发生了什么,而是还没有发生的事情。

1
2
3
4
5
6
7
import "runtime/trace"

func main() {
 trace.Start(os.Stdout)
 defer trace.Stop()
  // ...
}

追踪和性能测试不同,它不要求程序必须停下来,只是会把每次函数调用都记录下来,而且会精确到微秒级别。

1
2
main > cpu.out
go tool trace c.out 
Licensed under CC BY-NC-SA 4.0
本文阅读量 次, 总访问量 ,总访客数
Built with Hugo .   Theme Stack designed by Jimmy