• 程序优化有哪些方式和步骤?
  • 可供选择的调优工具有哪些?

调优之前

调优方式

要监控程序性能,有下面几种方式:

  • Timers: 计时器,用于基准测试,比较程序修改前和修改后的差异
  • Profilers: 专业的调优工具,用于更高级的程序验证

分析步骤

无论使用什么工具,都有如下的通用原则:

  • 识别更高层面的瓶颈

    • 例如:发现了一个长时间运行的函数
  • 减少操作量

    • 找出可替换的方法,减少时间消耗,或者调用次数
    • 找出可替换的方法,减少内存分配量
  • 向下挖掘数据

    • 使用工具,找出更底层、更细节的数据

思考性能更好的算法或者数据结构;找到更简单的处理方式;从实效角度审视你的代码

性能分析相关概要

首先,性能概要的定义:

性能概要(profile)是由多个堆栈(Wiki&BaiKe)跟踪组成的。堆栈跟踪能提供特殊事件(比如,内存分配)的调用顺序。包(Packages)可以创建、维护各自的性能概要信息。最常见的用途就是追踪资源的使用,而这些资源使用后必须被明确的关闭(closed),比如文件的操作、网络连接的关闭。- pkg/runtime/pprof

CPU 性能分析

最常用的就是CPU性能分析,当CPU性能分析启用后,Go runtime 会每 10ms 就暂停一下,记录当前运行的 Go runtime 的调用堆栈及相关数据。当性能分析数据保存到硬盘后,就可以分析代码中的热点了。

一个函数如果出现在数据中的次数越多,就越说明这个函数调用栈占用了更多的运行时间。

内存性能分析

内存性能分析则是在 堆(Heap)分配 的时候,记录一下调用堆栈。默认情况下,每 1000 次分配,取样一次,这个数值可以改变。

栈(Stack)分配 由于会随时释放,因此不会被内存分析所记录。

由于内存分析是取样方式,并且因为其记录的是 分配内存, 而不是使用内存。因此使用内存性能分析工具来准确判断程序具体的内存使用时比较困难的

阻塞性能分析

阻塞分析是一个很独特的分析。它有点类似于CPU性能分析,但是它所记录的是 goroutine 等待资源所花费的时间。

阻塞分析对分析程序 并发瓶颈 非常有帮助。阻塞性能分析可以显示出什么时候出现了大批的 goroutine 被阻塞了。阻塞包括:

  • 发送,接收无缓冲的 channel
  • 发送给一个满缓冲的 channel,或者从一个空缓冲的 channel 接收
  • 试图获取已被另一个 goroutine 锁定的 sync.Mutex 的锁 🔒

阻塞性能分析是特殊的分析工具,在排除CPU和内存瓶颈前,不应该用它来分析

一次只分析一个东西

性能分析不是没有开销的。虽然性能分析对程序的影响并不严重,但是毕竟有影响,特别是内存分析的时候增加采样率的情况下。大多数工具甚至直接就不允许你同时开启多个性能分析工具。如果你同时开启了多个性能分析工具,那很有可能会出现他们互相观察对方的开销从而导致你的分析结果彻底失去意义。

So, 一次只分析一个东西。

对函数分析

最简单的对一个函数进行性能分析的办法就是使用 testing 包。testing 包内置支持生成CPU、内存、阻塞的性能分析数据

  • -cpuprofile=xxx: 生成CPU性能分析数据,并写入文件 xxx
  • -memprofile=xxx: 生成内存性能分析数据,并写入文件 xxx
    • -memprofilerate=N: 调整采样率为 1/N
  • -blockprofile=xxx: 生成阻塞性能分析数据,并写入文件 xxx

如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$ go test -run=xxx -bench=IndexByte -cpuprofile=/tmp/cup.p bytes
goos: darwin
goarch: amd64
pkg: bytes
BenchmarkIndexByte/10-4         200000000                6.44 ns/op     1553.83 MB/s
BenchmarkIndexByte/32-4         200000000                7.41 ns/op     4318.84 MB/s
BenchmarkIndexByte/4K-4         10000000               210 ns/op        19455.95 MB/s
BenchmarkIndexByte/4M-4             5000            321910 ns/op        13029.39 MB/s
BenchmarkIndexByte/64M-4             300           5406798 ns/op        12411.94 MB/s
BenchmarkIndexBytePortable/10-4                 100000000               13.8 ns/op       722.79 MB/s
BenchmarkIndexBytePortable/32-4                 30000000                44.9 ns/op       712.86 MB/s
BenchmarkIndexBytePortable/4K-4                   500000              2910 ns/op        1407.32 MB/s
BenchmarkIndexBytePortable/4M-4                      500           2979323 ns/op        1407.80 MB/s
BenchmarkIndexBytePortable/64M-4                      30          47259940 ns/op        1419.99 MB/s
PASS
ok      bytes   18.689s

Note: 这里的 -run=xxx 是说只运行 Benchmarks,而不要运行任何 Tests

然后使用 go tool pprof 来分析

1
$ go tool pprof bytes.test /tmp/cpu.p

对整个应用分析

testing 适用于分析具体某个函数,但是如果想要分析整个应用,则可以使用 runtime/pprof 包。当然,这是比较底层的,Dave Cheney 在几年前还写了个包 github.com/pkg/profile,可以简化使用。

该包的使用时,只需要在 main 函数中加入 defer profile.Start().Stop() 即可:

1
2
3
4
5
6
import "github.com/pkg/profile"

func main() {
    defer profile.Start().Stop()
    ...
}

性能分析注意事项

性能分析必须在一个 可重复的、稳定的环境 中来进行

  • 机器 必须闲置

    • 不要在共享硬件上进行性能分析
    • 不要在性能分析期间,在同一个机器上去浏览网页!!!
  • 注意省电模式和过热保护,如果突然进入这些模式,会导致分析数据严重不准确

  • 不要使用虚拟机、共享的云主机, 太多的干扰因素,分析数据会很不一致

  • 不要在 macOS 10.11 及以前的版本运行性能分析,有 bug,之后的版本修复了

如果承受得起,购买专用的性能测试分析的硬件设备,上架。

  • 关闭电源管理、过热管理;
  • 绝不要升级,以保证测试的一致性,以及具有可比性。

如果没有这样的环境,那就一定要在多个环境中,执行多次,以取得可参考的、具有相对一致性的测试结果。

Note: 性能分析的采样数据尽可能保证在压测的环境下收集

性能分析工具

Go 性能分析工具

  • 方法一:time 命令
  • 方法二:GODEBUG
  • 方法三:pprof
  • 方法四:/debug/pprof
  • 方法五:perf
  • 方法六:火焰图(Flame Graph)
  • 方法七:go tool trace

See Also

Thanks to the authors 🙂